From f08cfeec0bf2193b44cc07e35efe153df913c7a4 Mon Sep 17 00:00:00 2001 From: Mohd Aquib Date: Sat, 11 Jul 2026 14:45:10 +0530 Subject: [PATCH 1/7] Implement custom theme for the sample application - Add custom `ScannerTheme` with dedicated `ScannerDarkColorScheme`, `ScannerTypography`, and `ScannerShapes`. - Define a semantic color palette including warning, info, and success colors in `Color.kt`. - Integrate Google Fonts (Inter) and configure typography settings in `Type.kt`. - Update `SampleActivity`, `FixedScreenAccessibilityTest`, and previews to use `ScannerTheme` instead of the default `MaterialTheme`. - Add `androidx.compose.ui.text.google.fonts` dependency to the sample module. - Update `.gitignore` to include Android Studio deployment target selector configurations. --- .gitignore | 3 +- .idea/deploymentTargetSelector.xml | 3 + .idea/misc.xml | 1 - gradle/libs.versions.toml | 1 + sample/build.gradle.kts | 1 + .../sample/FixedScreenAccessibilityTest.kt | 4 +- .../sample/SampleActivity.kt | 5 +- .../sample/ui/theme/Color.kt | 21 ++++++ .../sample/ui/theme/Shape.kt | 12 ++++ .../sample/ui/theme/Theme.kt | 66 +++++++++++++++++++ .../sample/ui/theme/Type.kt | 45 +++++++++++++ 11 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 sample/src/main/java/com/composea11yscanner/sample/ui/theme/Color.kt create mode 100644 sample/src/main/java/com/composea11yscanner/sample/ui/theme/Shape.kt create mode 100644 sample/src/main/java/com/composea11yscanner/sample/ui/theme/Theme.kt create mode 100644 sample/src/main/java/com/composea11yscanner/sample/ui/theme/Type.kt diff --git a/.gitignore b/.gitignore index 0e9511a..10f5ca6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ local.properties /.idea/misc.xml .claude/ .idea/ -.kotlin/errors/ \ No newline at end of file +.kotlin/errors/ +/.idea/deploymentTargetSelector.xml \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..5b63c6a 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,6 +5,9 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 74dd639..b2c751a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c25bca8..c6a3f1d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-compose-ui-text-google-fonts = { group = "androidx.compose.ui", name = "ui-text-google-fonts" } androidx-compose-ui-unit = { group = "androidx.compose.ui", name = "ui-unit" } androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index c935f61..ecf79f2 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.text.google.fonts) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) testImplementation(libs.junit) diff --git a/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt b/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt index 3876358..dd43e96 100644 --- a/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt +++ b/sample/src/androidTest/java/com/composea11yscanner/sample/FixedScreenAccessibilityTest.kt @@ -1,12 +1,12 @@ package com.composea11yscanner.sample -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.Density import com.composea11yscanner.core.model.ScannerConfig import com.composea11yscanner.rules.ScannerRules +import com.composea11yscanner.sample.ui.theme.ScannerTheme import com.composea11yscanner.ui.A11yScanner import org.junit.Assert.assertEquals import org.junit.Rule @@ -40,7 +40,7 @@ class FixedScreenAccessibilityTest { private fun assertNoScannerIssues(content: @Composable () -> Unit) { composeRule.setContent { - MaterialTheme { + ScannerTheme { content() } } diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt index 50c4ee2..8c4a434 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt @@ -75,6 +75,7 @@ import androidx.compose.ui.unit.dp import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.ScannerConfig import com.composea11yscanner.rules.ScannerRules +import com.composea11yscanner.sample.ui.theme.ScannerTheme import com.composea11yscanner.triggers.scanOnShake import com.composea11yscanner.ui.A11yNodeExtractor import com.composea11yscanner.ui.A11yScannerController @@ -85,7 +86,7 @@ class SampleActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - MaterialTheme { + ScannerTheme { BrokenAccessibilitySampleApp() } } @@ -743,7 +744,7 @@ private fun FixedLabeledAction(label: String, description: String) { @Preview(showBackground = true) @Composable fun BrokenAccessibilitySampleAppPreview() { - MaterialTheme { + ScannerTheme { BrokenAccessibilitySampleApp() } } diff --git a/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Color.kt b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Color.kt new file mode 100644 index 0000000..b041b64 --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Color.kt @@ -0,0 +1,21 @@ +package com.composea11yscanner.sample.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +val ScannerBackground = Color(0xFF0A0A0F) +val ScannerSurface = Color(0xFF12121A) +val ScannerSurfaceVariant = Color(0xFF1C1C28) +val ScannerPrimary = Color(0xFF6C63FF) +val ScannerSecondary = Color(0xFF00D4AA) +val ScannerError = Color(0xFFFF4D6A) +val ScannerWarning = Color(0xFFFFB547) +val ScannerInfo = Color(0xFF4DA6FF) +val ScannerSuccess = ScannerSecondary + +@Immutable +data class ScannerPalette( + val warning: Color, + val info: Color, + val success: Color, +) diff --git a/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Shape.kt b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Shape.kt new file mode 100644 index 0000000..e0eefe4 --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Shape.kt @@ -0,0 +1,12 @@ +package com.composea11yscanner.sample.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val ScannerShapes = Shapes( + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(16.dp), + large = RoundedCornerShape(24.dp), + extraLarge = RoundedCornerShape(32.dp), +) diff --git a/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Theme.kt b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Theme.kt new file mode 100644 index 0000000..aeb2ceb --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Theme.kt @@ -0,0 +1,66 @@ +package com.composea11yscanner.sample.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +private val ScannerDarkColorScheme = darkColorScheme( + primary = ScannerPrimary, + onPrimary = Color.White, + primaryContainer = ScannerPrimary.copy(alpha = 0.22f), + onPrimaryContainer = Color.White, + secondary = ScannerSecondary, + onSecondary = ScannerBackground, + secondaryContainer = ScannerSecondary.copy(alpha = 0.18f), + onSecondaryContainer = Color.White, + tertiary = ScannerInfo, + onTertiary = ScannerBackground, + error = ScannerError, + onError = Color.White, + errorContainer = ScannerError.copy(alpha = 0.2f), + onErrorContainer = Color.White, + background = ScannerBackground, + onBackground = Color(0xFFF4F4FA), + surface = ScannerSurface, + onSurface = Color(0xFFF4F4FA), + surfaceVariant = ScannerSurfaceVariant, + onSurfaceVariant = Color(0xFFC7C7D6), + outline = Color(0xFF6D6D80), + outlineVariant = Color(0xFF343447), + surfaceContainerLowest = ScannerBackground, + surfaceContainerLow = ScannerSurface, + surfaceContainer = ScannerSurfaceVariant, + surfaceContainerHigh = Color(0xFF242433), + surfaceContainerHighest = Color(0xFF2D2D3D), +) + +private val ScannerSemanticPalette = ScannerPalette( + warning = ScannerWarning, + info = ScannerInfo, + success = ScannerSuccess, +) + +private val LocalScannerPalette = staticCompositionLocalOf { ScannerSemanticPalette } + +object ScannerTheme { + val palette: ScannerPalette + @Composable + @ReadOnlyComposable + get() = LocalScannerPalette.current +} + +@Composable +fun ScannerTheme(content: @Composable () -> Unit) { + CompositionLocalProvider(LocalScannerPalette provides ScannerSemanticPalette) { + MaterialTheme( + colorScheme = ScannerDarkColorScheme, + typography = ScannerTypography, + shapes = ScannerShapes, + content = content, + ) + } +} diff --git a/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Type.kt b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Type.kt new file mode 100644 index 0000000..acccebb --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/ui/theme/Type.kt @@ -0,0 +1,45 @@ +package com.composea11yscanner.sample.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.googlefonts.Font +import androidx.compose.ui.text.googlefonts.GoogleFont + +private val InterFont = GoogleFont("Inter") + +private val GoogleFontsProvider = GoogleFont.Provider( + providerAuthority = "com.google.android.gms.fonts", + providerPackage = "com.google.android.gms", + certificates = 0, +) + +val InterFontFamily = FontFamily( + Font(googleFont = InterFont, fontProvider = GoogleFontsProvider, weight = FontWeight.Normal), + Font(googleFont = InterFont, fontProvider = GoogleFontsProvider, weight = FontWeight.Medium), + Font(googleFont = InterFont, fontProvider = GoogleFontsProvider, weight = FontWeight.SemiBold), + Font(googleFont = InterFont, fontProvider = GoogleFontsProvider, weight = FontWeight.Bold), +) + +private fun TextStyle.withInter(): TextStyle = copy(fontFamily = InterFontFamily) + +val ScannerTypography = Typography().run { + copy( + displayLarge = displayLarge.withInter(), + displayMedium = displayMedium.withInter(), + displaySmall = displaySmall.withInter(), + headlineLarge = headlineLarge.withInter(), + headlineMedium = headlineMedium.withInter(), + headlineSmall = headlineSmall.withInter(), + titleLarge = titleLarge.withInter(), + titleMedium = titleMedium.withInter(), + titleSmall = titleSmall.withInter(), + bodyLarge = bodyLarge.withInter(), + bodyMedium = bodyMedium.withInter(), + bodySmall = bodySmall.withInter(), + labelLarge = labelLarge.withInter(), + labelMedium = labelMedium.withInter(), + labelSmall = labelSmall.withInter(), + ) +} From 80c7bf8fc35ddcb62345852dc25d0006e009db7f Mon Sep 17 00:00:00 2001 From: Mohd Aquib Date: Sat, 11 Jul 2026 15:08:05 +0530 Subject: [PATCH 2/7] Update `BrokenLoginScreen` in sample app to a more complex, realistic UI - Redesign `BrokenLoginScreen` to simulate a modern banking login interface with "SecureBank" branding. - Add password visibility toggling using `VisualTransformation`. - Implement new UI elements including a custom `BankLogoMark`, gradient buttons, and a "Device trust" section. - Intentionally include accessibility issues like small touch targets for "Show/Hide" and "Forgot password" links to demonstrate scanner capabilities. - Update styling with a dark theme palette and improved layout using nested `Column` and `Row` components. --- .../sample/SampleActivity.kt | 213 +++++++++++++++--- 1 file changed, 178 insertions(+), 35 deletions(-) diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt index 8c4a434..41669fe 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt @@ -56,6 +56,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.platform.AbstractComposeView @@ -69,6 +70,8 @@ import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp @@ -306,30 +309,103 @@ fun BrokenAccessibilitySampleApp(modifier: Modifier = Modifier) { @Composable private fun BrokenLoginScreen(onViewFixed: (() -> Unit)? = null) { - BrokenScreenCard( - title = "Screen 1: Broken Login", - subtitle = "Missing content descriptions and small touch targets", - ) { - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("Email") }, - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("Password") }, + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF0D1117)) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + Row( modifier = Modifier.fillMaxWidth(), - ) + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + BankLogoMark() + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "SecureBank", + color = Color.White, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + ) + Text( + text = "Private client access", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodySmall, + ) + } + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Welcome back", + color = Color.White, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "Sign in to view balances, transfers, and card activity.", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodyMedium, + ) + } - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - TinyClickableLabel("?", Color(0xFF006D77)) - TinyClickableLabel("!", Color(0xFFE29578)) - TinyClickableLabel("x", Color(0xFF8D99AE)) + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + trailingIcon = { + Text( + text = if (passwordVisible) "Hide" else "Show", + color = Color(0xFF6C63FF), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .size(width = 44.dp, height = 30.dp) + .clickable { passwordVisible = !passwordVisible }, + ) + }, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + modifier = Modifier.fillMaxWidth(), + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + .clip(RoundedCornerShape(16.dp)) + .background( + Brush.horizontalGradient( + colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), + ), + ) + .clickable { }, + contentAlignment = Alignment.Center, + ) { + Text( + text = "Sign In", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) } Row( @@ -337,32 +413,99 @@ private fun BrokenLoginScreen(onViewFixed: (() -> Unit)? = null) { horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Box( - modifier = Modifier - .size(32.dp) - .clip(RoundedCornerShape(6.dp)) - .background(Color(0xFF2A9D8F)) - .clickable { }, - contentAlignment = Alignment.Center, - ) { - Text("Go", color = Color.White) - } Text( - text = "Forgot password", + text = "Forgot password?", + color = Color(0xFF4DA6FF), + style = MaterialTheme.typography.bodySmall, modifier = Modifier - .size(width = 92.dp, height = 30.dp) + .size(width = 108.dp, height = 30.dp) .clickable { }, - color = Color(0xFF1565C0), ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TinyClickableLabel("?", Color(0xFF1C2B3A)) + TinyClickableLabel("!", Color(0xFF2E2A1C)) + TinyClickableLabel("x", Color(0xFF281C2E)) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF121826)) + .padding(14.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "Device trust", + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "Review this session", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodySmall, + ) + } + Box( + modifier = Modifier + .size(32.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFF00D4AA)) + .clickable { }, + contentAlignment = Alignment.Center, + ) { + Text("Go", color = Color(0xFF0D1117), fontWeight = FontWeight.Bold) + } + } } + onViewFixed?.let { - Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF1C1C28)) + .clickable { it() }, + contentAlignment = Alignment.Center, + ) { Text("View Fixed Version") } } } } +@Composable +private fun BankLogoMark() { + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(14.dp)) + .background(Color(0xFF6C63FF)), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(width = 24.dp, height = 10.dp) + .clip(RoundedCornerShape(3.dp)) + .background(Color.White.copy(alpha = 0.92f)), + ) + Box( + modifier = Modifier + .size(width = 10.dp, height = 24.dp) + .clip(RoundedCornerShape(3.dp)) + .background(Color.White.copy(alpha = 0.72f)), + ) + } +} + @Composable private fun BrokenFeedScreen(onViewFixed: (() -> Unit)? = null) { BrokenScreenCard( From b02b60ac7c7000a2d0c0449363512c9eb107814b Mon Sep 17 00:00:00 2001 From: Mohd Aquib Date: Sun, 12 Jul 2026 09:05:01 +0530 Subject: [PATCH 3/7] Update sample application UI to a modern financial dashboard theme - Redesign `BrokenFeedScreen` and `FixedFeedScreen` to showcase a "Market Pulse" financial news feed, replacing the generic image placeholders. - Introduce `MarketFilterChip` and `FinancialNewsCard` / `FixedFinancialNewsCard` components to demonstrate accessibility issues like poor color contrast and duplicate descriptions in a realistic context. - Overhaul `FixedLoginScreen` with a "SecureBank" theme, featuring improved contrast, password visibility toggling, and explicit accessibility semantics for interactive elements. - Enhance the sample app's visual style using dark themes, custom drawing for charts, and modern Material 3 typography. - Ensure all new components maintain touch targets of at least 48dp and provide appropriate content descriptions in the "Fixed" versions. --- .../sample/SampleActivity.kt | 553 +++++++++++++++--- 1 file changed, 482 insertions(+), 71 deletions(-) diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt index 41669fe..f9ceb9e 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt @@ -56,6 +56,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.ColorPainter @@ -509,24 +511,37 @@ private fun BankLogoMark() { @Composable private fun BrokenFeedScreen(onViewFixed: (() -> Unit)? = null) { BrokenScreenCard( - title = "Screen 2: Broken Feed", - subtitle = "Poor contrast text over images and duplicate descriptions", + title = "Market Pulse", + subtitle = "Live financial news and price movers", ) { - FeedHero( - imageColor = Color(0xFF9E9E9E), - title = "Morning trail update", - description = "Open story", + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + MarketFilterChip("All", selected = true) + MarketFilterChip("Stocks", selected = false) + MarketFilterChip("Crypto", selected = false) + } + + FinancialNewsCard( + monogram = "A", + logoColor = Color(0xFF6C63FF), + headline = "Apex Bank rallies as mobile deposits hit quarterly record", + priceChange = "+4.8%", + isPositive = true, + timestamp = "2m ago", + chartStart = Color(0xFF26385F), + chartEnd = Color(0xFF121826), + description = "Open market story", ) - FeedHero( - imageColor = Color(0xFF78909C), - title = "City lights tonight", - description = "Open story", + FinancialNewsCard( + monogram = "N", + logoColor = Color(0xFFFF4D6A), + headline = "NovaPay slips after analysts flag rising card loss reserves", + priceChange = "-2.1%", + isPositive = false, + timestamp = "11m ago", + chartStart = Color(0xFF4B2332), + chartEnd = Color(0xFF161923), + description = "Open market story", ) - - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - DuplicateAction("Save", "Save item") - DuplicateAction("Bookmark", "Save item") - } onViewFixed?.let { Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { Text("View Fixed Version") @@ -535,6 +550,142 @@ private fun BrokenFeedScreen(onViewFixed: (() -> Unit)? = null) { } } +@Composable +private fun MarketFilterChip(label: String, selected: Boolean) { + Box( + modifier = Modifier + .height(48.dp) + .background( + color = if (selected) Color(0xFF6C63FF) else Color(0xFF1C1C28), + shape = RoundedCornerShape(24.dp), + ) + .clickable( + role = Role.Button, + onClick = {}, + ) + .padding(horizontal = 16.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = label, + color = if (selected) Color.White else Color(0xFF8B949E), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + ) + } +} + +@Composable +private fun FinancialNewsCard( + monogram: String, + logoColor: Color, + headline: String, + priceChange: String, + isPositive: Boolean, + timestamp: String, + chartStart: Color, + chartEnd: Color, + description: String, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(146.dp) + .clip(RoundedCornerShape(8.dp)) + .semantics { contentDescription = description } + .clickable { }, + ) { + Image( + painter = ColorPainter(chartStart), + contentDescription = description, + modifier = Modifier.fillMaxSize(), + ) + Box( + modifier = Modifier + .fillMaxSize() + .drawBehind { + drawRect( + brush = Brush.linearGradient( + colors = listOf(chartStart.copy(alpha = 0.35f), chartEnd), + start = Offset.Zero, + end = Offset(size.width, size.height), + ), + ) + repeat(5) { index -> + val y = size.height * (0.24f + index * 0.13f) + drawLine( + color = Color.White.copy(alpha = 0.09f), + start = Offset(0f, y + index * 12f), + end = Offset(size.width, y - 48f), + strokeWidth = 2.5f, + ) + } + drawLine( + color = Color(0xFF00D4AA).copy(alpha = 0.34f), + start = Offset(0f, size.height * 0.78f), + end = Offset(size.width, size.height * 0.34f), + strokeWidth = 5f, + ) + }, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Box( + modifier = Modifier + .size(42.dp) + .background(logoColor, RoundedCornerShape(21.dp)), + contentAlignment = Alignment.Center, + ) { + Text( + text = monogram, + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = headline, + color = Color(0xFF8B949E), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Text( + text = timestamp, + color = Color(0xFF6E7681), + style = MaterialTheme.typography.bodySmall, + ) + } + } + Box( + modifier = Modifier + .background( + color = if (isPositive) Color(0xFF164E3F) else Color(0xFF5A1D2A), + shape = RoundedCornerShape(14.dp), + ) + .padding(horizontal = 9.dp, vertical = 5.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = priceChange, + color = if (isPositive) Color(0xFF00D4AA) else Color(0xFFFF4D6A), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + ) + } + } + } +} + @Composable private fun BrokenFormScreen(onViewFixed: (() -> Unit)? = null) { BrokenScreenCard( @@ -586,70 +737,209 @@ private fun BrokenFormScreen(onViewFixed: (() -> Unit)? = null) { @Composable fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) { - BrokenScreenCard( - title = "Fixed Login", - subtitle = "Described controls with touch targets at least 48dp", - ) { - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("Email") }, - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("Password") }, + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF0D1117)) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + Row( modifier = Modifier.fillMaxWidth(), - ) + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + BankLogoMark() + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "SecureBank", + color = Color.White, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + ) + Text( + text = "Private client access", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodySmall, + ) + } + } - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - FixedIconAction("?", "Open login help", Color(0xFF006D77)) - FixedIconAction("!", "Show login requirements", Color(0xFFE29578)) - FixedIconAction("x", "Dismiss login form", Color(0xFF8D99AE)) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Welcome back", + color = Color.White, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "Sign in to view balances, transfers, and card activity.", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodyMedium, + ) } - Row( + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + trailingIcon = { + Box( + modifier = Modifier + .size(56.dp) + .semantics { + contentDescription = if (passwordVisible) { + "Hide password" + } else { + "Show password" + } + } + .clickable( + role = Role.Button, + onClick = { passwordVisible = !passwordVisible }, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = if (passwordVisible) "Hide" else "Show", + color = Color(0xFF6C63FF), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + ) + } + }, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + modifier = Modifier.fillMaxWidth(), + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + .clip(RoundedCornerShape(16.dp)) + .background( + Brush.horizontalGradient( + colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), + ), + ) + .semantics { contentDescription = "Sign in" } + .clickable( + role = Role.Button, + onClick = {}, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Sign In", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + + Column( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.spacedBy(10.dp), ) { Box( modifier = Modifier - .size(width = 56.dp, height = 48.dp) - .clip(RoundedCornerShape(6.dp)) - .background(Color(0xFF2A9D8F)) - .semantics { contentDescription = "Sign in" } + .fillMaxWidth() + .height(48.dp) + .semantics { contentDescription = "Reset password" } .clickable( role = Role.Button, onClick = {}, ), contentAlignment = Alignment.Center, ) { - Text("Go", color = Color.White) + Text( + text = "Forgot password?", + color = Color(0xFF4DA6FF), + style = MaterialTheme.typography.bodySmall, + ) } - Box( - modifier = Modifier - .size(width = 144.dp, height = 48.dp) - .semantics { contentDescription = "Reset password" } - .clickable( - role = Role.Button, - onClick = {}, - ), - contentAlignment = Alignment.Center, + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Text("Forgot password", color = Color(0xFF1565C0)) + FixedIconAction("?", "Open login help", Color(0xFF1C2B3A)) + FixedIconAction("!", "Show login requirements", Color(0xFF2E2A1C)) + FixedIconAction("x", "Dismiss login form", Color(0xFF281C2E)) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF121826)) + .padding(14.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "Device trust", + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "Review this session", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodySmall, + ) + } + Box( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF00D4AA)) + .semantics { contentDescription = "Review trusted device session" } + .clickable( + role = Role.Button, + onClick = {}, + ), + contentAlignment = Alignment.Center, + ) { + Text("Go", color = Color(0xFF0D1117), fontWeight = FontWeight.Bold) + } } } + onViewBroken?.let { - Button( - onClick = it, + Box( modifier = Modifier .fillMaxWidth() - .heightIn(min = 48.dp), + .heightIn(min = 48.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF1C1C28)) + .semantics { contentDescription = "View broken version" } + .clickable( + role = Role.Button, + onClick = { it() }, + ), + contentAlignment = Alignment.Center, ) { Text("View Broken Version") } @@ -660,24 +950,38 @@ fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) { @Composable fun FixedFeedScreen(onViewBroken: (() -> Unit)? = null) { BrokenScreenCard( - title = "Fixed Feed", - subtitle = "Readable captions and unique descriptions", + title = "Market Pulse", + subtitle = "Accessible financial news and price movers", ) { - FixedFeedItem( - imageColor = Color(0xFF607D8B), - title = "Morning trail update", - description = "Open morning trail story", + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + MarketFilterChip("All", selected = true) + MarketFilterChip("Stocks", selected = false) + MarketFilterChip("Crypto", selected = false) + } + + FixedFinancialNewsCard( + monogram = "A", + logoColor = Color(0xFF6C63FF), + headline = "Apex Bank rallies as mobile deposits hit quarterly record", + priceChange = "+4.8%", + isPositive = true, + timestamp = "2m ago", + chartStart = Color(0xFF26385F), + chartEnd = Color(0xFF121826), + description = "Open Apex Bank market story", ) - FixedFeedItem( - imageColor = Color(0xFF546E7A), - title = "City lights tonight", - description = "Open city lights story", + FixedFinancialNewsCard( + monogram = "N", + logoColor = Color(0xFFFF4D6A), + headline = "NovaPay slips after analysts flag rising card loss reserves", + priceChange = "-2.1%", + isPositive = false, + timestamp = "11m ago", + chartStart = Color(0xFF4B2332), + chartEnd = Color(0xFF161923), + description = "Open NovaPay market story", ) - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - FixedLabeledAction("Save", "Save feed item") - FixedLabeledAction("Bookmark", "Bookmark feed item") - } onViewBroken?.let { Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { Text("View Broken Version") @@ -686,6 +990,113 @@ fun FixedFeedScreen(onViewBroken: (() -> Unit)? = null) { } } +@Composable +private fun FixedFinancialNewsCard( + monogram: String, + logoColor: Color, + headline: String, + priceChange: String, + isPositive: Boolean, + timestamp: String, + chartStart: Color, + chartEnd: Color, + description: String, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(146.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF121826)) + .drawBehind { + drawRect( + brush = Brush.linearGradient( + colors = listOf(chartStart.copy(alpha = 0.45f), chartEnd), + start = Offset.Zero, + end = Offset(size.width, size.height), + ), + ) + repeat(5) { index -> + val y = size.height * (0.24f + index * 0.13f) + drawLine( + color = Color.White.copy(alpha = 0.08f), + start = Offset(0f, y + index * 12f), + end = Offset(size.width, y - 48f), + strokeWidth = 2.5f, + ) + } + drawLine( + color = Color(0xFF00D4AA).copy(alpha = 0.28f), + start = Offset(0f, size.height * 0.78f), + end = Offset(size.width, size.height * 0.34f), + strokeWidth = 5f, + ) + } + .semantics { contentDescription = description } + .clickable( + role = Role.Button, + onClick = {}, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.54f)) + .padding(14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Box( + modifier = Modifier + .size(42.dp) + .background(logoColor, RoundedCornerShape(21.dp)), + contentAlignment = Alignment.Center, + ) { + Text( + text = monogram, + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = headline, + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Text( + text = timestamp, + color = Color(0xFFD0D7DE), + style = MaterialTheme.typography.bodySmall, + ) + } + } + Box( + modifier = Modifier + .background( + color = if (isPositive) Color(0xFF164E3F) else Color(0xFF5A1D2A), + shape = RoundedCornerShape(14.dp), + ) + .padding(horizontal = 9.dp, vertical = 5.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = priceChange, + color = if (isPositive) Color(0xFF00D4AA) else Color(0xFFFF4D6A), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + ) + } + } + } +} + @Composable fun FixedFormScreen(onViewBroken: (() -> Unit)? = null) { BrokenScreenCard( From 92f265bd7c69676917858381afd57dee6b6b056e Mon Sep 17 00:00:00 2001 From: Mohd Aquib Date: Sun, 12 Jul 2026 09:42:27 +0530 Subject: [PATCH 4/7] Update sample app form screens to use a realistic Payment Form example - Replace the generic "Broken Form" example with a "Payment Form" containing fields for amount, recipient, payment date, and reference notes. - Implement intentional focus order issues in `BrokenFormScreen` by using absolute positioning for elements within a `Box` layout. - Provide a corresponding `FixedFormScreen` that uses a `Column` layout to ensure a logical top-to-bottom focus flow. - Add a reusable `PaymentSubmitAction` component featuring a gradient background and custom click role. - Improve accessibility in the fixed version by adding proper semantics and content descriptions to UI elements. --- .../sample/SampleActivity.kt | 235 +++++++++++++++--- 1 file changed, 194 insertions(+), 41 deletions(-) diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt index f9ceb9e..ba2eed0 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Button @@ -77,6 +78,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.ScannerConfig import com.composea11yscanner.rules.ScannerRules @@ -689,44 +691,97 @@ private fun FinancialNewsCard( @Composable private fun BrokenFormScreen(onViewFixed: (() -> Unit)? = null) { BrokenScreenCard( - title = "Screen 3: Broken Form", - subtitle = "Clickable elements lack roles and source order breaks focus flow", + title = "Payment form", + subtitle = "Transfer details with intentionally broken focus order", ) { - Text("Preference form", style = MaterialTheme.typography.titleMedium) + var amount by remember { mutableStateOf("") } + var recipient by remember { mutableStateOf("") } + var reference by remember { mutableStateOf("") } + var paymentDate by remember { mutableStateOf("") } Box( modifier = Modifier .fillMaxWidth() - .height(180.dp) - .background(Color(0xFFECEFF1), RoundedCornerShape(8.dp)), + .height(460.dp) + .background(Color(0xFF0D1117), RoundedCornerShape(8.dp)) + .padding(16.dp), ) { - FormAction( - text = "Submit preferences", - color = Color(0xFF00796B), + PaymentSubmitAction( modifier = Modifier .align(Alignment.BottomStart) - .padding(16.dp), + .fillMaxWidth(), ) - FormAction( - text = "Choose plan", - color = Color(0xFF5E35B1), + + OutlinedTextField( + value = amount, + onValueChange = { amount = it }, + label = { Text("Amount") }, + prefix = { + Text( + text = "$", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + }, + textStyle = MaterialTheme.typography.headlineMedium.copy( + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + ), + modifier = Modifier + .align(Alignment.TopStart) + .fillMaxWidth(), + ) + + Row( + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 116.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(56.dp) + .background(Color(0xFF6C63FF), RoundedCornerShape(28.dp)), + contentAlignment = Alignment.Center, + ) { + Text( + text = "MJ", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + OutlinedTextField( + value = recipient, + onValueChange = { recipient = it }, + label = { Text("Recipient Account") }, + modifier = Modifier.weight(1f), + ) + } + + OutlinedTextField( + value = paymentDate, + onValueChange = { paymentDate = it }, + label = { Text("Payment Date") }, modifier = Modifier .align(Alignment.TopStart) - .padding(16.dp), + .padding(top = 210.dp) + .fillMaxWidth(), ) - FormAction( - text = "Reset fields", - color = Color(0xFFC62828), + + OutlinedTextField( + value = reference, + onValueChange = { reference = it }, + label = { Text("Reference Note") }, modifier = Modifier - .align(Alignment.CenterEnd) - .padding(16.dp), + .align(Alignment.TopStart) + .padding(top = 294.dp) + .fillMaxWidth(), ) } - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - FormAction("Monthly", Color(0xFF455A64), Modifier.weight(1f)) - FormAction("Yearly", Color(0xFF455A64), Modifier.weight(1f)) - } onViewFixed?.let { Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { Text("View Fixed Version") @@ -735,6 +790,43 @@ private fun BrokenFormScreen(onViewFixed: (() -> Unit)? = null) { } } +@Composable +private fun PaymentSubmitAction(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .height(56.dp) + .background( + brush = Brush.horizontalGradient( + colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), + ), + shape = RoundedCornerShape(16.dp), + ) + .clickable( + role = Role.Button, + onClick = {}, + ), + contentAlignment = Alignment.Center, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Lock, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(18.dp), + ) + Text( + text = "Send Payment", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + } +} + @Composable fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) { var email by remember { mutableStateOf("") } @@ -1100,29 +1192,90 @@ private fun FixedFinancialNewsCard( @Composable fun FixedFormScreen(onViewBroken: (() -> Unit)? = null) { BrokenScreenCard( - title = "Fixed Form", - subtitle = "Button roles and top-to-bottom focus order", + title = "Payment form", + subtitle = "Accessible transfer details with top-to-bottom focus order", ) { - Text("Preference form", style = MaterialTheme.typography.titleMedium) - Button(onClick = {}, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { - Text("Choose plan") - } - Button(onClick = {}, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { - Text("Reset fields") - } - Button(onClick = {}, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { - Text("Submit preferences") - } - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button(onClick = {}, modifier = Modifier.weight(1f).heightIn(min = 48.dp)) { - Text("Monthly") - } - Button(onClick = {}, modifier = Modifier.weight(1f).heightIn(min = 48.dp)) { - Text("Yearly") + var amount by remember { mutableStateOf("") } + var recipient by remember { mutableStateOf("") } + var reference by remember { mutableStateOf("") } + var paymentDate by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF0D1117), RoundedCornerShape(8.dp)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + OutlinedTextField( + value = amount, + onValueChange = { amount = it }, + label = { Text("Amount") }, + prefix = { + Text( + text = "$", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + }, + textStyle = MaterialTheme.typography.headlineMedium.copy( + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + ), + modifier = Modifier.fillMaxWidth(), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(56.dp) + .background(Color(0xFF6C63FF), RoundedCornerShape(28.dp)) + .semantics { contentDescription = "Recipient avatar for Maya Johnson" }, + contentAlignment = Alignment.Center, + ) { + Text( + text = "MJ", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + OutlinedTextField( + value = recipient, + onValueChange = { recipient = it }, + label = { Text("Recipient Account") }, + modifier = Modifier.weight(1f), + ) } + + OutlinedTextField( + value = paymentDate, + onValueChange = { paymentDate = it }, + label = { Text("Payment Date") }, + modifier = Modifier.fillMaxWidth(), + ) + + OutlinedTextField( + value = reference, + onValueChange = { reference = it }, + label = { Text("Reference Note") }, + modifier = Modifier.fillMaxWidth(), + ) + + PaymentSubmitAction(modifier = Modifier.fillMaxWidth()) } + onViewBroken?.let { - Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { + Button( + onClick = it, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp), + ) { Text("View Broken Version") } } From 67007aca2dd1765d129beaf9214d586e89e85692 Mon Sep 17 00:00:00 2001 From: Mohd Aquib Date: Sun, 12 Jul 2026 09:58:35 +0530 Subject: [PATCH 5/7] Update navigation icon logic in SampleActivity - Introduce `screenIcons` list to provide specific icons for each navigation destination (Login, Feed, and Form). - Replace conditional icon logic in the navigation bar with dedicated icons from the new list. - Import `Person`, `List`, and `Edit` icons for use in the navigation component. --- .../com/composea11yscanner/sample/SampleActivity.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt index ba2eed0..ede6183 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt @@ -24,9 +24,12 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Button @@ -109,6 +112,7 @@ fun BrokenAccessibilitySampleApp(modifier: Modifier = Modifier) { var scanScrollY by remember { mutableIntStateOf(0) } val scrollState = rememberScrollState() val screens = listOf("Login", "Feed", "Form") + val screenIcons = listOf(Icons.Filled.Person, Icons.AutoMirrored.Filled.List, Icons.Filled.Edit) val screenSubtitles = listOf( "Credentials and compact actions", "Stories, captions, and repeated labels", @@ -191,11 +195,7 @@ fun BrokenAccessibilitySampleApp(modifier: Modifier = Modifier) { }, icon = { Icon( - imageVector = if (selectedScreen == index) { - Icons.Filled.CheckCircle - } else { - Icons.Filled.Search - }, + imageVector = screenIcons[index], contentDescription = null, ) }, From 205dae0f4225f73cdd5bdd17a37a44044077c252 Mon Sep 17 00:00:00 2001 From: Mohd Aquib Date: Sun, 12 Jul 2026 10:34:59 +0530 Subject: [PATCH 6/7] Refactor sample application to improve modularity and semantics extraction - Extract UI components from `SampleActivity.kt` into dedicated files: `LoginScreens.kt`, `FeedScreens.kt`, `FormScreens.kt`, and `SampleComponents.kt`. - Implement `SampleSemanticsExtractor.kt` to handle `A11yNode` extraction, including viewport filtering and Compose view traversal. - Simplify `SampleActivity.kt` by using modularized screen components and a new `SampleScreen` data model for navigation. - Improve accessibility of "Fixed" sample screens by adding proper semantic roles, content descriptions, and touch target sizes. - Add `SampleViewportTag` and `BrokenSampleContentTag` to facilitate targeted accessibility scanning within the sample app. --- .../composea11yscanner/sample/FeedScreens.kt | 300 ++++ .../composea11yscanner/sample/FormScreens.kt | 228 +++ .../composea11yscanner/sample/LoginScreens.kt | 361 ++++ .../sample/SampleActivity.kt | 1466 ++--------------- .../sample/SampleComponents.kt | 176 ++ .../sample/SampleSemanticsExtractor.kt | 90 + 6 files changed, 1317 insertions(+), 1304 deletions(-) create mode 100644 sample/src/main/java/com/composea11yscanner/sample/FeedScreens.kt create mode 100644 sample/src/main/java/com/composea11yscanner/sample/FormScreens.kt create mode 100644 sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt create mode 100644 sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt create mode 100644 sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt diff --git a/sample/src/main/java/com/composea11yscanner/sample/FeedScreens.kt b/sample/src/main/java/com/composea11yscanner/sample/FeedScreens.kt new file mode 100644 index 0000000..dd4af22 --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/FeedScreens.kt @@ -0,0 +1,300 @@ +package com.composea11yscanner.sample + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun BrokenFeedScreen(onViewFixed: (() -> Unit)? = null) { + BrokenScreenCard( + title = "Market Pulse", + subtitle = "Live financial news and price movers", + ) { + MarketFilters() + + FinancialNewsCard( + monogram = "A", + logoColor = Color(0xFF6C63FF), + headline = "Apex Bank rallies as mobile deposits hit quarterly record", + priceChange = "+4.8%", + isPositive = true, + timestamp = "2m ago", + chartStart = Color(0xFF26385F), + chartEnd = Color(0xFF121826), + description = "Open market story", + ) + FinancialNewsCard( + monogram = "N", + logoColor = Color(0xFFFF4D6A), + headline = "NovaPay slips after analysts flag rising card loss reserves", + priceChange = "-2.1%", + isPositive = false, + timestamp = "11m ago", + chartStart = Color(0xFF4B2332), + chartEnd = Color(0xFF161923), + description = "Open market story", + ) + onViewFixed?.let { + Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { + Text("View Fixed Version") + } + } + } +} + +@Composable +fun FixedFeedScreen(onViewBroken: (() -> Unit)? = null) { + BrokenScreenCard( + title = "Market Pulse", + subtitle = "Accessible financial news and price movers", + ) { + MarketFilters() + + FixedFinancialNewsCard( + monogram = "A", + logoColor = Color(0xFF6C63FF), + headline = "Apex Bank rallies as mobile deposits hit quarterly record", + priceChange = "+4.8%", + isPositive = true, + timestamp = "2m ago", + chartStart = Color(0xFF26385F), + chartEnd = Color(0xFF121826), + description = "Open Apex Bank market story", + ) + FixedFinancialNewsCard( + monogram = "N", + logoColor = Color(0xFFFF4D6A), + headline = "NovaPay slips after analysts flag rising card loss reserves", + priceChange = "-2.1%", + isPositive = false, + timestamp = "11m ago", + chartStart = Color(0xFF4B2332), + chartEnd = Color(0xFF161923), + description = "Open NovaPay market story", + ) + + onViewBroken?.let { + Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { + Text("View Broken Version") + } + } + } +} + +@Composable +private fun MarketFilters() { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + MarketFilterChip("All", selected = true) + MarketFilterChip("Stocks", selected = false) + MarketFilterChip("Crypto", selected = false) + } +} + +@Composable +private fun FinancialNewsCard( + monogram: String, + logoColor: Color, + headline: String, + priceChange: String, + isPositive: Boolean, + timestamp: String, + chartStart: Color, + chartEnd: Color, + description: String, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(146.dp) + .clip(RoundedCornerShape(8.dp)) + .semantics { contentDescription = description } + .clickable { }, + ) { + Image( + painter = ColorPainter(chartStart), + contentDescription = description, + modifier = Modifier.fillMaxSize(), + ) + ChartOverlay(chartStart = chartStart, chartEnd = chartEnd, isFixed = false) + FinancialNewsContent( + monogram = monogram, + logoColor = logoColor, + headline = headline, + priceChange = priceChange, + isPositive = isPositive, + timestamp = timestamp, + useScrim = false, + ) + } +} + +@Composable +private fun FixedFinancialNewsCard( + monogram: String, + logoColor: Color, + headline: String, + priceChange: String, + isPositive: Boolean, + timestamp: String, + chartStart: Color, + chartEnd: Color, + description: String, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(146.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF121826)) + .drawBehind { drawChart(chartStart, chartEnd, isFixed = true) } + .semantics { contentDescription = description } + .clickable( + role = Role.Button, + onClick = {}, + ), + ) { + FinancialNewsContent( + monogram = monogram, + logoColor = logoColor, + headline = headline, + priceChange = priceChange, + isPositive = isPositive, + timestamp = timestamp, + useScrim = true, + ) + } +} + +@Composable +private fun ChartOverlay(chartStart: Color, chartEnd: Color, isFixed: Boolean) { + Box( + modifier = Modifier + .fillMaxSize() + .drawBehind { drawChart(chartStart, chartEnd, isFixed) }, + ) +} + +private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawChart( + chartStart: Color, + chartEnd: Color, + isFixed: Boolean, +) { + drawRect( + brush = Brush.linearGradient( + colors = listOf(chartStart.copy(alpha = if (isFixed) 0.45f else 0.35f), chartEnd), + start = Offset.Zero, + end = Offset(size.width, size.height), + ), + ) + repeat(5) { index -> + val y = size.height * (0.24f + index * 0.13f) + drawLine( + color = Color.White.copy(alpha = if (isFixed) 0.08f else 0.09f), + start = Offset(0f, y + index * 12f), + end = Offset(size.width, y - 48f), + strokeWidth = 2.5f, + ) + } + drawLine( + color = Color(0xFF00D4AA).copy(alpha = if (isFixed) 0.28f else 0.34f), + start = Offset(0f, size.height * 0.78f), + end = Offset(size.width, size.height * 0.34f), + strokeWidth = 5f, + ) +} + +@Composable +private fun FinancialNewsContent( + monogram: String, + logoColor: Color, + headline: String, + priceChange: String, + isPositive: Boolean, + timestamp: String, + useScrim: Boolean, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .then(if (useScrim) Modifier.background(Color.Black.copy(alpha = 0.54f)) else Modifier) + .padding(14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Box( + modifier = Modifier + .size(42.dp) + .background(logoColor, RoundedCornerShape(21.dp)), + contentAlignment = Alignment.Center, + ) { + Text( + text = monogram, + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = headline, + color = if (useScrim) Color.White else Color(0xFF8B949E), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Text( + text = timestamp, + color = if (useScrim) Color(0xFFD0D7DE) else Color(0xFF6E7681), + style = MaterialTheme.typography.bodySmall, + ) + } + } + Box( + modifier = Modifier + .background( + color = if (isPositive) Color(0xFF164E3F) else Color(0xFF5A1D2A), + shape = RoundedCornerShape(14.dp), + ) + .padding(horizontal = 9.dp, vertical = 5.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = priceChange, + color = if (isPositive) Color(0xFF00D4AA) else Color(0xFFFF4D6A), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + ) + } + } +} diff --git a/sample/src/main/java/com/composea11yscanner/sample/FormScreens.kt b/sample/src/main/java/com/composea11yscanner/sample/FormScreens.kt new file mode 100644 index 0000000..62b7f73 --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/FormScreens.kt @@ -0,0 +1,228 @@ +package com.composea11yscanner.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun BrokenFormScreen(onViewFixed: (() -> Unit)? = null) { + BrokenScreenCard( + title = "Payment form", + subtitle = "Transfer details with intentionally broken focus order", + ) { + var amount by remember { mutableStateOf("") } + var recipient by remember { mutableStateOf("") } + var reference by remember { mutableStateOf("") } + var paymentDate by remember { mutableStateOf("") } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(460.dp) + .background(Color(0xFF0D1117), RoundedCornerShape(8.dp)) + .padding(16.dp), + ) { + PaymentSubmitAction( + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth(), + ) + + PaymentAmountField( + amount = amount, + onAmountChange = { amount = it }, + modifier = Modifier + .align(Alignment.TopStart) + .fillMaxWidth(), + ) + + RecipientRow( + recipient = recipient, + onRecipientChange = { recipient = it }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 116.dp) + .fillMaxWidth(), + isFixed = false, + ) + + OutlinedTextField( + value = paymentDate, + onValueChange = { paymentDate = it }, + label = { Text("Payment Date") }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 210.dp) + .fillMaxWidth(), + ) + + OutlinedTextField( + value = reference, + onValueChange = { reference = it }, + label = { Text("Reference Note") }, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 294.dp) + .fillMaxWidth(), + ) + } + + onViewFixed?.let { + Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { + Text("View Fixed Version") + } + } + } +} + +@Composable +fun FixedFormScreen(onViewBroken: (() -> Unit)? = null) { + BrokenScreenCard( + title = "Payment form", + subtitle = "Accessible transfer details with top-to-bottom focus order", + ) { + var amount by remember { mutableStateOf("") } + var recipient by remember { mutableStateOf("") } + var reference by remember { mutableStateOf("") } + var paymentDate by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF0D1117), RoundedCornerShape(8.dp)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + PaymentAmountField( + amount = amount, + onAmountChange = { amount = it }, + modifier = Modifier.fillMaxWidth(), + ) + + RecipientRow( + recipient = recipient, + onRecipientChange = { recipient = it }, + modifier = Modifier.fillMaxWidth(), + isFixed = true, + ) + + OutlinedTextField( + value = paymentDate, + onValueChange = { paymentDate = it }, + label = { Text("Payment Date") }, + modifier = Modifier.fillMaxWidth(), + ) + + OutlinedTextField( + value = reference, + onValueChange = { reference = it }, + label = { Text("Reference Note") }, + modifier = Modifier.fillMaxWidth(), + ) + + PaymentSubmitAction(modifier = Modifier.fillMaxWidth()) + } + + onViewBroken?.let { + Button( + onClick = it, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp), + ) { + Text("View Broken Version") + } + } + } +} + +@Composable +private fun PaymentAmountField( + amount: String, + onAmountChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedTextField( + value = amount, + onValueChange = onAmountChange, + label = { Text("Amount") }, + prefix = { + Text( + text = "$", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + }, + textStyle = MaterialTheme.typography.headlineMedium.copy( + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + ), + modifier = modifier, + ) +} + +@Composable +private fun RecipientRow( + recipient: String, + onRecipientChange: (String) -> Unit, + modifier: Modifier = Modifier, + isFixed: Boolean, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(56.dp) + .background(Color(0xFF6C63FF), RoundedCornerShape(28.dp)) + .then( + if (isFixed) { + Modifier.semantics { contentDescription = "Recipient avatar for Maya Johnson" } + } else { + Modifier + }, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "MJ", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + OutlinedTextField( + value = recipient, + onValueChange = onRecipientChange, + label = { Text("Recipient Account") }, + modifier = Modifier.weight(1f), + ) + } +} diff --git a/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt b/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt new file mode 100644 index 0000000..3a0ef1b --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/LoginScreens.kt @@ -0,0 +1,361 @@ +package com.composea11yscanner.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp + +@Composable +fun BrokenLoginScreen(onViewFixed: (() -> Unit)? = null) { + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF0D1117)) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + SecureBankHeader() + LoginIntro() + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + trailingIcon = { + Text( + text = if (passwordVisible) "Hide" else "Show", + color = Color(0xFF6C63FF), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .size(width = 44.dp, height = 30.dp) + .clickable { passwordVisible = !passwordVisible }, + ) + }, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + modifier = Modifier.fillMaxWidth(), + ) + } + + GradientSignInButton(isFixed = false) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Forgot password?", + color = Color(0xFF4DA6FF), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .size(width = 108.dp, height = 30.dp) + .clickable { }, + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TinyClickableLabel("?", Color(0xFF1C2B3A)) + TinyClickableLabel("!", Color(0xFF2E2A1C)) + TinyClickableLabel("x", Color(0xFF281C2E)) + } + } + + DeviceTrustCard(isFixed = false) + + onViewFixed?.let { + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF1C1C28)) + .clickable { it() }, + contentAlignment = Alignment.Center, + ) { + Text("View Fixed Version") + } + } + } +} + +@Composable +fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) { + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF0D1117)) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + SecureBankHeader() + LoginIntro() + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + trailingIcon = { + Box( + modifier = Modifier + .size(56.dp) + .semantics { + contentDescription = if (passwordVisible) { + "Hide password" + } else { + "Show password" + } + } + .clickable( + role = Role.Button, + onClick = { passwordVisible = !passwordVisible }, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = if (passwordVisible) "Hide" else "Show", + color = Color(0xFF6C63FF), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + ) + } + }, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + modifier = Modifier.fillMaxWidth(), + ) + } + + GradientSignInButton(isFixed = true) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .semantics { contentDescription = "Reset password" } + .clickable( + role = Role.Button, + onClick = {}, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Forgot password?", + color = Color(0xFF4DA6FF), + style = MaterialTheme.typography.bodySmall, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + FixedIconAction("?", "Open login help", Color(0xFF1C2B3A)) + FixedIconAction("!", "Show login requirements", Color(0xFF2E2A1C)) + FixedIconAction("x", "Dismiss login form", Color(0xFF281C2E)) + } + } + + DeviceTrustCard(isFixed = true) + + onViewBroken?.let { + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF1C1C28)) + .semantics { contentDescription = "View broken version" } + .clickable( + role = Role.Button, + onClick = { it() }, + ), + contentAlignment = Alignment.Center, + ) { + Text("View Broken Version") + } + } + } +} + +@Composable +private fun SecureBankHeader() { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + BankLogoMark() + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "SecureBank", + color = Color.White, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + ) + Text( + text = "Private client access", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodySmall, + ) + } + } +} + +@Composable +private fun LoginIntro() { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Welcome back", + color = Color.White, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "Sign in to view balances, transfers, and card activity.", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +@Composable +private fun GradientSignInButton(isFixed: Boolean) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + .clip(RoundedCornerShape(16.dp)) + .background( + Brush.horizontalGradient( + colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), + ), + ) + .then( + if (isFixed) { + Modifier + .semantics { contentDescription = "Sign in" } + .clickable(role = Role.Button, onClick = {}) + } else { + Modifier.clickable { } + }, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Sign In", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } +} + +@Composable +private fun DeviceTrustCard(isFixed: Boolean) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFF121826)) + .padding(14.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "Device trust", + color = Color.White, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "Review this session", + color = Color(0xFF8B949E), + style = MaterialTheme.typography.bodySmall, + ) + } + Box( + modifier = Modifier + .size(if (isFixed) 56.dp else 32.dp) + .clip(RoundedCornerShape(if (isFixed) 16.dp else 10.dp)) + .background(Color(0xFF00D4AA)) + .then( + if (isFixed) { + Modifier + .semantics { contentDescription = "Review trusted device session" } + .clickable(role = Role.Button, onClick = {}) + } else { + Modifier.clickable { } + }, + ), + contentAlignment = Alignment.Center, + ) { + Text("Go", color = Color(0xFF0D1117), fontWeight = FontWeight.Bold) + } + } + } +} diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt index ede6183..cb8a4cc 100644 --- a/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleActivity.kt @@ -1,23 +1,16 @@ package com.composea11yscanner.sample import android.os.Bundle -import android.view.View -import android.view.ViewGroup import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -28,12 +21,9 @@ import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.Button -import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ElevatedCard @@ -41,10 +31,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -59,35 +48,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.SemanticsNode -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.getOrNull -import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.composea11yscanner.core.model.A11yNode import com.composea11yscanner.core.model.ScannerConfig import com.composea11yscanner.rules.ScannerRules import com.composea11yscanner.sample.ui.theme.ScannerTheme import com.composea11yscanner.triggers.scanOnShake -import com.composea11yscanner.ui.A11yNodeExtractor import com.composea11yscanner.ui.A11yScannerController import com.composea11yscanner.ui.A11yScannerScaffold @@ -111,13 +81,13 @@ fun BrokenAccessibilitySampleApp(modifier: Modifier = Modifier) { var viewingFixed by remember { mutableStateOf(false) } var scanScrollY by remember { mutableIntStateOf(0) } val scrollState = rememberScrollState() - val screens = listOf("Login", "Feed", "Form") - val screenIcons = listOf(Icons.Filled.Person, Icons.AutoMirrored.Filled.List, Icons.Filled.Edit) - val screenSubtitles = listOf( - "Credentials and compact actions", - "Stories, captions, and repeated labels", - "Form controls and focus order", - ) + val screens = remember { + listOf( + SampleScreen("Login", "Credentials and compact actions", Icons.Filled.Person), + SampleScreen("Feed", "Stories, captions, and repeated labels", Icons.AutoMirrored.Filled.List), + SampleScreen("Form", "Form controls and focus order", Icons.Filled.Edit), + ) + } val issueOffsetY by remember { derivedStateOf { scanScrollY - scrollState.value } } @@ -136,6 +106,7 @@ fun BrokenAccessibilitySampleApp(modifier: Modifier = Modifier) { autoScan = false, ) } + fun startSampleScan() { scanScrollY = scrollState.value scannerController.startScan() @@ -157,1297 +128,220 @@ fun BrokenAccessibilitySampleApp(modifier: Modifier = Modifier) { Scaffold( modifier = Modifier.fillMaxSize(), topBar = { - CenterAlignedTopAppBar( - title = { - Text( - text = "Compose A11y Scanner", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - ) - }, - actions = { - IconButton(onClick = { scannerController.clearState() }) { - Icon( - imageVector = Icons.Filled.Clear, - contentDescription = "Clear scan results", - ) - } - IconButton(onClick = { startSampleScan() }) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = "Scan selected sample", - ) - } - }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + SampleTopBar( + onClear = { scannerController.clearState() }, + onScan = { startSampleScan() }, ) }, bottomBar = { - NavigationBar { - screens.forEachIndexed { index, label -> - NavigationBarItem( - selected = selectedScreen == index, - onClick = { - selectedScreen = index - viewingFixed = false - }, - icon = { - Icon( - imageVector = screenIcons[index], - contentDescription = null, - ) - }, - label = { Text(label) }, - ) - } - } + SampleBottomBar( + screens = screens, + selectedScreen = selectedScreen, + onScreenSelected = { + selectedScreen = it + viewingFixed = false + }, + ) }, ) { innerPadding -> - Column( + SampleContent( modifier = Modifier .padding(innerPadding) - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceContainerLowest) - .verticalScroll(scrollState) - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - colors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = screens[selectedScreen], - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - ) - Text( - text = screenSubtitles[selectedScreen], - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - FilterChip( - selected = !viewingFixed, - onClick = { viewingFixed = false }, - label = { Text("Broken") }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Warning, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - }, - ) - FilterChip( - selected = viewingFixed, - onClick = { viewingFixed = true }, - label = { Text("Fixed") }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.CheckCircle, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - }, - ) - } - } - } - - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 1.dp, - ) { - Box( - modifier = Modifier - .padding(8.dp) - .semantics { testTag = BrokenSampleContentTag }, - ) - { - if (viewingFixed) { - when (selectedScreen) { - 0 -> FixedLoginScreen() - 1 -> FixedFeedScreen() - else -> FixedFormScreen() - } - } else { - when (selectedScreen) { - 0 -> BrokenLoginScreen() - 1 -> BrokenFeedScreen() - else -> BrokenFormScreen() - } - } - } - } - } + .fillMaxSize(), + screens = screens, + selectedScreen = selectedScreen, + viewingFixed = viewingFixed, + onViewingFixedChange = { viewingFixed = it }, + scrollModifier = Modifier.verticalScroll(scrollState), + ) } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun BrokenLoginScreen(onViewFixed: (() -> Unit)? = null) { - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var passwordVisible by remember { mutableStateOf(false) } - - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(Color(0xFF0D1117)) - .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(18.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - BankLogoMark() - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = "SecureBank", - color = Color.White, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - ) - Text( - text = "Private client access", - color = Color(0xFF8B949E), - style = MaterialTheme.typography.bodySmall, - ) - } - } - - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { +private fun SampleTopBar( + onClear: () -> Unit, + onScan: () -> Unit, +) { + CenterAlignedTopAppBar( + title = { Text( - text = "Welcome back", - color = Color.White, - style = MaterialTheme.typography.headlineSmall, + text = "Compose A11y Scanner", + style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, ) - Text( - text = "Sign in to view balances, transfers, and card activity.", - color = Color(0xFF8B949E), - style = MaterialTheme.typography.bodyMedium, - ) - } - - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("Email") }, - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("Password") }, - trailingIcon = { - Text( - text = if (passwordVisible) "Hide" else "Show", - color = Color(0xFF6C63FF), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .size(width = 44.dp, height = 30.dp) - .clickable { passwordVisible = !passwordVisible }, - ) - }, - visualTransformation = if (passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - modifier = Modifier.fillMaxWidth(), - ) - } - - Box( - modifier = Modifier - .fillMaxWidth() - .height(54.dp) - .clip(RoundedCornerShape(16.dp)) - .background( - Brush.horizontalGradient( - colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), - ), + }, + actions = { + IconButton(onClick = onClear) { + Icon( + imageVector = Icons.Filled.Clear, + contentDescription = "Clear scan results", ) - .clickable { }, - contentAlignment = Alignment.Center, - ) { - Text( - text = "Sign In", - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "Forgot password?", - color = Color(0xFF4DA6FF), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier - .size(width = 108.dp, height = 30.dp) - .clickable { }, - ) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - TinyClickableLabel("?", Color(0xFF1C2B3A)) - TinyClickableLabel("!", Color(0xFF2E2A1C)) - TinyClickableLabel("x", Color(0xFF281C2E)) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background(Color(0xFF121826)) - .padding(14.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = "Device trust", - color = Color.White, - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.SemiBold, - ) - Text( - text = "Review this session", - color = Color(0xFF8B949E), - style = MaterialTheme.typography.bodySmall, - ) - } - Box( - modifier = Modifier - .size(32.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFF00D4AA)) - .clickable { }, - contentAlignment = Alignment.Center, - ) { - Text("Go", color = Color(0xFF0D1117), fontWeight = FontWeight.Bold) - } - } - } - - onViewFixed?.let { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 48.dp) - .clip(RoundedCornerShape(16.dp)) - .background(Color(0xFF1C1C28)) - .clickable { it() }, - contentAlignment = Alignment.Center, - ) { - Text("View Fixed Version") - } - } - } -} - -@Composable -private fun BankLogoMark() { - Box( - modifier = Modifier - .size(48.dp) - .clip(RoundedCornerShape(14.dp)) - .background(Color(0xFF6C63FF)), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .size(width = 24.dp, height = 10.dp) - .clip(RoundedCornerShape(3.dp)) - .background(Color.White.copy(alpha = 0.92f)), - ) - Box( - modifier = Modifier - .size(width = 10.dp, height = 24.dp) - .clip(RoundedCornerShape(3.dp)) - .background(Color.White.copy(alpha = 0.72f)), - ) - } -} - -@Composable -private fun BrokenFeedScreen(onViewFixed: (() -> Unit)? = null) { - BrokenScreenCard( - title = "Market Pulse", - subtitle = "Live financial news and price movers", - ) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - MarketFilterChip("All", selected = true) - MarketFilterChip("Stocks", selected = false) - MarketFilterChip("Crypto", selected = false) - } - - FinancialNewsCard( - monogram = "A", - logoColor = Color(0xFF6C63FF), - headline = "Apex Bank rallies as mobile deposits hit quarterly record", - priceChange = "+4.8%", - isPositive = true, - timestamp = "2m ago", - chartStart = Color(0xFF26385F), - chartEnd = Color(0xFF121826), - description = "Open market story", - ) - FinancialNewsCard( - monogram = "N", - logoColor = Color(0xFFFF4D6A), - headline = "NovaPay slips after analysts flag rising card loss reserves", - priceChange = "-2.1%", - isPositive = false, - timestamp = "11m ago", - chartStart = Color(0xFF4B2332), - chartEnd = Color(0xFF161923), - description = "Open market story", - ) - onViewFixed?.let { - Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { - Text("View Fixed Version") } - } - } -} - -@Composable -private fun MarketFilterChip(label: String, selected: Boolean) { - Box( - modifier = Modifier - .height(48.dp) - .background( - color = if (selected) Color(0xFF6C63FF) else Color(0xFF1C1C28), - shape = RoundedCornerShape(24.dp), - ) - .clickable( - role = Role.Button, - onClick = {}, - ) - .padding(horizontal = 16.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = label, - color = if (selected) Color.White else Color(0xFF8B949E), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - ) - } -} - -@Composable -private fun FinancialNewsCard( - monogram: String, - logoColor: Color, - headline: String, - priceChange: String, - isPositive: Boolean, - timestamp: String, - chartStart: Color, - chartEnd: Color, - description: String, -) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(146.dp) - .clip(RoundedCornerShape(8.dp)) - .semantics { contentDescription = description } - .clickable { }, - ) { - Image( - painter = ColorPainter(chartStart), - contentDescription = description, - modifier = Modifier.fillMaxSize(), - ) - Box( - modifier = Modifier - .fillMaxSize() - .drawBehind { - drawRect( - brush = Brush.linearGradient( - colors = listOf(chartStart.copy(alpha = 0.35f), chartEnd), - start = Offset.Zero, - end = Offset(size.width, size.height), - ), - ) - repeat(5) { index -> - val y = size.height * (0.24f + index * 0.13f) - drawLine( - color = Color.White.copy(alpha = 0.09f), - start = Offset(0f, y + index * 12f), - end = Offset(size.width, y - 48f), - strokeWidth = 2.5f, - ) - } - drawLine( - color = Color(0xFF00D4AA).copy(alpha = 0.34f), - start = Offset(0f, size.height * 0.78f), - end = Offset(size.width, size.height * 0.34f), - strokeWidth = 5f, - ) - }, - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(14.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top, - ) { - Row( - modifier = Modifier.weight(1f), - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - Box( - modifier = Modifier - .size(42.dp) - .background(logoColor, RoundedCornerShape(21.dp)), - contentAlignment = Alignment.Center, - ) { - Text( - text = monogram, - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - } - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - text = headline, - color = Color(0xFF8B949E), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - Text( - text = timestamp, - color = Color(0xFF6E7681), - style = MaterialTheme.typography.bodySmall, - ) - } - } - Box( - modifier = Modifier - .background( - color = if (isPositive) Color(0xFF164E3F) else Color(0xFF5A1D2A), - shape = RoundedCornerShape(14.dp), - ) - .padding(horizontal = 9.dp, vertical = 5.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = priceChange, - color = if (isPositive) Color(0xFF00D4AA) else Color(0xFFFF4D6A), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, + IconButton(onClick = onScan) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = "Scan selected sample", ) } - } - } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) } @Composable -private fun BrokenFormScreen(onViewFixed: (() -> Unit)? = null) { - BrokenScreenCard( - title = "Payment form", - subtitle = "Transfer details with intentionally broken focus order", - ) { - var amount by remember { mutableStateOf("") } - var recipient by remember { mutableStateOf("") } - var reference by remember { mutableStateOf("") } - var paymentDate by remember { mutableStateOf("") } - - Box( - modifier = Modifier - .fillMaxWidth() - .height(460.dp) - .background(Color(0xFF0D1117), RoundedCornerShape(8.dp)) - .padding(16.dp), - ) { - PaymentSubmitAction( - modifier = Modifier - .align(Alignment.BottomStart) - .fillMaxWidth(), - ) - - OutlinedTextField( - value = amount, - onValueChange = { amount = it }, - label = { Text("Amount") }, - prefix = { - Text( - text = "$", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, +private fun SampleBottomBar( + screens: List, + selectedScreen: Int, + onScreenSelected: (Int) -> Unit, +) { + NavigationBar { + screens.forEachIndexed { index, screen -> + NavigationBarItem( + selected = selectedScreen == index, + onClick = { onScreenSelected(index) }, + icon = { + Icon( + imageVector = screen.icon, + contentDescription = null, ) }, - textStyle = MaterialTheme.typography.headlineMedium.copy( - fontSize = 32.sp, - fontWeight = FontWeight.Bold, - ), - modifier = Modifier - .align(Alignment.TopStart) - .fillMaxWidth(), - ) - - Row( - modifier = Modifier - .align(Alignment.TopStart) - .padding(top = 116.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .size(56.dp) - .background(Color(0xFF6C63FF), RoundedCornerShape(28.dp)), - contentAlignment = Alignment.Center, - ) { - Text( - text = "MJ", - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - } - OutlinedTextField( - value = recipient, - onValueChange = { recipient = it }, - label = { Text("Recipient Account") }, - modifier = Modifier.weight(1f), - ) - } - - OutlinedTextField( - value = paymentDate, - onValueChange = { paymentDate = it }, - label = { Text("Payment Date") }, - modifier = Modifier - .align(Alignment.TopStart) - .padding(top = 210.dp) - .fillMaxWidth(), - ) - - OutlinedTextField( - value = reference, - onValueChange = { reference = it }, - label = { Text("Reference Note") }, - modifier = Modifier - .align(Alignment.TopStart) - .padding(top = 294.dp) - .fillMaxWidth(), + label = { Text(screen.label) }, ) } - - onViewFixed?.let { - Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { - Text("View Fixed Version") - } - } } } @Composable -private fun PaymentSubmitAction(modifier: Modifier = Modifier) { - Box( +private fun SampleContent( + modifier: Modifier = Modifier, + screens: List, + selectedScreen: Int, + viewingFixed: Boolean, + onViewingFixedChange: (Boolean) -> Unit, + scrollModifier: Modifier = Modifier, +) { + Column( modifier = modifier - .height(56.dp) - .background( - brush = Brush.horizontalGradient( - colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), - ), - shape = RoundedCornerShape(16.dp), - ) - .clickable( - role = Role.Button, - onClick = {}, - ), - contentAlignment = Alignment.Center, + .background(MaterialTheme.colorScheme.surfaceContainerLowest) + .then(scrollModifier) + .semantics { testTag = SampleViewportTag } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Filled.Lock, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(18.dp), - ) - Text( - text = "Send Payment", - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - } + SampleModeCard( + screen = screens[selectedScreen], + viewingFixed = viewingFixed, + onViewingFixedChange = onViewingFixedChange, + ) + SampleScreenHost( + selectedScreen = selectedScreen, + viewingFixed = viewingFixed, + ) } } @Composable -fun FixedLoginScreen(onViewBroken: (() -> Unit)? = null) { - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var passwordVisible by remember { mutableStateOf(false) } - - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(Color(0xFF0D1117)) - .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(18.dp), +private fun SampleModeCard( + screen: SampleScreen, + viewingFixed: Boolean, + onViewingFixedChange: (Boolean) -> Unit, +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - BankLogoMark() - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = "SecureBank", - color = Color.White, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - ) - Text( - text = "Private client access", - color = Color(0xFF8B949E), - style = MaterialTheme.typography.bodySmall, - ) - } - } - - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - text = "Welcome back", - color = Color.White, - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.SemiBold, - ) - Text( - text = "Sign in to view balances, transfers, and card activity.", - color = Color(0xFF8B949E), - style = MaterialTheme.typography.bodyMedium, - ) - } - - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("Email") }, - modifier = Modifier.fillMaxWidth(), - ) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("Password") }, - trailingIcon = { - Box( - modifier = Modifier - .size(56.dp) - .semantics { - contentDescription = if (passwordVisible) { - "Hide password" - } else { - "Show password" - } - } - .clickable( - role = Role.Button, - onClick = { passwordVisible = !passwordVisible }, - ), - contentAlignment = Alignment.Center, - ) { - Text( - text = if (passwordVisible) "Hide" else "Show", - color = Color(0xFF6C63FF), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - ) - } - }, - visualTransformation = if (passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - modifier = Modifier.fillMaxWidth(), - ) - } - - Box( - modifier = Modifier - .fillMaxWidth() - .height(54.dp) - .clip(RoundedCornerShape(16.dp)) - .background( - Brush.horizontalGradient( - colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), - ), - ) - .semantics { contentDescription = "Sign in" } - .clickable( - role = Role.Button, - onClick = {}, - ), - contentAlignment = Alignment.Center, - ) { - Text( - text = "Sign In", - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - } - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .semantics { contentDescription = "Reset password" } - .clickable( - role = Role.Button, - onClick = {}, - ), - contentAlignment = Alignment.Center, + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( - text = "Forgot password?", - color = Color(0xFF4DA6FF), + text = screen.label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = screen.subtitle, style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - FixedIconAction("?", "Open login help", Color(0xFF1C2B3A)) - FixedIconAction("!", "Show login requirements", Color(0xFF2E2A1C)) - FixedIconAction("x", "Dismiss login form", Color(0xFF281C2E)) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background(Color(0xFF121826)) - .padding(14.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = "Device trust", - color = Color.White, - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.SemiBold, - ) - Text( - text = "Review this session", - color = Color(0xFF8B949E), - style = MaterialTheme.typography.bodySmall, - ) - } - Box( - modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(16.dp)) - .background(Color(0xFF00D4AA)) - .semantics { contentDescription = "Review trusted device session" } - .clickable( - role = Role.Button, - onClick = {}, - ), - contentAlignment = Alignment.Center, - ) { - Text("Go", color = Color(0xFF0D1117), fontWeight = FontWeight.Bold) - } - } - } - - onViewBroken?.let { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 48.dp) - .clip(RoundedCornerShape(16.dp)) - .background(Color(0xFF1C1C28)) - .semantics { contentDescription = "View broken version" } - .clickable( - role = Role.Button, - onClick = { it() }, - ), - contentAlignment = Alignment.Center, - ) { - Text("View Broken Version") - } - } - } -} - -@Composable -fun FixedFeedScreen(onViewBroken: (() -> Unit)? = null) { - BrokenScreenCard( - title = "Market Pulse", - subtitle = "Accessible financial news and price movers", - ) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - MarketFilterChip("All", selected = true) - MarketFilterChip("Stocks", selected = false) - MarketFilterChip("Crypto", selected = false) - } - - FixedFinancialNewsCard( - monogram = "A", - logoColor = Color(0xFF6C63FF), - headline = "Apex Bank rallies as mobile deposits hit quarterly record", - priceChange = "+4.8%", - isPositive = true, - timestamp = "2m ago", - chartStart = Color(0xFF26385F), - chartEnd = Color(0xFF121826), - description = "Open Apex Bank market story", - ) - FixedFinancialNewsCard( - monogram = "N", - logoColor = Color(0xFFFF4D6A), - headline = "NovaPay slips after analysts flag rising card loss reserves", - priceChange = "-2.1%", - isPositive = false, - timestamp = "11m ago", - chartStart = Color(0xFF4B2332), - chartEnd = Color(0xFF161923), - description = "Open NovaPay market story", - ) - - onViewBroken?.let { - Button(onClick = it, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) { - Text("View Broken Version") - } - } - } -} - -@Composable -private fun FixedFinancialNewsCard( - monogram: String, - logoColor: Color, - headline: String, - priceChange: String, - isPositive: Boolean, - timestamp: String, - chartStart: Color, - chartEnd: Color, - description: String, -) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(146.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Color(0xFF121826)) - .drawBehind { - drawRect( - brush = Brush.linearGradient( - colors = listOf(chartStart.copy(alpha = 0.45f), chartEnd), - start = Offset.Zero, - end = Offset(size.width, size.height), - ), - ) - repeat(5) { index -> - val y = size.height * (0.24f + index * 0.13f) - drawLine( - color = Color.White.copy(alpha = 0.08f), - start = Offset(0f, y + index * 12f), - end = Offset(size.width, y - 48f), - strokeWidth = 2.5f, - ) - } - drawLine( - color = Color(0xFF00D4AA).copy(alpha = 0.28f), - start = Offset(0f, size.height * 0.78f), - end = Offset(size.width, size.height * 0.34f), - strokeWidth = 5f, - ) - } - .semantics { contentDescription = description } - .clickable( - role = Role.Button, - onClick = {}, - ), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(Color.Black.copy(alpha = 0.54f)) - .padding(14.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top, - ) { - Row( - modifier = Modifier.weight(1f), - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - Box( - modifier = Modifier - .size(42.dp) - .background(logoColor, RoundedCornerShape(21.dp)), - contentAlignment = Alignment.Center, - ) { - Text( - text = monogram, - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - } - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - text = headline, - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - Text( - text = timestamp, - color = Color(0xFFD0D7DE), - style = MaterialTheme.typography.bodySmall, - ) - } - } - Box( - modifier = Modifier - .background( - color = if (isPositive) Color(0xFF164E3F) else Color(0xFF5A1D2A), - shape = RoundedCornerShape(14.dp), - ) - .padding(horizontal = 9.dp, vertical = 5.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = priceChange, - color = if (isPositive) Color(0xFF00D4AA) else Color(0xFFFF4D6A), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, + FilterChip( + selected = !viewingFixed, + onClick = { onViewingFixedChange(false) }, + label = { Text("Broken") }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, ) - } - } - } -} - -@Composable -fun FixedFormScreen(onViewBroken: (() -> Unit)? = null) { - BrokenScreenCard( - title = "Payment form", - subtitle = "Accessible transfer details with top-to-bottom focus order", - ) { - var amount by remember { mutableStateOf("") } - var recipient by remember { mutableStateOf("") } - var reference by remember { mutableStateOf("") } - var paymentDate by remember { mutableStateOf("") } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF0D1117), RoundedCornerShape(8.dp)) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), - ) { - OutlinedTextField( - value = amount, - onValueChange = { amount = it }, - label = { Text("Amount") }, - prefix = { - Text( - text = "$", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - ) - }, - textStyle = MaterialTheme.typography.headlineMedium.copy( - fontSize = 32.sp, - fontWeight = FontWeight.Bold, - ), - modifier = Modifier.fillMaxWidth(), - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .size(56.dp) - .background(Color(0xFF6C63FF), RoundedCornerShape(28.dp)) - .semantics { contentDescription = "Recipient avatar for Maya Johnson" }, - contentAlignment = Alignment.Center, - ) { - Text( - text = "MJ", - color = Color.White, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - } - OutlinedTextField( - value = recipient, - onValueChange = { recipient = it }, - label = { Text("Recipient Account") }, - modifier = Modifier.weight(1f), + FilterChip( + selected = viewingFixed, + onClick = { onViewingFixedChange(true) }, + label = { Text("Fixed") }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, ) } - - OutlinedTextField( - value = paymentDate, - onValueChange = { paymentDate = it }, - label = { Text("Payment Date") }, - modifier = Modifier.fillMaxWidth(), - ) - - OutlinedTextField( - value = reference, - onValueChange = { reference = it }, - label = { Text("Reference Note") }, - modifier = Modifier.fillMaxWidth(), - ) - - PaymentSubmitAction(modifier = Modifier.fillMaxWidth()) - } - - onViewBroken?.let { - Button( - onClick = it, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 48.dp), - ) { - Text("View Broken Version") - } } } } @Composable -private fun BrokenScreenCard( - title: String, - subtitle: String, - content: @Composable ColumnScope.() -> Unit, +private fun SampleScreenHost( + selectedScreen: Int, + viewingFixed: Boolean, ) { - Card( + Surface( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp, ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), + Box( + modifier = Modifier + .padding(8.dp) + .semantics { testTag = BrokenSampleContentTag }, ) { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold) - Text(subtitle, style = MaterialTheme.typography.bodyMedium) + if (viewingFixed) { + when (selectedScreen) { + 0 -> FixedLoginScreen() + 1 -> FixedFeedScreen() + else -> FixedFormScreen() + } + } else { + when (selectedScreen) { + 0 -> BrokenLoginScreen() + 1 -> BrokenFeedScreen() + else -> BrokenFormScreen() + } } - content() } } } -@Composable -private fun TinyClickableLabel(text: String, color: Color) { - Box( - modifier = Modifier - .size(28.dp) - .background(color, RoundedCornerShape(6.dp)) - .clickable { }, - contentAlignment = Alignment.Center, - ) { - Text(text, color = Color.White, fontWeight = FontWeight.Bold) - } -} - -@Composable -private fun FeedHero( - imageColor: Color, - title: String, - description: String, -) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(150.dp) - .clip(RoundedCornerShape(8.dp)) - .semantics { contentDescription = description } - .clickable { }, - ) { - Image( - painter = ColorPainter(imageColor), - contentDescription = description, - modifier = Modifier.fillMaxSize(), - ) - Text( - text = title, - color = Color(0xFFBDBDBD), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(14.dp), - ) - } -} - -@Composable -private fun DuplicateAction(label: String, description: String) { - Box( - modifier = Modifier - .size(width = 132.dp, height = 52.dp) - .background(Color(0xFF283593), RoundedCornerShape(8.dp)) - .semantics { contentDescription = description } - .clickable { }, - contentAlignment = Alignment.Center, - ) { - Text(label, color = Color.White) - } -} - -@Composable -private fun FormAction( - text: String, - color: Color, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .height(56.dp) - .background(color, RoundedCornerShape(8.dp)) - .clickable { } - .padding(horizontal = 16.dp), - contentAlignment = Alignment.Center, - ) { - Text(text, color = Color.White, fontWeight = FontWeight.SemiBold) - } -} - -@Composable -private fun FixedIconAction( - label: String, - description: String, - color: Color, -) { - Box( - modifier = Modifier - .size(56.dp) - .background(color, RoundedCornerShape(8.dp)) - .semantics { contentDescription = description } - .clickable( - role = Role.Button, - onClick = {}, - ), - contentAlignment = Alignment.Center, - ) { - Text(label, color = Color.White, fontWeight = FontWeight.Bold) - } -} - -@Composable -private fun FixedFeedItem( - imageColor: Color, - title: String, - description: String, -) { - Column( - modifier = Modifier - .fillMaxWidth() - .semantics { contentDescription = description } - .clickable( - role = Role.Button, - onClick = {}, - ), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Image( - painter = ColorPainter(imageColor), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .height(120.dp) - .clip(RoundedCornerShape(8.dp)), - ) - Text(title, style = MaterialTheme.typography.titleMedium) - } -} - -@Composable -private fun FixedLabeledAction(label: String, description: String) { - Box( - modifier = Modifier - .size(width = 132.dp, height = 56.dp) - .background(Color(0xFF283593), RoundedCornerShape(8.dp)) - .semantics { contentDescription = description } - .clickable( - role = Role.Button, - onClick = {}, - ), - contentAlignment = Alignment.Center, - ) { - Text(label, color = Color.White) - } -} - @Preview(showBackground = true) @Composable fun BrokenAccessibilitySampleAppPreview() { @@ -1456,44 +350,8 @@ fun BrokenAccessibilitySampleAppPreview() { } } -private fun ComponentActivity.extractBrokenSampleNodes(): List = - runCatching { - val hostView = (window.decorView as? ViewGroup) - ?.findFirstAbstractComposeView() - ?: return emptyList() - val semanticsOwner = hostView.findSemanticsOwner() ?: return emptyList() - val sampleRoot = semanticsOwner.unmergedRootSemanticsNode - .findNodeByTestTag(BrokenSampleContentTag) - ?: return emptyList() - A11yNodeExtractor(Density(this)).extract(sampleRoot) - }.getOrDefault(emptyList()) - -private const val BrokenSampleContentTag = "broken-sample-content" - -private fun SemanticsNode.findNodeByTestTag(tag: String): SemanticsNode? { - if (config.getOrNull(SemanticsProperties.TestTag) == tag) return this - children.forEach { child -> - child.findNodeByTestTag(tag)?.let { return it } - } - return null -} - -private fun ViewGroup.findFirstAbstractComposeView(): AbstractComposeView? { - for (i in 0 until childCount) { - val child = getChildAt(i) - if (child is AbstractComposeView) return child - if (child is ViewGroup) { - child.findFirstAbstractComposeView()?.let { return it } - } - } - return null -} - -private fun AbstractComposeView.findSemanticsOwner(): SemanticsOwner? { - val composeOwnerView: View = getChildAt(0) ?: return null - return runCatching { - composeOwnerView.javaClass - .getMethod("getSemanticsOwner") - .invoke(composeOwnerView) as? SemanticsOwner - }.getOrNull() -} +private data class SampleScreen( + val label: String, + val subtitle: String, + val icon: androidx.compose.ui.graphics.vector.ImageVector, +) diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt new file mode 100644 index 0000000..4252950 --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleComponents.kt @@ -0,0 +1,176 @@ +package com.composea11yscanner.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun BrokenScreenCard( + title: String, + subtitle: String, + content: @Composable ColumnScope.() -> Unit, +) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold) + Text(subtitle, style = MaterialTheme.typography.bodyMedium) + } + content() + } + } +} + +@Composable +fun BankLogoMark() { + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(14.dp)) + .background(Color(0xFF6C63FF)), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(width = 24.dp, height = 10.dp) + .clip(RoundedCornerShape(3.dp)) + .background(Color.White.copy(alpha = 0.92f)), + ) + Box( + modifier = Modifier + .size(width = 10.dp, height = 24.dp) + .clip(RoundedCornerShape(3.dp)) + .background(Color.White.copy(alpha = 0.72f)), + ) + } +} + +@Composable +fun TinyClickableLabel(text: String, color: Color) { + Box( + modifier = Modifier + .size(28.dp) + .background(color, RoundedCornerShape(6.dp)) + .clickable { }, + contentAlignment = Alignment.Center, + ) { + Text(text, color = Color.White, fontWeight = FontWeight.Bold) + } +} + +@Composable +fun FixedIconAction( + label: String, + description: String, + color: Color, +) { + Box( + modifier = Modifier + .size(56.dp) + .background(color, RoundedCornerShape(8.dp)) + .semantics { contentDescription = description } + .clickable( + role = Role.Button, + onClick = {}, + ), + contentAlignment = Alignment.Center, + ) { + Text(label, color = Color.White, fontWeight = FontWeight.Bold) + } +} + +@Composable +fun MarketFilterChip(label: String, selected: Boolean) { + Box( + modifier = Modifier + .height(48.dp) + .background( + color = if (selected) Color(0xFF6C63FF) else Color(0xFF1C1C28), + shape = RoundedCornerShape(24.dp), + ) + .clickable( + role = Role.Button, + onClick = {}, + ) + .padding(horizontal = 16.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = label, + color = if (selected) Color.White else Color(0xFF8B949E), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + ) + } +} + +@Composable +fun PaymentSubmitAction(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .height(56.dp) + .background( + brush = Brush.horizontalGradient( + colors = listOf(Color(0xFF6C63FF), Color(0xFF00D4AA)), + ), + shape = RoundedCornerShape(16.dp), + ) + .clickable( + role = Role.Button, + onClick = {}, + ), + contentAlignment = Alignment.Center, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Lock, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(18.dp), + ) + Text( + text = "Send Payment", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + } +} diff --git a/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt b/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt new file mode 100644 index 0000000..f68eb59 --- /dev/null +++ b/sample/src/main/java/com/composea11yscanner/sample/SampleSemanticsExtractor.kt @@ -0,0 +1,90 @@ +package com.composea11yscanner.sample + +import android.view.View +import android.view.ViewGroup +import androidx.activity.ComponentActivity +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsOwner +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.unit.Density +import com.composea11yscanner.core.model.A11yNode +import com.composea11yscanner.core.model.Rect +import com.composea11yscanner.ui.A11yNodeExtractor +import kotlin.math.roundToInt + +internal const val BrokenSampleContentTag = "broken-sample-content" +internal const val SampleViewportTag = "sample-viewport" + +internal fun ComponentActivity.extractBrokenSampleNodes(): List = + runCatching { + val hostView = (window.decorView as? ViewGroup) + ?.findFirstAbstractComposeView() + ?: return emptyList() + val semanticsOwner = hostView.findSemanticsOwner() ?: return emptyList() + val sampleRoot = semanticsOwner.unmergedRootSemanticsNode + .findNodeByTestTag(BrokenSampleContentTag) + ?: return emptyList() + val viewport = semanticsOwner.unmergedRootSemanticsNode + .findNodeByTestTag(SampleViewportTag) + ?.boundsInRoot + ?.let { Rect(it.left.roundToInt(), it.top.roundToInt(), it.right.roundToInt(), it.bottom.roundToInt()) } + ?: sampleRoot.boundsInRoot.let { + Rect(it.left.roundToInt(), it.top.roundToInt(), it.right.roundToInt(), it.bottom.roundToInt()) + } + A11yNodeExtractor(Density(this)) + .extract(sampleRoot) + .filterVisibleIn(viewport) + }.getOrDefault(emptyList()) + +private fun List.filterVisibleIn(viewport: Rect): List = + filter { node -> + if (node.isFocusable) { + node.bounds.centerInside(viewport) + } else { + node.bounds.intersects(viewport) + } + } + +private fun Rect.centerInside(other: Rect): Boolean { + if (isEmpty()) return false + val centerX = (left + right) / 2 + val centerY = (top + bottom) / 2 + return centerX in other.left..other.right && centerY in other.top..other.bottom +} + +private fun Rect.intersects(other: Rect): Boolean = + !isEmpty() && + right > other.left && + left < other.right && + bottom > other.top && + top < other.bottom + +private fun SemanticsNode.findNodeByTestTag(tag: String): SemanticsNode? { + if (config.getOrNull(SemanticsProperties.TestTag) == tag) return this + children.forEach { child -> + child.findNodeByTestTag(tag)?.let { return it } + } + return null +} + +private fun ViewGroup.findFirstAbstractComposeView(): AbstractComposeView? { + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child is AbstractComposeView) return child + if (child is ViewGroup) { + child.findFirstAbstractComposeView()?.let { return it } + } + } + return null +} + +private fun AbstractComposeView.findSemanticsOwner(): SemanticsOwner? { + val composeOwnerView: View = getChildAt(0) ?: return null + return runCatching { + composeOwnerView.javaClass + .getMethod("getSemanticsOwner") + .invoke(composeOwnerView) as? SemanticsOwner + }.getOrNull() +} From f26388d7c144c1338b8e9d8e8b7f2d6e64e9f18a Mon Sep 17 00:00:00 2001 From: Mohd Aquib Date: Sun, 12 Jul 2026 14:46:26 +0530 Subject: [PATCH 7/7] Refine UI components and visual feedback for the accessibility scanner - Enhance `IssueHighlightBox` with corner accent lines and a semi-transparent fill for improved visibility of identified issues. - Redesign `ScanSummaryBar` into a centered, animated pill-shaped surface with elevation and a glow effect. - Implement a custom `RadarSweepIcon` with an infinite rotation animation to represent active scanning. - Update `ScanSummaryBar` layout to use animated width transitions between scanning, complete, and error states. - Standardize chips for errors, warnings, info, and scores using a pill shape and updated color palette. - Refactor internal content components (`ScanningContent`, `ScanCompleteContent`, `ErrorContent`) for better layout and typography consistency. --- .../ui/IssueHighlightBox.kt | 50 ++- .../composea11yscanner/ui/ScanSummaryBar.kt | 336 +++++++++++------- 2 files changed, 258 insertions(+), 128 deletions(-) diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt index 0fe3903..bc6838b 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/IssueHighlightBox.kt @@ -7,7 +7,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview @@ -24,6 +26,8 @@ private val InfoBorderColor = Color(0xFF1976D2) private val BorderStrokeWidth = 2.dp private val BorderCornerRadius = 4.dp +private val CornerAccentLength = 12.dp +private val CornerAccentStrokeWidth = 3.dp private fun A11ySeverity.toBorderColor(): Color = when (this) { A11ySeverity.Error -> ErrorBorderColor @@ -48,15 +52,57 @@ fun IssueHighlightBox( .size(width, height) .clickable { onIssueSelected(issue) } .drawBehind { + val strokeWidth = BorderStrokeWidth.toPx() + val cornerRadius = BorderCornerRadius.toPx() + val cornerAccentLength = CornerAccentLength.toPx() + val cornerAccentStrokeWidth = CornerAccentStrokeWidth.toPx() + val cornerInset = cornerAccentStrokeWidth / 2f + + drawRoundRect( + color = borderColor, + alpha = 0.08f, + cornerRadius = CornerRadius(cornerRadius), + ) drawRoundRect( color = borderColor, - style = Stroke(width = BorderStrokeWidth.toPx()), - cornerRadius = CornerRadius(BorderCornerRadius.toPx()), + alpha = 0.92f, + style = Stroke(width = strokeWidth), + cornerRadius = CornerRadius(cornerRadius), + ) + drawCornerAccents( + color = borderColor, + length = cornerAccentLength, + strokeWidth = cornerAccentStrokeWidth, + inset = cornerInset, ) }, ) } +private fun DrawScope.drawCornerAccents( + color: Color, + length: Float, + strokeWidth: Float, + inset: Float, +) { + val left = inset + val top = inset + val right = size.width - inset + val bottom = size.height - inset + + drawLine(color, Offset(left, top), Offset(left + length, top), strokeWidth) + drawLine(color, Offset(left, top), Offset(left, top + length), strokeWidth) + + drawLine(color, Offset(right, top), Offset(right - length, top), strokeWidth) + drawLine(color, Offset(right, top), Offset(right, top + length), strokeWidth) + + drawLine(color, Offset(left, bottom), Offset(left + length, bottom), strokeWidth) + drawLine(color, Offset(left, bottom), Offset(left, bottom - length), strokeWidth) + + drawLine(color, Offset(right, bottom), Offset(right - length, bottom), strokeWidth) + drawLine(color, Offset(right, bottom), Offset(right, bottom - length), strokeWidth) +} + @Preview(showBackground = true) @Composable private fun IssueHighlightBoxErrorPreview() { diff --git a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt index 4f32d1f..b99f15f 100644 --- a/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt +++ b/scanner-ui/src/main/java/com/composea11yscanner/ui/ScanSummaryBar.kt @@ -1,18 +1,28 @@ package com.composea11yscanner.ui import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error @@ -33,9 +43,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.composea11yscanner.core.model.A11yIssue import com.composea11yscanner.core.model.A11yNode @@ -45,79 +62,84 @@ import com.composea11yscanner.core.model.Rect import com.composea11yscanner.core.model.ScanResult import com.composea11yscanner.core.model.ScannerState -// ───────────────────────────────────────────────────────────────────────────── -// Colour constants -// ───────────────────────────────────────────────────────────────────────────── - private val ErrorColor = Color(0xFFD32F2F) private val WarningColor = Color(0xFFFFA000) private val InfoColor = Color(0xFF1976D2) +private val ScoreColor = Color(0xFF6C63FF) +private val PrimaryGlow = Color(0x556C63FF) -private val ScoreGoodColor = Color(0xFF2E7D32) // ≥ 90 % -private val ScoreFairColor = Color(0xFFF57C00) // ≥ 70 % -private val ScorePoorColor = Color(0xFFC62828) // < 70 % - -private fun Float.toScoreColor(): Color = when { - this >= 90f -> ScoreGoodColor - this >= 70f -> ScoreFairColor - else -> ScorePoorColor -} - -// ───────────────────────────────────────────────────────────────────────────── -// Public composable -// ───────────────────────────────────────────────────────────────────────────── +private val PillShape = RoundedCornerShape(percent = 50) +private val ScanningPillWidth = 220.dp +private val CompletePillWidth = 280.dp +private val DefaultTopOffset = 72.dp -/** - * Top bar that shows scan progress while a scan is running and a summary of - * findings once it completes. Tapping the score chip opens [ScanReportSheet]. - * - * Callers are responsible for showing/hiding the bar itself; this composable - * renders content for [ScannerState.Scanning] and [ScannerState.Complete] and - * nothing for [ScannerState.Idle] and [ScannerState.Error]. - */ @Composable fun ScanSummaryBar( state: ScannerState, modifier: Modifier = Modifier, + topOffset: Dp = DefaultTopOffset, ) { var showReport by remember { mutableStateOf(false) } - - // Retain last complete result so the sheet stays populated when state rolls - // back to Idle while the sheet is still open. var lastResult by remember { mutableStateOf(null) } + LaunchedEffect(state) { if (state is ScannerState.Complete) lastResult = state.result } - Surface( - tonalElevation = 2.dp, - modifier = modifier.fillMaxWidth(), + val targetWidth = when (state) { + is ScannerState.Complete -> CompletePillWidth + is ScannerState.Scanning -> ScanningPillWidth + is ScannerState.Error -> CompletePillWidth + ScannerState.Idle -> 120.dp + } + val pillWidth by animateDpAsState( + targetValue = targetWidth, + animationSpec = tween(durationMillis = 360), + label = "scan-summary-width", + ) + + Box( + modifier = modifier + .fillMaxWidth() + .padding(top = topOffset, start = 16.dp, end = 16.dp), + contentAlignment = Alignment.TopCenter, ) { - // contentKey groups Scanning(0.1f) and Scanning(0.9f) under the same - // key so the progress bar isn't cross-faded on every progress tick — - // the fade only fires on state-category transitions (e.g. Scanning → - // Complete). - AnimatedContent( - targetState = state, - transitionSpec = { fadeIn() togetherWith fadeOut() }, - contentKey = { s -> - when (s) { - is ScannerState.Scanning -> "scanning" - is ScannerState.Complete -> "complete" - else -> "other" + Surface( + color = MaterialTheme.colorScheme.surface, + shape = PillShape, + tonalElevation = 6.dp, + modifier = Modifier + .width(pillWidth) + .shadow( + elevation = 16.dp, + shape = PillShape, + ambientColor = PrimaryGlow, + spotColor = PrimaryGlow, + ), + ) { + AnimatedContent( + targetState = state, + transitionSpec = { fadeIn() togetherWith fadeOut() }, + contentKey = { current -> + when (current) { + is ScannerState.Scanning -> "scanning" + is ScannerState.Complete -> "complete" + is ScannerState.Error -> "error" + ScannerState.Idle -> "idle" + } + }, + label = "scan-state", + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + ) { currentState -> + when (currentState) { + is ScannerState.Scanning -> ScanningContent(progress = currentState.progress) + is ScannerState.Complete -> ScanCompleteContent( + result = currentState.result, + onScoreClick = { showReport = true }, + ) + is ScannerState.Error -> ErrorContent(message = currentState.message) + ScannerState.Idle -> Unit } - }, - label = "scan-state", - modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), - ) { currentState -> - when (currentState) { - is ScannerState.Scanning -> ScanningContent(progress = currentState.progress) - is ScannerState.Complete -> ScanCompleteContent( - result = currentState.result, - onScoreClick = { showReport = true }, - ) - is ScannerState.Error -> ErrorContent(message = currentState.message) - ScannerState.Idle -> Unit } } } @@ -132,21 +154,22 @@ fun ScanSummaryBar( } } -// ───────────────────────────────────────────────────────────────────────────── -// Bar content slots -// ───────────────────────────────────────────────────────────────────────────── - @Composable private fun ScanningContent(progress: Float) { - Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(7.dp)) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { + RadarSweepIcon( + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(22.dp), + ) Text( - text = "Scanning accessibility…", - style = MaterialTheme.typography.bodyMedium, + text = "Scanning...", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, ) Text( text = "${(progress * 100).toInt()}%", @@ -156,7 +179,12 @@ private fun ScanningContent(progress: Float) { } LinearProgressIndicator( progress = { progress }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .height(3.dp) + .clip(PillShape), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.16f), ) } } @@ -171,33 +199,24 @@ private fun ScanCompleteContent( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, ) { - if (result.errorCount > 0) { - CountChip( - count = result.errorCount, - color = ErrorColor, - icon = Icons.Filled.Error, - contentDescription = "${result.errorCount} errors", - ) - } - if (result.warningCount > 0) { - CountChip( - count = result.warningCount, - color = WarningColor, - icon = Icons.Filled.Warning, - contentDescription = "${result.warningCount} warnings", - ) - } - if (result.infoCount > 0) { - CountChip( - count = result.infoCount, - color = InfoColor, - icon = Icons.Filled.Info, - contentDescription = "${result.infoCount} info items", - ) - } - - Spacer(modifier = Modifier.weight(1f)) - + CountChip( + count = result.errorCount, + color = ErrorColor, + icon = Icons.Filled.Error, + contentDescription = "${result.errorCount} errors", + ) + CountChip( + count = result.warningCount, + color = WarningColor, + icon = Icons.Filled.Warning, + contentDescription = "${result.warningCount} warnings", + ) + CountChip( + count = result.infoCount, + color = InfoColor, + icon = Icons.Filled.Info, + contentDescription = "${result.infoCount} info items", + ) ScoreChip(score = result.overallScore, onClick = onScoreClick) } } @@ -205,6 +224,7 @@ private fun ScanCompleteContent( @Composable private fun ErrorContent(message: String) { Row( + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -218,13 +238,58 @@ private fun ErrorContent(message: String) { text = "Scan failed: $message", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error, + maxLines = 1, ) } } -// ───────────────────────────────────────────────────────────────────────────── -// Chip atoms used in the bar -// ───────────────────────────────────────────────────────────────────────────── +@Composable +private fun RadarSweepIcon( + color: Color, + modifier: Modifier = Modifier, +) { + val isInspecting = LocalInspectionMode.current + val infiniteTransition = rememberInfiniteTransition(label = "radar-sweep") + val sweepRotation by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1100, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + label = "radar-sweep-rotation", + ) + val rotation = if (isInspecting) 35f else sweepRotation + + Canvas(modifier = modifier) { + val strokeWidth = 1.5.dp.toPx() + val radius = size.minDimension / 2f - strokeWidth + val center = Offset(size.width / 2f, size.height / 2f) + + drawCircle( + color = color.copy(alpha = 0.22f), + radius = radius, + center = center, + style = Stroke(width = strokeWidth), + ) + drawCircle( + color = color.copy(alpha = 0.14f), + radius = radius * 0.55f, + center = center, + style = Stroke(width = strokeWidth), + ) + rotate(degrees = rotation, pivot = center) { + drawLine( + color = color, + start = center, + end = Offset(center.x, center.y - radius), + strokeWidth = 2.dp.toPx(), + cap = StrokeCap.Round, + ) + } + drawCircle(color = color, radius = 2.2.dp.toPx(), center = center) + } +} @Composable private fun CountChip( @@ -235,10 +300,10 @@ private fun CountChip( ) { Row( modifier = Modifier - .clip(RoundedCornerShape(4.dp)) + .clip(PillShape) .background(color) - .padding(horizontal = 8.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + .padding(horizontal = 7.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(3.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -259,31 +324,27 @@ private fun CountChip( private fun ScoreChip(score: Float, onClick: () -> Unit) { Row( modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(score.toScoreColor()) + .clip(PillShape) + .background(ScoreColor) .clickable(onClick = onClick) - .padding(horizontal = 12.dp, vertical = 6.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + .padding(horizontal = 9.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = "${score.toInt()}%", color = Color.White, - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelMedium, ) Icon( imageVector = Icons.Filled.ExpandMore, contentDescription = "View full report", tint = Color.White, - modifier = Modifier.size(16.dp), + modifier = Modifier.size(14.dp), ) } } -// ───────────────────────────────────────────────────────────────────────────── -// Previews -// ───────────────────────────────────────────────────────────────────────────── - @Preview(showBackground = true) @Composable private fun ScanSummaryBarScanningPreview() { @@ -334,45 +395,68 @@ private fun previewResult( failed: Int, ): ScanResult { val node = A11yNode( - nodeId = "node-1", composableName = "Button", - bounds = Rect(0, 0, 300, 120), contentDescription = null, - isTouchTarget = true, touchTargetSize = DpSize(100f, 40f), - textColor = null, backgroundColors = emptyList(), - isFocusable = true, isMergedDescendant = false, depth = 1, + nodeId = "node-1", + composableName = "Button", + bounds = Rect(0, 0, 300, 120), + contentDescription = null, + isTouchTarget = true, + touchTargetSize = DpSize(100f, 40f), + textColor = null, + backgroundColors = emptyList(), + isFocusable = true, + isMergedDescendant = false, + depth = 1, ) val issues = buildList { repeat(errors) { add( A11yIssue( - "err-$it", A11ySeverity.Error, "missing-content-description", - "Missing Content Description", node, - "Interactive element has no content description.", - "Add Modifier.semantics { contentDescription = … }", "WCAG 1.1.1 Non-text Content (Level A)", + issueId = "err-$it", + severity = A11ySeverity.Error, + ruleId = "missing-content-description", + ruleName = "Missing Content Description", + affectedNode = node, + message = "Interactive element has no content description.", + howToFix = "Add Modifier.semantics { contentDescription = \"Label\" }", + wcagReference = "WCAG 1.1.1 Non-text Content (Level A)", ) ) } repeat(warnings) { add( A11yIssue( - "warn-$it", A11ySeverity.Warning, "focus-order", - "Focus Order", node, "Focus jumps upward unexpectedly.", - "Reorder composables top-to-bottom.", "WCAG 2.4.3 Focus Order (Level A)", + issueId = "warn-$it", + severity = A11ySeverity.Warning, + ruleId = "focus-order", + ruleName = "Focus Order", + affectedNode = node, + message = "Focus jumps upward unexpectedly.", + howToFix = "Reorder composables top-to-bottom.", + wcagReference = "WCAG 2.4.3 Focus Order (Level A)", ) ) } repeat(info) { add( A11yIssue( - "info-$it", A11ySeverity.Info, "text-scaling", - "Text Scaling", node, "Text does not scale with system font size.", - "Use sp units for all text sizes.", null, + issueId = "info-$it", + severity = A11ySeverity.Info, + ruleId = "text-scaling", + ruleName = "Text Scaling", + affectedNode = node, + message = "Text does not scale with system font size.", + howToFix = "Use sp units for all text sizes.", + wcagReference = null, ) ) } } return ScanResult( - scanId = "preview", timestamp = 0L, - totalNodes = 24, issues = issues, - passedRules = passed, failedRules = failed, + scanId = "preview", + timestamp = 0L, + totalNodes = 24, + issues = issues, + passedRules = passed, + failedRules = failed, ) }