diff --git a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/DesktopAppRoot.kt b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/DesktopAppRoot.kt index 7f1953d4b..ce92fc1b1 100644 --- a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/DesktopAppRoot.kt +++ b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/DesktopAppRoot.kt @@ -10,6 +10,8 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -18,7 +20,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -174,11 +175,8 @@ fun DesktopApp( // temporarily disconnected — the VM handles disconnection gracefully while preserving // selectedEndpoint). allDevices is a live collection, updated before the flow fires. LaunchedEffect(Unit) { - var knownKeys = activeDeviceMonitor.allDevices.map { it.device.toString() }.toSet() - activeDeviceMonitor.onDeviceConnectionStateChange.collect { - val currentKeys = activeDeviceMonitor.allDevices.map { it.device.toString() }.toSet() - (knownKeys - currentKeys).forEach { evictDesktopViewModelsForKey(it) } - knownKeys = currentKeys + activeDeviceMonitor.onDeviceRemoved.collect { + evictDesktopViewModelsForKey(it) } } @@ -194,13 +192,35 @@ fun DesktopApp( } } +@Composable +private fun ScrimOverlay(visible: Boolean, onDismiss: () -> Unit) { + Column(modifier = Modifier.fillMaxSize()) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(animationDuration)), + exit = fadeOut(animationSpec = tween(animationDuration)), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = scrimAlpha)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onDismiss, + ) + ) + } + } +} + @Composable private fun DeviceContent( device: Device, strings: Strings ) { val viewModel = getViewModel( - key = device.toString() + device = device ) { parametersOf(device) } val state by viewModel.state.collectAsState() @@ -282,7 +302,7 @@ private fun ConnectedDeviceLayout( device = connectedState.activeDevice.device, onEndpointClicked = { key -> viewModel.setSelectedEndpoint(key) - state = state.copy(rightPanelTab = RightPanelTab.EndpointDetails) + state = state.copy(rightPanelTab = RightPanelTab.EndpointDetails.takeUnless { key == null }) }, onGlobalControlsClicked = { state = state.copy(globalControlsOpen = true) }, ) @@ -353,16 +373,8 @@ private fun ConnectedDeviceLayout( ) } - // Scrim behind preset overlay — Column provides ColumnScope for AnimatedVisibility - Column(modifier = Modifier.fillMaxSize()) { - AnimatedVisibility( - visible = presetVisible, - enter = fadeIn(animationSpec = tween(animationDuration)), - exit = fadeOut(animationSpec = tween(animationDuration)), - ) { - Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = scrimAlpha))) - } - } + // Scrim behind preset overlay + ScrimOverlay(visible = presetVisible, onDismiss = { state = state.copy(presetOpen = false) }) // Create/Edit Preset overlay — Box provides alignment; Column provides ColumnScope Box(modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd)) { @@ -403,6 +415,9 @@ private fun ConnectedDeviceLayout( } } + // Scrim behind global controls overlay + ScrimOverlay(visible = state.globalControlsOpen, onDismiss = { state = state.copy(globalControlsOpen = false) }) + // Global Controls overlay — Box provides alignment; Column provides ColumnScope Box(modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd)) { Column(modifier = Modifier.fillMaxHeight()) { @@ -422,15 +437,8 @@ private fun ConnectedDeviceLayout( Surface( modifier = Modifier .fillMaxHeight() - .width(state.globalControlsWidthDp.dp) - .shadow(8.dp) - .border( - 1.dp, - MaterialTheme.colorScheme.outline, - RoundedCornerShape(topStart = 8.dp), - ), + .width(state.globalControlsWidthDp.dp), color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(topStart = 8.dp), ) { GlobalControlsWidget( device = connectedState.activeDevice.device, diff --git a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/deviceconnection/DeviceConnectionWidget.kt b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/deviceconnection/DeviceConnectionWidget.kt index e18fda475..7acddee98 100644 --- a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/deviceconnection/DeviceConnectionWidget.kt +++ b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/deviceconnection/DeviceConnectionWidget.kt @@ -72,7 +72,6 @@ import com.apadmi.mockzilla.ui.ui.common.components.StandardTextTooltip import com.apadmi.mockzilla.ui.ui.common.components.buttons.BaseButton import com.apadmi.mockzilla.ui.ui.common.components.buttons.SolidButton import com.apadmi.mockzilla.ui.ui.common.theme.LocalMonoFontFamily -import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.theme.success @@ -313,7 +312,7 @@ private fun ConnectionCard( HorizontalDivider(Modifier.weight(1f), color = colorScheme.outline) Text( text = strings.widgets.deviceConnection.connectAutomatically, - color = colorScheme.onSurfaceFaint, + color = colorScheme.onSurfaceMuted, style = MaterialTheme.typography.labelMedium, ) HorizontalDivider(Modifier.weight(1f), color = colorScheme.outline) diff --git a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/devicetabs/DeviceTabsWidget.kt b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/devicetabs/DeviceTabsWidget.kt index ed487c101..c2907d4c4 100644 --- a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/devicetabs/DeviceTabsWidget.kt +++ b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/devicetabs/DeviceTabsWidget.kt @@ -65,7 +65,9 @@ import com.apadmi.mockzilla.ui.ui.common.components.PlatformHorizontalScrollbar import com.apadmi.mockzilla.ui.ui.common.components.PreviewSurface import com.apadmi.mockzilla.ui.ui.common.theme.LocalMonoFontFamily import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.theme.success +import com.apadmi.mockzilla.ui.utils.iconButtonSize import kotlin.Float @@ -237,7 +239,7 @@ fun DeviceTabsWidgetContent( text = LocalStrings.current.widgets.deviceTabs.betaBanner, modifier = Modifier.align(Alignment.CenterEnd).padding(8.dp), style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurfaceFaint + color = MaterialTheme.colorScheme.onSurfaceMuted ) } } @@ -362,12 +364,9 @@ private fun DeviceTab( ) } - Box( - modifier = Modifier - .size(12.dp) - .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = onClose), - contentAlignment = Alignment.Center, + IconButton( + onClick = onClose, + modifier = Modifier.iconButtonSize(), ) { Icon( imageVector = Icons.Filled.Close, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/androidMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.android.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/androidMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.android.kt index 82d04c133..e62606e74 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/androidMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.android.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/androidMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.android.kt @@ -1,6 +1,7 @@ package com.apadmi.mockzilla.ui.di.utils import androidx.compose.runtime.Composable +import com.apadmi.mockzilla.ui.engine.device.Device import com.apadmi.mockzilla.ui.viewmodel.ViewModel as InternalViewModel @@ -21,13 +22,14 @@ actual inline fun Module.viewModel( @Composable actual inline fun getViewModel( qualifier: Qualifier?, - key: String?, + device: Device?, + keyPrefix: String?, noinline parameters: ParametersDefinition? ): T = koinViewModel( qualifier = qualifier, - key = key, + key = keyPrefix + device.toString(), parameters = parameters, scope = MockzillaUiKoinContext.koin.scopeRegistry.rootScope ) -actual fun evictDesktopViewModelsForKey(key: String) = Unit +actual fun evictDesktopViewModelsForKey(device: Device, keyPrefix: String?) = Unit diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.kt index 2edde6946..c38fba163 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.kt @@ -1,6 +1,7 @@ package com.apadmi.mockzilla.ui.di.utils import androidx.compose.runtime.Composable +import com.apadmi.mockzilla.ui.engine.device.Device import com.apadmi.mockzilla.ui.viewmodel.ViewModel import org.koin.core.definition.Definition import org.koin.core.definition.KoinDefinition @@ -16,8 +17,9 @@ expect inline fun Module.viewModel( @Composable expect inline fun getViewModel( qualifier: Qualifier? = null, - key: String? = null, + device: Device? = null, + keyPrefix: String? = null, noinline parameters: ParametersDefinition? = null ): T -expect fun evictDesktopViewModelsForKey(key: String) +expect fun evictDesktopViewModelsForKey(device: Device, keyPrefix: String? = null) diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt index 8a8c2d4e9..04b0a290d 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt @@ -17,6 +17,7 @@ interface ActiveDeviceMonitor { // Fires when a device connects / disconnects val onDeviceConnectionStateChange: Flow + val onDeviceRemoved: Flow val allDevices: Collection } @@ -35,6 +36,7 @@ internal class ActiveDeviceManagerImpl( ) : ActiveDeviceMonitor, ActiveDeviceSelector { override val selectedDevice = MutableStateFlow(null) override val onDeviceConnectionStateChange = MutableSharedFlow(replay = 1) + override val onDeviceRemoved = MutableSharedFlow(replay = 0) private val allDevicesInternal = mutableMapOf() override val allDevices get() = allDevicesInternal.values @@ -106,6 +108,7 @@ internal class ActiveDeviceManagerImpl( } allDevicesInternal.remove(device) onDeviceConnectionStateChange.emit(Unit) + onDeviceRemoved.emit(device) } } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/i18n/En.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/i18n/En.kt index f57a2da79..8ef4b640d 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/i18n/En.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/i18n/En.kt @@ -193,7 +193,7 @@ val EnStrings = Strings( filterPlaceholderEmpty = "No matches", statusCodeFallback = "XXX", applyLabel = "Apply", - appliedLabel = "Set", + appliedLabel = "Applied", editLabel = "Edit", forceFailureBannerTitle = "Forced Failure is on.", forceFailureBannerBody = "Presets are ignored and locked", diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/CustomSlider.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/CustomSlider.kt index cf9a8ef1d..b8845412c 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/CustomSlider.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/CustomSlider.kt @@ -13,88 +13,49 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint - @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomSlider( value: Float, onValueChange: (Float) -> Unit, + steps: Int = 0, modifier: Modifier = Modifier, valueRange: ClosedFloatingPointRange = 0f..1f, - enabled: Boolean = true, - activeTrackColor: Color? = null, - showThumb: Boolean = true, - onValueChangeFinished: (() -> Unit)? = null, ) { val colorScheme = MaterialTheme.colorScheme - val activeColor = activeTrackColor ?: colorScheme.primary Slider( + modifier = modifier, value = value, - onValueChange = onValueChange, - modifier = modifier.height(24.dp), - enabled = enabled, + onValueChange = { onValueChange(it) }, + steps = steps, valueRange = valueRange, - onValueChangeFinished = onValueChangeFinished, - thumb = { - if (showThumb) { - Box( - modifier = Modifier - .size(22.dp) - .shadow( - elevation = 2.dp, - shape = CircleShape, - clip = false - ) - .background(Color.Transparent, CircleShape), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .size(12.dp) - .background( - if (enabled) { - activeColor - } else { - colorScheme.onSurfaceFaint - }, - CircleShape - ) - ) - } - } + Box( + modifier = Modifier + .size(16.dp) + .clip(CircleShape) + .background(colorScheme.primary) + ) }, - - track = { sliderState -> + track = { state -> SliderDefaults.Track( - sliderState = sliderState, - modifier = Modifier.height(4.dp), + sliderState = state, + modifier = Modifier.height(2.dp), colors = SliderDefaults.colors( - activeTrackColor = activeColor, - inactiveTrackColor = colorScheme.outline.copy(alpha = 0.25f), - disabledActiveTrackColor = colorScheme.onSurfaceFaint.copy(alpha = 0.3f), - disabledInactiveTrackColor = colorScheme.outline.copy(alpha = 0.1f), - thumbColor = Color.Transparent, + activeTrackColor = colorScheme.primary, + inactiveTrackColor = colorScheme.outline, + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, ), - drawStopIndicator = null, - thumbTrackGapSize = 0.dp, ) - }, - - colors = SliderDefaults.colors( - thumbColor = Color.Transparent, - activeTrackColor = activeColor, - inactiveTrackColor = colorScheme.outline.copy(alpha = 0.25f), - ), + } ) } @@ -109,11 +70,5 @@ private fun MockzillaSliderPreview() = PreviewSurface { value = 0.4f, onValueChange = {}, ) - - CustomSlider( - value = 0.7f, - onValueChange = {}, - enabled = false, - ) } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/EmptyState.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/EmptyState.kt index 871732e5a..b5178d06f 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/EmptyState.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/EmptyState.kt @@ -19,6 +19,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted + @Composable fun EmptyState( title: String, @@ -39,7 +42,7 @@ fun EmptyState( text = title, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + color = MaterialTheme.colorScheme.onSurfaceMuted ) description?.let { Spacer(modifier = Modifier.height(4.dp)) @@ -47,7 +50,7 @@ fun EmptyState( text = it, style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + color = MaterialTheme.colorScheme.onSurfaceFaint ) } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt index e611404dc..42321ac3a 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt @@ -4,7 +4,6 @@ package com.apadmi.mockzilla.ui.ui.common.components import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,14 +12,13 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight 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.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check import androidx.compose.material3.ColorScheme -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,6 +32,7 @@ import androidx.compose.ui.unit.dp import com.apadmi.mockzilla.ui.i18n.LocalStrings import com.apadmi.mockzilla.ui.i18n.Strings +import com.apadmi.mockzilla.ui.ui.common.assets.CircleCheck import com.apadmi.mockzilla.ui.ui.common.assets.LightningBolt import com.apadmi.mockzilla.ui.ui.common.components.buttons.BaseButton import com.apadmi.mockzilla.ui.ui.common.components.buttons.ButtonSize @@ -89,7 +88,7 @@ internal fun ForceFailureBanner( Row( modifier = modifier .fillMaxWidth() - .height(64.dp) + .heightIn(min = 64.dp) .clip(shape) .background(color = colorScheme.background) .border( @@ -144,29 +143,14 @@ internal fun ForceFailureBanner( if (state != ForceFailureBannerState.Normal) { val successColor = colorScheme.success.primary - val resumeShape = RoundedCornerShape(resumeButtonCornerRadius.dp) - Row( - modifier = Modifier - .clip(resumeShape) - .border(1.dp, successColor, resumeShape) - .clickable { onRestoreApiClicked() } - .padding(horizontal = 12.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - tint = successColor, - modifier = Modifier.size(14.dp), - ) - Text( - text = strings.widgets.globalControls.restoreButtonLabel, - style = MaterialTheme.typography.labelSmall, - color = successColor, - fontWeight = FontWeight.Medium, - ) - } + BaseButton( + label = strings.widgets.globalControls.restoreButtonLabel, + leadingIcon = Icons.CircleCheck, + contentColor = successColor, + size = ButtonSize.Sm, + variant = ButtonVariant.Outline, + onClick = onRestoreApiClicked, + ) } } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/PresetCard.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/PresetCard.kt index e2ff87297..882f5caba 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/PresetCard.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/PresetCard.kt @@ -66,6 +66,7 @@ import com.apadmi.mockzilla.ui.ui.common.assets.EditUnderscore import com.apadmi.mockzilla.ui.ui.common.components.editor.EditorMode import com.apadmi.mockzilla.ui.ui.common.theme.LocalForceDarkMode import com.apadmi.mockzilla.ui.ui.common.theme.StateColors +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.theme.success import com.apadmi.mockzilla.ui.ui.common.theme.warning @@ -74,6 +75,7 @@ import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.endpoints.RowDensity import com.apadmi.mockzilla.ui.ui.common.widgets.monitorlogs.details.minifyJson import com.apadmi.mockzilla.ui.ui.common.widgets.monitorlogs.details.prettyPrintJson import com.apadmi.mockzilla.ui.ui.common.widgets.monitorlogs.details.typeFormat +import com.apadmi.mockzilla.ui.utils.minimumTouchTarget import io.ktor.http.HttpStatusCode @@ -168,7 +170,7 @@ internal fun PresetCard( animationSpec = tween(200), label = "chevron", ) - val chevronTint = if (hasExpandableContent) iconTint else colorScheme.onSurface.copy(alpha = 0.3f) + val chevronTint = if (hasExpandableContent) iconTint else colorScheme.onSurfaceFaint Column( Modifier.fillMaxWidth() @@ -230,6 +232,7 @@ internal fun PresetCard( ) { Box( modifier = Modifier + .minimumTouchTarget() .fillMaxHeight() .clip(RoundedCornerShape(6.dp)) .clickable { onEdit() } @@ -272,20 +275,18 @@ internal fun PresetCard( ) } - PresetCardVariant.Selectable -> Box( + PresetCardVariant.Selectable -> Tag( modifier = Modifier + .minimumTouchTarget() .clip(RoundedCornerShape(6.dp)) - .clickable { onClicked(preset) } - ) { - Tag( - label = strings.applyLabel, - textColor = colorScheme.onSurface, - borderColor = colorScheme.outline, - backgroundColor = colorScheme.surfaceContainer, - shape = RoundedCornerShape(6.dp), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - ) - } + .clickable { onClicked(preset) }, + label = strings.applyLabel, + textColor = colorScheme.onSurface, + borderColor = colorScheme.outline, + backgroundColor = colorScheme.surfaceContainer, + shape = RoundedCornerShape(6.dp), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), + ) } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ResponseLatencyCard.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ResponseLatencyCard.kt index d8a053beb..e72ea5518 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ResponseLatencyCard.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ResponseLatencyCard.kt @@ -48,11 +48,15 @@ import androidx.compose.ui.unit.sp import com.apadmi.mockzilla.ui.engine.maxLatencySliderMs import com.apadmi.mockzilla.ui.i18n.LocalStrings import com.apadmi.mockzilla.ui.i18n.Strings +import com.apadmi.mockzilla.ui.ui.common.components.buttons.BaseButton +import com.apadmi.mockzilla.ui.ui.common.components.buttons.ButtonSize +import com.apadmi.mockzilla.ui.ui.common.components.buttons.ButtonVariant import com.apadmi.mockzilla.ui.ui.common.theme.LocalForceDarkMode import com.apadmi.mockzilla.ui.ui.common.theme.darkSurface import com.apadmi.mockzilla.ui.ui.common.theme.mockzillaMonoFontFamily import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.theme.warning +import com.apadmi.mockzilla.ui.utils.minimumTouchTarget import kotlin.math.max import kotlin.math.min @@ -143,7 +147,7 @@ internal fun ResponseLatencyCard( } } - val updateValue = remember { + val updateValue = remember(onChange) { { it: Int -> val clamped = it.clamped() value = clamped @@ -199,28 +203,13 @@ internal fun ResponseLatencyCard( ) ) - Row( - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = onReset) - .padding(4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = colorScheme.onSurfaceVariant - ) - Text( - text = strings.widgets.latency.clear, - style = MaterialTheme.typography.labelMedium.copy( - color = colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Medium - ) - ) - } + BaseButton( + variant = ButtonVariant.Ghost, + size = ButtonSize.Sm, + leadingIcon = Icons.Default.Close, + label = strings.widgets.latency.clear, + onClick = onReset, + ) } } @@ -331,8 +320,6 @@ internal fun ResponseLatencyCard( modifier = Modifier .fillMaxWidth() .padding(end = if (isOverflowing) 4.dp else 0.dp), - activeTrackColor = colorScheme.primary, - showThumb = !isOverflowing, onValueChange = { updateValue(it.toInt()) }, @@ -348,9 +335,7 @@ internal fun ResponseLatencyCard( } Row( - modifier = Modifier - .fillMaxWidth() - .height(14.dp), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -398,6 +383,7 @@ internal fun ResponseLatencyCard( Box( modifier = Modifier + .minimumTouchTarget() .clip(componentShape) .border( width = 1.dp, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/BaseButton.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/BaseButton.kt index a3ffc369e..aba97e772 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/BaseButton.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/BaseButton.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -34,6 +33,7 @@ import androidx.compose.ui.unit.dp import com.apadmi.mockzilla.ui.ui.common.assets.LightningBolt import com.apadmi.mockzilla.ui.ui.common.components.PreviewSurface +import com.apadmi.mockzilla.ui.utils.minimumTouchTarget enum class ButtonVariant { Danger, Ghost, Outline, Soft, Solid @@ -81,7 +81,7 @@ fun BaseButton( val borderTarget = when (variant) { ButtonVariant.Ghost -> Color.Transparent - ButtonVariant.Outline -> if (isHovered && enabled) colorScheme.outlineVariant else colorScheme.outline + ButtonVariant.Outline -> contentColor ?: if (isHovered && enabled) colorScheme.outlineVariant else colorScheme.outline ButtonVariant.Solid -> Color.Transparent ButtonVariant.Soft -> colorScheme.outline ButtonVariant.Danger -> colorScheme.error.copy(alpha = 0.3f) @@ -97,7 +97,7 @@ fun BaseButton( val iconSize = if (size == ButtonSize.Lg) 18.dp else 14.dp Button( - modifier = modifier.defaultMinSize(minHeight = 1.dp).alpha(if (enabled) 1f else 0.5f), + modifier = modifier.minimumTouchTarget().alpha(if (enabled) 1f else 0.5f), onClick = onClick, enabled = enabled, shape = RoundedCornerShape(6.dp), diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/DensityButtons.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/DensityButtons.kt index 88f61f8a5..0c3f197da 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/DensityButtons.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/buttons/DensityButtons.kt @@ -3,16 +3,20 @@ package com.apadmi.mockzilla.ui.ui.common.components.buttons import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape 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.text.font.FontWeight import androidx.compose.ui.unit.dp + import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.endpoints.RowDensity +import com.apadmi.mockzilla.ui.utils.minimumTouchTarget private const val UNSELECTED_BORDER_ALPHA = 0.2f @@ -40,14 +44,20 @@ private fun RowDensityButton( val cs = MaterialTheme.colorScheme val borderColor = if (isSelected) cs.onSurface else cs.onSurface.copy(alpha = UNSELECTED_BORDER_ALPHA) - Text( - text = label, + Box( + contentAlignment = Alignment.Center, modifier = Modifier - .border(width = 1.dp, color = borderColor, shape = RoundedCornerShape(4.dp)) - .clickable(onClick = onClick) - .padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.labelSmall, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, - color = if (isSelected) cs.onSurface else cs.onSurfaceVariant, - ) + .minimumTouchTarget() + .clickable(onClick = onClick), + ) { + Text( + text = label, + modifier = Modifier + .border(width = 1.dp, color = borderColor, shape = RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.labelSmall, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + color = if (isSelected) cs.onSurface else cs.onSurfaceVariant, + ) + } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/EditorTextField.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/EditorTextField.kt index 54fd8ac51..d8961b59e 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/EditorTextField.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/EditorTextField.kt @@ -329,7 +329,7 @@ private fun EditorContent( Icon( imageVector = Icons.DragCorner, contentDescription = null, - tint = colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + tint = colorScheme.onSurfaceMuted, modifier = Modifier .align(Alignment.BottomEnd) .size(16.dp) diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/FindableEditorTextField.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/FindableEditorTextField.kt index aa6ed0aeb..63373ab2d 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/FindableEditorTextField.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/editor/FindableEditorTextField.kt @@ -14,6 +14,7 @@ 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -59,6 +60,7 @@ import com.apadmi.mockzilla.ui.ui.common.components.CustomTextField import com.apadmi.mockzilla.ui.ui.common.components.buttons.BaseButton import com.apadmi.mockzilla.ui.ui.common.components.buttons.ButtonSize import com.apadmi.mockzilla.ui.ui.common.components.buttons.ButtonVariant +import com.apadmi.mockzilla.ui.utils.iconButtonSize @Composable internal fun FindableEditorTextField( @@ -246,7 +248,7 @@ private fun FindReplaceBar( ) { IconButton( onClick = onToggleReplaceMode, - modifier = Modifier.size(24.dp), + modifier = Modifier.iconButtonSize(), ) { Icon( imageVector = Icons.Default.KeyboardArrowRight, @@ -299,7 +301,7 @@ private fun FindReplaceBar( }, ) - IconButton(onClick = onPrev, modifier = Modifier.size(28.dp)) { + IconButton(onClick = onPrev, modifier = Modifier.iconButtonSize()) { Icon( imageVector = Icons.Default.KeyboardArrowUp, contentDescription = strings.previousMatchDescription, @@ -307,7 +309,7 @@ private fun FindReplaceBar( tint = colorScheme.onSurfaceVariant, ) } - IconButton(onClick = onNext, modifier = Modifier.size(28.dp)) { + IconButton(onClick = onNext, modifier = Modifier.iconButtonSize()) { Icon( imageVector = Icons.Default.KeyboardArrowDown, contentDescription = strings.nextMatchDescription, @@ -321,7 +323,7 @@ private fun FindReplaceBar( .width(1.dp) .background(colorScheme.outlineVariant), ) - IconButton(onClick = onClose, modifier = Modifier.size(28.dp)) { + IconButton(onClick = onClose, modifier = Modifier.iconButtonSize()) { Icon( imageVector = Icons.Default.Close, contentDescription = strings.closeFindBarDescription, @@ -356,7 +358,7 @@ private fun FindReplaceBar( leadingIcon = Icons.Default.FindReplace, variant = ButtonVariant.Soft, size = ButtonSize.Sm, - modifier = Modifier.height(30.dp), + modifier = Modifier.heightIn(min = 30.dp), onClick = onReplace, ) BaseButton( @@ -364,7 +366,7 @@ private fun FindReplaceBar( leadingIcon = Icons.Default.DoneAll, variant = ButtonVariant.Soft, size = ButtonSize.Sm, - modifier = Modifier.height(30.dp), + modifier = Modifier.heightIn(min = 30.dp), onClick = onReplaceAll, ) } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Color.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Color.kt index 9da0551dc..51972569e 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Color.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Color.kt @@ -23,13 +23,13 @@ internal val lightElevated = Color(0xFFBABFC6) // ── On-surface text ────────────────────────────────────────────────────────── internal val darkOnSurface = Color(0xFFECEFF3) internal val darkOnSurfaceVariant = Color(0xFFC0C4CB) -internal val darkOnSurfaceMuted = Color(0xFF888C94) -internal val darkOnSurfaceFaint = Color(0xFF5A5E65) +internal val darkOnSurfaceMuted = Color(0xFF91959D) +internal val darkOnSurfaceFaint = Color(0xFF747880) internal val lightOnSurface = Color(0xFF161A20) internal val lightOnSurfaceVariant = Color(0xFF2A2E34) internal val lightOnSurfaceMuted = Color(0xFF494D55) -internal val lightOnSurfaceFaint = Color(0xFF646971) +internal val lightOnSurfaceFaint = Color(0xFF565A61) // ── Outline ────────────────────────────────────────────────────────────────── internal val darkOutline = Color(0xFF2E323A) @@ -43,7 +43,7 @@ internal val darkOnPrimary = Color(0xFF07090D) internal val darkPrimaryContainer = Color(0xFF123738) internal val darkInversePrimary = Color(0xFF00858D) -internal val lightPrimary = Color(0xFF00858D) +internal val lightPrimary = Color(0xFF005F67) internal val lightOnPrimary = Color(0xFFFAFCFF) internal val lightPrimaryContainer = Color(0xFFC0E7E8) internal val lightInversePrimary = Color(0xFF68D5DA) @@ -58,13 +58,13 @@ internal val darkErrorContainer = Color(0xFF37292c) internal val darkInfo = Color(0xFF59C5F5) internal val darkInfoContainer = Color(0xFF073241) -internal val lightSuccess = Color(0xFF007C36) +internal val lightSuccess = Color(0xFF006A2D) internal val lightSuccessContainer = Color(0xFFC0F3D0) -internal val lightWarning = Color(0xFFB16600) +internal val lightWarning = Color(0xFF8A5000) internal val lightWarningContainer = Color(0xFFFFE1B6) -internal val lightError = Color(0xFFC9222B) +internal val lightError = Color(0xFFA81B23) internal val lightErrorContainer = Color(0xFFe6d7d9) -internal val lightInfo = Color(0xFF0075A9) +internal val lightInfo = Color(0xFF005F88) internal val lightInfoContainer = Color(0xFFC4ECFF) // ── HTTP methods ───────────────────────────────────────────────────────────── @@ -75,13 +75,13 @@ internal val darkMethodPatch = Color(0xFFCEA4FF) internal val darkMethodDelete = Color(0xFFFD736D) internal val darkMethodOther = Color(0xFF888C94) -internal val lightMethodGet = Color(0xFF008892) -internal val lightMethodPost = Color(0xFF007C36) -internal val lightMethodPut = Color(0xFFB76C00) -internal val lightMethodPatch = Color(0xFF794DB6) -internal val lightMethodDelete = Color(0xFFC9222B) +internal val lightMethodGet = Color(0xFF006F77) +internal val lightMethodPost = Color(0xFF006A2D) +internal val lightMethodPut = Color(0xFF8A5200) +internal val lightMethodPatch = Color(0xFF6A3EA8) +internal val lightMethodDelete = Color(0xFFA81B23) internal val lightMethodOther = Color(0xFF494D55) // ── JSON syntax ────────────────────────────────────────────────────────────── internal val darkJsonKey = Color(0xFFCEA4FF) -internal val lightJsonKey = Color(0xFF794DB6) +internal val lightJsonKey = Color(0xFF6A3EA8) diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Theme.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Theme.kt index 76d2fd13b..8aafd3326 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Theme.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/theme/Theme.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import com.apadmi.mockzilla.ui.i18n.ProvideLocalisableStrings -import com.apadmi.mockzilla.ui.ui.common.widgets.monitorlogs.details.ALPHA_MUTED import com.apadmi.mockzilla.ui.utils.Platform @Suppress("VARIABLE_NAME_INCORRECT_FORMAT") @@ -158,7 +157,7 @@ internal val ColorScheme.jsonHighlight: JsonHighlightColors stringColor = success.primary, numberColor = tertiary, boolColor = warning.primary, - nullColor = onSurface.copy(alpha = ALPHA_MUTED) + nullColor = onSurfaceMuted ) @get:Composable diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/utils/formatting/BodyVisualTransformation.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/utils/formatting/BodyVisualTransformation.kt index a21745617..e7f823a99 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/utils/formatting/BodyVisualTransformation.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/utils/formatting/BodyVisualTransformation.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.text.SpanStyle import com.apadmi.mockzilla.ui.ui.common.components.editor.EditorMode import com.apadmi.mockzilla.ui.ui.common.theme.jsonHighlight +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted internal object BodyVisualTransformation { internal fun isBodyTooLarge(body: String?) = body != null && body.length >= 25_000 @@ -15,10 +16,11 @@ internal object BodyVisualTransformation { val colorScheme = MaterialTheme.colorScheme val highlight = colorScheme.jsonHighlight + val onSurfaceMuted = colorScheme.onSurfaceMuted return remember(mode, highlight, colorScheme.onSurface, colorScheme.onSurfaceVariant) { when (mode) { EditorMode.Json -> JsonBodyVisualTransformation( - comment = SpanStyle(color = colorScheme.onSurface.copy(alpha = 0.5f)), + comment = SpanStyle(color = onSurfaceMuted), brace = SpanStyle(color = colorScheme.onSurfaceVariant), comma = SpanStyle(color = colorScheme.onSurfaceVariant), colon = SpanStyle(color = colorScheme.onSurfaceVariant), @@ -34,7 +36,7 @@ internal object BodyVisualTransformation { tagName = SpanStyle(color = highlight.keyColor), attributeName = SpanStyle(color = highlight.stringColor), attributeValue = SpanStyle(color = highlight.numberColor), - comment = SpanStyle(color = colorScheme.onSurface.copy(alpha = 0.5f)), + comment = SpanStyle(color = onSurfaceMuted), default = SpanStyle(color = colorScheme.onSurface), ) diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt index 5c92dd5e9..7dc78fdf5 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Spacer 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -86,10 +87,13 @@ import com.apadmi.mockzilla.ui.ui.common.components.buttons.OutlineButtonVariant import com.apadmi.mockzilla.ui.ui.common.components.editor.EditorMode import com.apadmi.mockzilla.ui.ui.common.components.editor.FindableEditorTextField import com.apadmi.mockzilla.ui.ui.common.theme.LocalMonoFontFamily +import com.apadmi.mockzilla.ui.ui.common.theme.jsonKey import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.createeditpreset.CreateEditPresetViewModel.* import com.apadmi.mockzilla.ui.utils.blockedPointerIcon +import com.apadmi.mockzilla.ui.utils.iconButtonSize +import com.apadmi.mockzilla.ui.utils.minimumTouchTarget import io.ktor.http.HttpStatusCode import org.koin.core.parameter.parametersOf @@ -114,23 +118,25 @@ private fun ColumnScope.HeadersSection( Row( modifier = Modifier .fillMaxWidth() - .height(48.dp) + .heightIn(min = 48.dp) .padding(start = 16.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( text = header.key, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = LocalMonoFontFamily.current), + color = MaterialTheme.colorScheme.jsonKey, + modifier = Modifier.weight(1f).padding(vertical = 4.dp), ) + Row( modifier = Modifier.weight(3f), verticalAlignment = Alignment.CenterVertically, ) { Text( - text = ": ${header.value}", - style = MaterialTheme.typography.bodyMedium, + text = header.value, + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = LocalMonoFontFamily.current), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f), ) @@ -191,39 +197,21 @@ private fun ColumnScope.HeadersSection( onValueChange = { localValue = it }, ) val canAdd = localKey.isNotEmpty() && localValue.isNotEmpty() - Box( + IconButton( + onClick = { + onAddHeader(localKey, localValue) + localKey = "" + localValue = "" + }, + enabled = canAdd, modifier = Modifier - .size(28.dp) - .background(MaterialTheme.colorScheme.surfaceContainer, RoundedCornerShape(6.dp)) - .border( - width = 1.dp, - color = if (canAdd) { - MaterialTheme.colorScheme.outline - } else { - MaterialTheme.colorScheme.outline.copy(alpha = 0.8f) - }, - shape = RoundedCornerShape(6.dp), - ) - .clickable( - enabled = canAdd, - onClick = { - onAddHeader(localKey, localValue) - localKey = "" - localValue = "" - } - ) + .iconButtonSize() .pointerHoverIcon(if (canAdd) PointerIcon.Hand else blockedPointerIcon), - contentAlignment = Alignment.Center, ) { Icon( imageVector = Icons.Default.Add, contentDescription = strings.addHeaderButton, - tint = if (canAdd) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) - }, - modifier = Modifier.size(14.dp) + modifier = Modifier.size(14.dp), ) } } @@ -366,10 +354,11 @@ fun CreateEditPresetWidget( onCancel: () -> Unit = {}, onSave: () -> Unit = {}, ) { - val vmKey = "CreateEditPresetViewModel-${activeEndpoint.raw}-$device" - val cleanupVm = { evictDesktopViewModelsForKey(key = vmKey) } + val keyPrefix = "CreateEditPresetViewModel-${activeEndpoint.raw}" + val cleanupVm = { evictDesktopViewModelsForKey(device = device, keyPrefix = keyPrefix) } val viewModel = getViewModel( - key = vmKey + device = device, + keyPrefix = keyPrefix ) { parametersOf( activeEndpoint, @@ -536,7 +525,7 @@ private fun BodySection( IconButton( onClick = onToggleExpand, - modifier = Modifier.size(40.dp), + modifier = Modifier.iconButtonSize(), ) { Icon( imageVector = if (isExpanded) Icons.Default.FullscreenExit else Icons.Default.Fullscreen, @@ -758,6 +747,7 @@ private fun BodyTypeToggle( val isHovered by interactionSource.collectIsHoveredAsState() Box( modifier = Modifier + .minimumTouchTarget() .clip(chipShape) .background( color = when { diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt index 3322482a8..ad2e1708f 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt @@ -84,11 +84,13 @@ import com.apadmi.mockzilla.ui.ui.common.components.buttons.RowDensityControls import com.apadmi.mockzilla.ui.ui.common.components.statusColors import com.apadmi.mockzilla.ui.ui.common.theme.LocalForceDarkMode import com.apadmi.mockzilla.ui.ui.common.theme.mockzillaMonoFontFamily +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.details.EndpointDetailsViewModel.* import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.details.components.PresetsContainer import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.endpoints.EndpointProperties import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.endpoints.RowDensity +import com.apadmi.mockzilla.ui.utils.iconButtonSize import org.koin.core.parameter.parametersOf @@ -231,27 +233,14 @@ private fun ColumnScope.PopulatedState( ) } Spacer(Modifier.width(8.dp)) - val customLabel = strings.endpointDetails.presets.typeDescriptions.other - Row( - modifier = Modifier - .clip(RoundedCornerShape(6.dp)) - .clickable(enabled = state.config.shouldFail != true, onClick = onCreatePreset) - .padding(horizontal = 8.dp, vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(12.dp), - ) - Text( - text = customLabel, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurface, - ) - } + BaseButton( + variant = ButtonVariant.Ghost, + size = ButtonSize.Sm, + leadingIcon = Icons.Default.Add, + label = strings.endpointDetails.presets.typeDescriptions.other, + enabled = state.config.shouldFail != true, + onClick = onCreatePreset, + ) } ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { @@ -287,7 +276,8 @@ fun EndpointDetailsWidget( ) { val uriHandler = LocalUriHandler.current val viewModel = getViewModel( - key = "${activeEndpoint?.raw}-$device" + device = device, + keyPrefix = activeEndpoint?.raw ) { parametersOf(activeEndpoint, device) } val state by viewModel.state.collectAsState() @@ -344,7 +334,7 @@ internal fun EndpointDetailsWidgetContent( imageVector = Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + tint = MaterialTheme.colorScheme.onSurfaceFaint ) } ) @@ -498,7 +488,7 @@ private fun ActivePresetBanner( color = colorScheme.onSurfaceVariant, ) } - IconButton(onClick = onClear, modifier = Modifier.size(24.dp)) { + IconButton(onClick = onClear, modifier = Modifier.iconButtonSize()) { Icon( imageVector = Icons.Default.Close, contentDescription = null, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsWidget.kt index 92f58f1b0..84bc3e58c 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsWidget.kt @@ -1,7 +1,6 @@ package com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.endpoints import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.hoverable @@ -19,13 +18,11 @@ import androidx.compose.foundation.layout.fillMaxSize 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.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Search @@ -43,7 +40,6 @@ 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.Size import androidx.compose.ui.graphics.Color @@ -63,7 +59,10 @@ import com.apadmi.mockzilla.ui.ui.common.components.EmptyState import com.apadmi.mockzilla.ui.ui.common.components.PlatformVerticalScrollbar import com.apadmi.mockzilla.ui.ui.common.components.PreviewSurface import com.apadmi.mockzilla.ui.ui.common.components.StatusChip +import com.apadmi.mockzilla.ui.ui.common.components.buttons.BaseButton +import com.apadmi.mockzilla.ui.ui.common.components.buttons.ButtonVariant import com.apadmi.mockzilla.ui.ui.common.components.buttons.RowDensityControls +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.theme.warning @@ -92,10 +91,10 @@ private fun RowDensity.verticalPadding(): Dp = when (this) { @Composable fun EndpointsWidget( device: Device, - onEndpointClicked: (Key) -> Unit, + onEndpointClicked: (Key?) -> Unit, onGlobalControlsClicked: () -> Unit ) { - val viewModel = getViewModel(key = device.toString()) { + val viewModel = getViewModel(device = device) { parametersOf(device) } val state by viewModel.state.collectAsState() @@ -107,8 +106,13 @@ fun EndpointsWidget( onFilterUpdate = viewModel::onFilterChanged, onRowDensityChanged = viewModel::onRowDensityChanged, onEndpointClicked = { key -> - selectedKey = key - onEndpointClicked(key) + if (selectedKey == key) { + selectedKey = null + onEndpointClicked(null) + } else { + selectedKey = key + onEndpointClicked(key) + } }, onGlobalControlsClicked = onGlobalControlsClicked ) @@ -173,38 +177,18 @@ private fun EndpointsList( } } -@Suppress("MAGIC_NUMBER") @Composable private fun GlobalControlsButton( isOpen: Boolean, strings: Strings = LocalStrings.current, onClick: () -> Unit ) { - val colorScheme = MaterialTheme.colorScheme - val shape = RoundedCornerShape(8.dp) - - Row( - modifier = Modifier - .clip(shape) - .background(if (isOpen) colorScheme.primary else colorScheme.surface) - .border(1.dp, if (isOpen) colorScheme.primary else colorScheme.outline, shape) - .clickable(onClick = onClick) - .padding(horizontal = 12.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - imageVector = Icons.Filled.Tune, - contentDescription = null, - tint = if (isOpen) colorScheme.onPrimary else colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp), - ) - Text( - text = strings.widgets.globalControls.title, - style = MaterialTheme.typography.labelMedium, - color = if (isOpen) colorScheme.onPrimary else colorScheme.onSurface, - ) - } + BaseButton( + variant = if (isOpen) ButtonVariant.Solid else ButtonVariant.Outline, + leadingIcon = Icons.Filled.Tune, + label = strings.widgets.globalControls.title, + onClick = onClick, + ) } @Composable @@ -298,7 +282,7 @@ private fun EndpointRow( Icon( imageVector = Icons.Default.ChevronRight, contentDescription = null, - tint = cs.onSurface.copy(alpha = 0.4f), + tint = cs.onSurfaceFaint, ) } HorizontalDivider(color = cs.onSurface.copy(alpha = 0.12f)) @@ -322,7 +306,7 @@ private fun EndpointRowChips( Text( text = strings.widgets.endpoints.noOverrides, style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + color = MaterialTheme.colorScheme.onSurfaceMuted, ) return } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/globalcontrols/GlobalControlsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/globalcontrols/GlobalControlsWidget.kt index badbc06dc..86c0c5c1f 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/globalcontrols/GlobalControlsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/globalcontrols/GlobalControlsWidget.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.FlowRow 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -54,12 +54,14 @@ import com.apadmi.mockzilla.ui.ui.common.theme.warning import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.endpoints.EndpointProperties import com.apadmi.mockzilla.ui.ui.common.widgets.endpoints.endpoints.EndpointsViewModel import com.apadmi.mockzilla.ui.ui.common.widgets.globalcontrols.GlobalControlsViewModel.State +import com.apadmi.mockzilla.ui.utils.Platform +import com.apadmi.mockzilla.ui.utils.iconButtonSize import org.koin.core.parameter.parametersOf @Composable fun GlobalControlsWidget(device: Device, onClose: () -> Unit = {}) { - val viewModel = getViewModel(key = device.toString()) { + val viewModel = getViewModel(device = device) { parametersOf(device) } val focusManager = LocalFocusManager.current @@ -158,7 +160,7 @@ internal fun GlobalControlsWidgetIdleContent( } BaseButton( - modifier = Modifier.height(32.dp), + modifier = Modifier.heightIn(min = 32.dp), label = strings.widgets.globalControls.resetAllLabel, variant = ButtonVariant.Ghost, size = ButtonSize.Sm, @@ -166,16 +168,18 @@ internal fun GlobalControlsWidgetIdleContent( onClick = onResetClicked ) - IconButton( - modifier = Modifier.size(24.dp), - onClick = onClose - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = strings.common.closeDescription, - tint = colorScheme.onSurfaceFaint, - modifier = Modifier.size(16.dp) - ) + if (Platform.current == Platform.Desktop) { + IconButton( + modifier = Modifier.iconButtonSize(), + onClick = onClose + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = strings.common.closeDescription, + tint = colorScheme.onSurfaceFaint, + modifier = Modifier.size(16.dp) + ) + } } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt index 7a578576e..a10f18382 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt @@ -44,6 +44,7 @@ import com.apadmi.mockzilla.ui.i18n.Strings import com.apadmi.mockzilla.ui.ui.common.assets.MockzillaLogo import com.apadmi.mockzilla.ui.ui.common.components.PreviewSurface import com.apadmi.mockzilla.ui.ui.common.components.SectionHeader +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import org.jetbrains.compose.resources.decodeToImageBitmap import org.koin.core.parameter.parametersOf @@ -63,7 +64,7 @@ private fun RunTarget.label(strings: Strings) = when (this) { @Composable fun MetaDataWidget(device: Device) { val viewModel = - getViewModel(key = device.toString()) { parametersOf(device) } + getViewModel(device = device) { parametersOf(device) } val state by viewModel.state.collectAsState() MetaDataWidgetContent(state, device) @@ -157,7 +158,7 @@ internal fun MetaDataRow( textAlign = TextAlign.Start, text = label, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + color = MaterialTheme.colorScheme.onSurfaceMuted ) Text( modifier = Modifier.weight(1f), @@ -215,7 +216,7 @@ private fun SessionRow(label: String, value: String) = Row( text = label, style = MaterialTheme.typography.bodySmall, fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + color = MaterialTheme.colorScheme.onSurfaceMuted ) Text( text = value, @@ -269,7 +270,7 @@ private fun AppHeader( text = appPackage, style = MaterialTheme.typography.bodySmall, fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + color = MaterialTheme.colorScheme.onSurfaceMuted, maxLines = 2 ) } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt index 93eb8f7b3..96224b78f 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt @@ -1,8 +1,6 @@ package com.apadmi.mockzilla.ui.ui.common.widgets.misccontrols import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,14 +10,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Restore import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -30,8 +25,6 @@ import androidx.compose.runtime.saveable.rememberSaveable 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.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -41,6 +34,7 @@ import com.apadmi.mockzilla.ui.di.utils.getViewModel import com.apadmi.mockzilla.ui.engine.device.Device import com.apadmi.mockzilla.ui.i18n.LocalStrings import com.apadmi.mockzilla.ui.i18n.Strings +import com.apadmi.mockzilla.ui.ui.common.components.CustomSlider import com.apadmi.mockzilla.ui.ui.common.components.CustomToggle import com.apadmi.mockzilla.ui.ui.common.components.PreviewSurface import com.apadmi.mockzilla.ui.ui.common.components.SectionHeader @@ -53,6 +47,7 @@ import com.apadmi.mockzilla.ui.ui.common.theme.LocalSetForceDarkMode import com.apadmi.mockzilla.ui.ui.common.theme.LocalSetScaleFactor import com.apadmi.mockzilla.ui.ui.common.theme.ScaleFactor import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import org.koin.core.parameter.parametersOf @@ -76,7 +71,7 @@ private data object PresentationModeScaleFactor { fun MiscControlsWidget( device: Device? ) { - val viewModel = getViewModel(key = device?.toString()) { parametersOf(device) } + val viewModel = getViewModel(device = device) { parametersOf(device) } val state by viewModel.state.collectAsState() MiscControlsWidgetContent( @@ -184,6 +179,11 @@ private fun DarkModeSettings( Row( modifier = Modifier.fillMaxWidth() + .toggleable( + value = LocalForceDarkMode.current, + onValueChange = { checked -> setForceDarkMode(checked) }, + role = Role.Switch, + ) .padding(horizontal = 4.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -244,38 +244,18 @@ private fun PresentationModeSettings( modifier = Modifier.fillMaxWidth().padding(end = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { - Slider( + CustomSlider( modifier = Modifier.weight(1f), value = presentationModeScaleFactor, - onValueChange = { onPresentationModeScaleFactorChange(it) }, steps = 5, + onValueChange = { onPresentationModeScaleFactorChange(it) }, valueRange = PresentationModeScaleFactor.MIN..PresentationModeScaleFactor.MAX, - thumb = { - Box( - modifier = Modifier - .size(16.dp) - .clip(CircleShape) - .background(colorScheme.primary) - ) - }, - track = { state -> - SliderDefaults.Track( - sliderState = state, - modifier = Modifier.height(2.dp), - colors = SliderDefaults.colors( - activeTrackColor = colorScheme.primary, - inactiveTrackColor = colorScheme.outline, - activeTickColor = Color.Transparent, - inactiveTickColor = Color.Transparent, - ), - ) - } ) Spacer(modifier = Modifier.width(8.dp)) Text( text = strings.widgets.miscControls.fontScaleLabel(presentationModeScaleFactor), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + color = MaterialTheme.colorScheme.onSurfaceMuted ) } } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt index e1e65b696..af8972ad1 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt @@ -56,6 +56,8 @@ import com.apadmi.mockzilla.ui.ui.common.components.EmptyState import com.apadmi.mockzilla.ui.ui.common.components.PreviewSurface import com.apadmi.mockzilla.ui.ui.common.components.StatusChip import com.apadmi.mockzilla.ui.ui.common.theme.LocalMonoFontFamily +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint +import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted import com.apadmi.mockzilla.ui.ui.common.theme.success import com.apadmi.mockzilla.ui.ui.common.theme.warning import com.apadmi.mockzilla.ui.ui.common.widgets.monitorlogs.details.formatTimestamp @@ -71,7 +73,7 @@ private fun HttpStatusCode.statusColor(isIntendedFailure: Boolean): Color { value in 200..299 -> cs.success.primary isIntendedFailure -> cs.warning.primary value >= 400 -> cs.error - else -> cs.onSurface.copy(alpha = 0.5f) + else -> cs.onSurfaceMuted } } @@ -85,7 +87,7 @@ fun MonitorLogsWidget( onExpandToggled: () -> Unit = {}, onViewDetail: (LogEvent) -> Unit, ) { - val viewModel = getViewModel(key = device.toString()) { parametersOf(device) } + val viewModel = getViewModel(device = device) { parametersOf(device) } val state by viewModel.state.collectAsState() MonitorLogsWidgetContent( @@ -112,7 +114,7 @@ fun LogRow( val isRealError = event.status.value >= 400 && !event.isIntendedFailure val errorColor = cs.error val errorBgColor = cs.errorContainer - val faintColor = cs.onSurface.copy(alpha = 0.3f) + val faintColor = cs.onSurfaceMuted val slowColor = cs.warning.primary Row( @@ -261,8 +263,7 @@ internal fun MonitorLogsWidgetContent( val monoFont = LocalMonoFontFamily.current val cs = MaterialTheme.colorScheme val streamingColor = cs.success.primary - val dimColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) - val faintColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + val dimColor = cs.onSurfaceMuted val entryList = state.entries val titleStyle = MaterialTheme.typography.labelSmall.copy( fontFamily = monoFont, @@ -291,7 +292,7 @@ internal fun MonitorLogsWidgetContent( Icon( imageVector = Icons.Default.KeyboardArrowRight, contentDescription = null, - tint = faintColor, + tint = cs.onSurfaceFaint, modifier = Modifier.size(14.dp).rotate(chevronRotation), ) Text( @@ -308,7 +309,7 @@ internal fun MonitorLogsWidgetContent( Text( text = strings.widgets.logs.clickToInspect, style = MaterialTheme.typography.labelSmall.copy(fontFamily = monoFont), - color = faintColor, + color = cs.onSurfaceMuted, ) Spacer(modifier = Modifier.weight(1f)) Text( @@ -342,7 +343,7 @@ private fun MonitorLogsList( strings: Strings = LocalStrings.current, ) { if (entryList.isEmpty()) { - val iconTint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + val iconTint = MaterialTheme.colorScheme.onSurfaceFaint EmptyState( title = strings.widgets.logs.emptyTitle, description = strings.widgets.logs.emptyDescription, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt index 8b2cca280..5990e1748 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt @@ -76,7 +76,8 @@ fun MonitorLogDetailsWidget( onClose: () -> Unit = {}, ) { val viewModel = getViewModel( - key = logDetail?.id ?: "empty" + device = device, + keyPrefix = logDetail?.id ) { parametersOf(device, logDetail) } val state by viewModel.state.collectAsState() @@ -218,7 +219,7 @@ internal fun MonitorLogDetailsEmptyContent( imageVector = Icons.Default.Menu, contentDescription = null, modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + tint = MaterialTheme.colorScheme.onSurfaceFaint ) } ) @@ -347,7 +348,7 @@ private fun LogTabBar( val selectedColor = if (isSelected) { MaterialTheme.colorScheme.primary } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + MaterialTheme.colorScheme.onSurfaceMuted } Column( @@ -439,7 +440,7 @@ private fun HeadersContent(headers: List>, strings: Strings Text( text = strings.widgets.logDetails.noHeaders, style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + color = MaterialTheme.colorScheme.onSurfaceMuted, modifier = Modifier.padding(vertical = 4.dp), ) return diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/utils/TouchTarget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/utils/TouchTarget.kt new file mode 100644 index 000000000..4b4f28bba --- /dev/null +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/utils/TouchTarget.kt @@ -0,0 +1,29 @@ +package com.apadmi.mockzilla.ui.utils + +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Suppress("MAGIC_NUMBER") +private val touchTargetSize: Dp + get() = when (Platform.current) { + Platform.Android, Platform.Ios -> 44.dp + else -> 24.dp + } + +/** Enforces WCAG AA minimum touch target (44dp mobile, 24dp desktop). + * + * @return*/ +fun Modifier.minimumTouchTarget(): Modifier = defaultMinSize( + minWidth = touchTargetSize, + minHeight = touchTargetSize, +) + +/** + * Sets an exact size for a compact IconButton based on the platform + * + * @return + */ +fun Modifier.iconButtonSize(): Modifier = size(touchTargetSize) diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.desktop.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.desktop.kt index a11d2dfcf..280c64ae5 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.desktop.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.desktop.kt @@ -2,6 +2,7 @@ package com.apadmi.mockzilla.ui.di.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.apadmi.mockzilla.ui.engine.device.Device import com.apadmi.mockzilla.ui.viewmodel.ViewModel import org.koin.core.definition.Definition import org.koin.core.module.Module @@ -23,13 +24,14 @@ actual inline fun Module.viewModel( @Composable actual inline fun getViewModel( qualifier: Qualifier?, - key: String?, + device: Device?, + keyPrefix: String?, noinline parameters: ParametersDefinition? -): T = remember(qualifier, key) { - key?.let { +): T = remember(qualifier, keyPrefix, device) { + device?.let { // Keyed VMs: check the persistent cache so the same instance is returned after the // composable re-enters composition (e.g. switching back to a device tab). - val cacheKey = "${T::class.qualifiedName}|$qualifier|$key" + val cacheKey = "${T::class.qualifiedName}|$qualifier|$keyPrefix|$device" @Suppress("UNCHECKED_CAST") desktopViewModelCache.getOrPut(cacheKey) { MockzillaUiKoinContext.koin.get(qualifier = qualifier, parameters = parameters) @@ -39,9 +41,9 @@ actual inline fun getViewModel( // Called when a device is fully removed from allDevices. Cancels the coroutine scope directly // because the base ViewModel class has no clear() method. -actual fun evictDesktopViewModelsForKey(key: String) { +actual fun evictDesktopViewModelsForKey(device: Device, keyPrefix: String?) { desktopViewModelCache.keys - .filter { it.endsWith("|$key") } + .filter { it.endsWith("${"|$keyPrefix".takeIf { keyPrefix != null } ?: ""}|$device") } .forEach { cacheKey -> (desktopViewModelCache.remove(cacheKey) as? ViewModel)?.viewModelScope?.cancel() } diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModelTests.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModelTests.kt index 17503f951..9f3ba006f 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModelTests.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModelTests.kt @@ -28,7 +28,6 @@ import kotlin.test.assertTrue import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.yield -import java.lang.Exception class CreateEditPresetViewModelTests : CoroutineTest() { private val dummyKey = EndpointConfiguration.Key("my-endpoint") diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/iosMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.ios.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/iosMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.ios.kt index 6e2fc5677..9fac16db8 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/iosMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.ios.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/iosMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.ios.kt @@ -2,6 +2,7 @@ package com.apadmi.mockzilla.ui.di.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.apadmi.mockzilla.ui.engine.device.Device import com.apadmi.mockzilla.ui.viewmodel.ViewModel import org.koin.core.definition.Definition import org.koin.core.module.Module @@ -16,10 +17,11 @@ actual inline fun Module.viewModel( @Composable actual inline fun getViewModel( qualifier: Qualifier?, - key: String?, + device: Device?, + keyPrefix: String?, noinline parameters: ParametersDefinition? -): T = remember(qualifier, key) { +): T = remember(qualifier, device) { MockzillaUiKoinContext.koin.get(qualifier = qualifier, parameters = parameters) } -actual fun evictDesktopViewModelsForKey(key: String) = Unit +actual fun evictDesktopViewModelsForKey(device: Device, keyPrefix: String?) = Unit diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/jsMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.js.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/jsMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.js.kt index 966662420..44fb5dffe 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/jsMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.js.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/jsMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinViewModel.js.kt @@ -2,6 +2,7 @@ package com.apadmi.mockzilla.ui.di.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.apadmi.mockzilla.ui.engine.device.Device import com.apadmi.mockzilla.ui.viewmodel.ViewModel import org.koin.core.definition.Definition import org.koin.core.parameter.ParametersDefinition @@ -15,10 +16,11 @@ actual inline fun org.koin.core.module.Module.viewModel( @Composable actual inline fun getViewModel( qualifier: Qualifier?, - key: String?, + device: Device?, + keyPrefix: String?, noinline parameters: ParametersDefinition? -): T = remember(qualifier, key) { +): T = remember(qualifier, device) { MockzillaUiKoinContext.koin.get(qualifier = qualifier, parameters = parameters) } -actual fun evictDesktopViewModelsForKey(key: String) = Unit +actual fun evictDesktopViewModelsForKey(device: Device, keyPrefix: String?) = Unit diff --git a/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/MobileAppRoot.kt b/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/MobileAppRoot.kt index 67e766440..63cffe4fe 100644 --- a/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/MobileAppRoot.kt +++ b/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/MobileAppRoot.kt @@ -143,7 +143,7 @@ internal fun MobileAppRoot( @Composable private fun DeviceContent(device: Device, navController: NavHostController) { - val viewModel = getViewModel(key = device.toString()) { parametersOf(device) } + val viewModel = getViewModel(device = device) { parametersOf(device) } val state by viewModel.state.collectAsState() when (val currentState = state) { @@ -207,9 +207,9 @@ private fun ConnectedState( ) { EndpointDetailsWidget( device = currentState.activeDevice.device, - activeEndpoint = EndpointConfiguration.Key( - backStackEntry.toRoute().key, - ), + activeEndpoint = backStackEntry.toRoute().key?.let { + EndpointConfiguration.Key(it) + }, onCreatePreset = { navController.navigate(Destination.CreateEditPreset(it.raw, true)) }, @@ -225,7 +225,7 @@ private fun ConnectedState( EndpointsWidget( device = currentState.activeDevice.device, onEndpointClicked = { - navController.navigate(Destination.EndpointDetails(it.raw)) + navController.navigate(Destination.EndpointDetails(it?.raw)) }, onGlobalControlsClicked = { navController.navigate(Destination.GlobalControls) @@ -249,6 +249,7 @@ private fun ConnectedState( ), creatingNewPreset = backStackEntry.toRoute().creatingNewPreset, onSave = navController::navigateUp, + onCancel = navController::navigateUp, ) } } diff --git a/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/utils/Destination.kt b/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/utils/Destination.kt index 92f435f3c..ab465a141 100644 --- a/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/utils/Destination.kt +++ b/mockzilla-management-ui/mockzilla-mobile-ui/src/commonMain/kotlin/com/apadmi/mockzilla/mobile/ui/utils/Destination.kt @@ -27,5 +27,5 @@ internal sealed class Destination { * @property key */ @Serializable - internal data class EndpointDetails(val key: String) : Destination() + internal data class EndpointDetails(val key: String?) : Destination() }