diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/UpstreamContentFocusTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/UpstreamContentFocusTest.kt deleted file mode 100644 index 78690396..00000000 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/UpstreamContentFocusTest.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.justinvisser.wolfskin.tv - -import android.app.Application -import android.view.View -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toPixelMap -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.test.assertIsFocused -import androidx.compose.ui.test.assertIsNotFocused -import androidx.compose.ui.test.captureToImage -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performKeyInput -import androidx.compose.ui.test.pressKey -import androidx.compose.ui.unit.dp -import com.github.damontecres.wholphin.data.model.JellyfinServer -import com.github.damontecres.wholphin.data.model.JellyfinUser -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.BackdropStyle -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.BackdropResult -import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.takahirom.roborazzi.captureRoboImage -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import java.io.File -import java.util.UUID -import kotlinx.coroutines.flow.MutableStateFlow -import org.junit.After -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.justinvisser.wolfskin.design.ShellPerson -import org.justinvisser.wolfskin.design.ShellState -import org.justinvisser.wolfskin.design.ShellSurface -import org.justinvisser.wolfskin.design.WolfskinTheme -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode - -/** The rollout-off path must preserve native content focus and its backdrop beneath the shell. */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") -@GraphicsMode(GraphicsMode.Mode.NATIVE) -class UpstreamContentFocusTest { - @get:Rule val rule = createAndroidComposeRule() - - @Before fun prepare() { - WolfskinScreenPreference.initialize(rule.activity) - WolfskinScreenPreference.enabled = false - rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) - @Suppress("DEPRECATION") - rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - } - - @After fun restore() { WolfskinScreenPreference.enabled = true } - - private fun shellState() = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin").apply { - pickerVisible = false - navFocused = false - } - - @Test fun switchOffUpMovesBetweenContentRowsBeforeEnteringNav() { - val state = shellState() - val first = FocusRequester() - val second = FocusRequester() - rule.setContent { - val upstream = resolveUpstreamDestination(Destination.Home(), WolfskinScreenPreference.enabled, emptyMap()) - WolfskinTheme { - ShellSurface(state, handlesContentKeys = upstream == null) { contentModifier -> - Column(contentModifier.padding(start = 100.dp, top = 150.dp), verticalArrangement = Arrangement.spacedBy(32.dp)) { - Box(Modifier.size(200.dp, 60.dp).testTag("upstream-first").focusRequester(first).focusable()) - Box(Modifier.size(200.dp, 60.dp).testTag("upstream-second").focusRequester(second).focusable()) - } - } - } - } - rule.runOnIdle { second.requestFocus() } - rule.onNodeWithTag("upstream-second").assertIsFocused() - rule.onNodeWithTag("upstream-second").performKeyInput { pressKey(Key.DirectionUp) } - rule.onNodeWithTag("upstream-first").assertIsFocused() - rule.runOnIdle { assertFalse("Up from the second row must not enter nav", state.navFocused) } - rule.onNodeWithTag("upstream-first").performKeyInput { pressKey(Key.DirectionUp) } - rule.runOnIdle { assertTrue("Only the first row exits upward to nav", state.navFocused) } - rule.onNodeWithTag("upstream-first").assertIsNotFocused() - rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionDown) } - rule.onNodeWithTag("upstream-first").assertIsFocused() - } - - @Test fun switchOffShowsRealUpstreamBackdropAndSwitchOnRestoresOwnedBackground() { - val state = shellState() - val model = mockk(relaxed = true) - every { model.session } returns WolfskinSession().also { it.state = state } - every { model.libraryDestinations } returns emptyMap() - // An unavailable saved identity uses the real sign-in placeholder, avoiding any server reads. - every { model.signedInPerson } returns "unavailable" - every { model.backdropFlow } returns MutableStateFlow(BackdropResult("fixture", null, Color.Red, Color.Red, Color.Red)) - coEvery { model.load(any(), any()) } returns Unit - val server = JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null) - val user = JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Justin", serverId = server.id, accessToken = null) - val defaults = AppPreferences.getDefaultInstance() - val preferences = UserPreferences(defaults.toBuilder().setInterfacePreferences(defaults.interfacePreferences.toBuilder().setBackdropStyle(BackdropStyle.BACKDROP_DYNAMIC_COLOR)).build(), null) - val navigation = NavigationManager() - rule.setContent { WolfskinApp(server, user, navigation, preferences, Modifier.fillMaxSize(), model) } - rule.mainClock.advanceTimeBy(1500) - rule.waitForIdle() - val off = rule.onRoot().captureToImage().toPixelMap()[1700, 900] - assertTrue("The real dynamic backdrop must remain visible through native content", off.red > .4f && off.green < .1f && off.blue < .1f) - val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory"))) - directory.mkdirs() - rule.onRoot().captureRoboImage(File(directory, "review-upstream-off.png").absolutePath) - rule.runOnIdle { WolfskinScreenPreference.enabled = true } - rule.mainClock.advanceTimeBy(1500) - rule.waitForIdle() - val on = rule.onRoot().captureToImage().toPixelMap()[1700, 900] - val background = org.justinvisser.wolfskin.design.WolfskinTokens.Color.bg - org.junit.Assert.assertEquals("Owned background red", background.red, on.red, 1f / 255f) - org.junit.Assert.assertEquals("Owned background green", background.green, on.green, 1f / 255f) - org.junit.Assert.assertEquals("Owned background blue", background.blue, on.blue, 1f / 255f) - rule.onRoot().captureRoboImage(File(directory, "review-upstream-on.png").absolutePath) - } -} diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt index 068c029b..a6b8a396 100644 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt +++ b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt @@ -2,7 +2,6 @@ package org.justinvisser.wolfskin.tv import android.content.Context import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -16,7 +15,6 @@ import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator @@ -25,12 +23,10 @@ import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.BackdropResult import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupNavigationManager -import com.github.damontecres.wholphin.ui.nav.Backdrop import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.DestinationContent import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption @@ -39,7 +35,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.justinvisser.wolfskin.design.PeopleStatus @@ -71,7 +66,6 @@ class WolfskinShellViewModel @Inject constructor( private val backdrop: BackdropService, private val libraries: LibraryReadFacade, ) : ViewModel() { - val backdropFlow: StateFlow = backdrop.backdropFlow var libraryDestinations by mutableStateOf>(emptyMap()) private set var signedInPerson by mutableStateOf("") @@ -156,46 +150,35 @@ fun WolfskinApp( val state = viewModel.session.state ?: loading WolfskinTheme { CompositionLocalProvider(LocalShellState provides state) { - Box(modifier) { - val current = navigationManager.backStack.last() - if (!state.pickerVisible && resolveUpstreamDestination(current, WolfskinScreenPreference.enabled, viewModel.libraryDestinations) != null) { - val backdrop by viewModel.backdropFlow.collectAsStateWithLifecycle() - Backdrop( - backdrop = backdrop, - drawerIsOpen = false, - backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle, - ) - } - NavDisplay( - backStack = navigationManager.backStack, - onBack = { if (navigationManager.backStack.last().fullScreen) navigationManager.goBack() else state.key(org.justinvisser.wolfskin.design.ShellKey.Back) }, - entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator()), - entryProvider = { destination -> - NavEntry(destination, contentKey = "${destination}_${server.id}_${user.id}") { - val route = when (destination) { - is Destination.Home -> "home" - is Destination.Wolfskin -> destination.route - else -> "pushed" - } - LaunchedEffect(destination) { if (!destination.fullScreen) state.enterDestination(route) } - if (destination.fullScreen) { - DestinationContent(destination, preferences, viewModel::clearBackdrop, Modifier.fillMaxSize()) - } else { - BackHandler { state.key(org.justinvisser.wolfskin.design.ShellKey.Back) } - val upstream = resolveUpstreamDestination(destination, WolfskinScreenPreference.enabled, viewModel.libraryDestinations) - ShellSurface(state, handlesContentKeys = upstream == null) { contentModifier -> - when { - upstream != null && state.person.key != viewModel.signedInPerson -> ShellPlaceholder("Sign in as ${state.person.name} to open their Jellyfin library", contentModifier) - upstream != null -> DestinationContent(upstream, preferences, viewModel::clearBackdrop, contentModifier) - destination is Destination.Wolfskin -> DestinationContent(destination, preferences, viewModel::clearBackdrop, contentModifier) - else -> ShellPlaceholder(route, contentModifier) - } + NavDisplay( + backStack = navigationManager.backStack, + onBack = { if (navigationManager.backStack.last().fullScreen) navigationManager.goBack() else state.key(org.justinvisser.wolfskin.design.ShellKey.Back) }, + entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator()), + entryProvider = { destination -> + NavEntry(destination, contentKey = "${destination}_${server.id}_${user.id}") { + val route = when (destination) { + is Destination.Home -> "home" + is Destination.Wolfskin -> destination.route + else -> "pushed" + } + LaunchedEffect(destination) { if (!destination.fullScreen) state.enterDestination(route) } + if (destination.fullScreen) { + DestinationContent(destination, preferences, viewModel::clearBackdrop, Modifier.fillMaxSize()) + } else { + BackHandler { state.key(org.justinvisser.wolfskin.design.ShellKey.Back) } + val upstream = resolveUpstreamDestination(destination, WolfskinScreenPreference.enabled, viewModel.libraryDestinations) + ShellSurface(state, modifier, handlesContentKeys = upstream == null) { contentModifier -> + when { + upstream != null && state.person.key != viewModel.signedInPerson -> ShellPlaceholder("Sign in as ${state.person.name} to open their Jellyfin library", contentModifier) + upstream != null -> DestinationContent(upstream, preferences, viewModel::clearBackdrop, contentModifier) + destination is Destination.Wolfskin -> DestinationContent(destination, preferences, viewModel::clearBackdrop, contentModifier) + else -> ShellPlaceholder(route, contentModifier) } } } - }, - ) - } + } + }, + ) } } } diff --git a/docker/README.md b/docker/README.md index 205f190a..6bfcbc8e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -72,19 +72,12 @@ outputs. Editing any of them without regenerating fails the build. The shell use the generated palette rather than the retired `WOLFSKIN` theme-enum value; the old numeric value is reserved. -Compose output maps source weights 600/650 to the platform Bold (700) face, which -resolves consistently in native captures; the approved JSON weights stay unchanged. -Nav, picker names, labels and pill weights use that same mapping. Nav label colour -fades for 200 ms ease-out; the blob retains its critically damped spring. - The small gear under the final person opens Settings with Requests focused. Settings groups hand off to existing upstream pages; the Requests entry explicitly has no request data yet. **Advanced settings → Wolfskin screens** is the persistent -rollout switch: turning it off renders upstream Home and library destinations with -the upstream Backdrop and top scrim, while retaining Wolfskin's nav, picker and -Settings list. Native Up traversal stays in content until the first focusable row, -then enters the nav. Turning the switch on restores the labelled placeholders -until the corresponding screen slices ship. +rollout switch: turning it off renders upstream Home and library destinations, +while retaining Wolfskin's nav, picker and Settings list. Turning it on renders +the labelled placeholders until the corresponding screen slices ship. Debug people, tabs and initials come from the shell-only projection of the locked prototype fixture in `app/src/debug/assets/wolfskin-shell.json`; release resources @@ -105,11 +98,7 @@ the container's ephemeral home. A cache miss still needs Maven Central; rate-lim hosts can seed the existing cache from SHA512-verified local Maven artifacts without changing the wrapper or adding volumes. -Default-flavour launcher artwork and English, Hungarian and Polish string overrides -live in `app/src/default/res/`. Legacy Wolfskin strings in 25 translated -`main` resource folders have not yet been moved; non-default `appstore`/`firetv` -flavours therefore combine upstream icons with some legacy Wolfskin text. Those -flavours and a full locale migration are not certified by this slice. Debug +Branding strings and launcher artwork live in `app/src/default/res/`. Debug assembly runs `checkWolfskinBranding` against every packaged APK through AGP's public artifact provider and AAPT2. It checks all string, plural and array configurations, rejecting user-visible upstream branding while preserving the upstream legal diff --git a/wolfskin-design/generate_tokens.py b/wolfskin-design/generate_tokens.py index 63a9707e..857f1979 100755 --- a/wolfskin-design/generate_tokens.py +++ b/wolfskin-design/generate_tokens.py @@ -21,13 +21,8 @@ def property_name(name: str) -> str: return name.replace("-", "_") -def compose_weight(weight: int) -> str: - """Use the platform bold face for intermediate weights that resolve to Regular in native captures.""" - return "FontWeight.Bold" if 600 <= weight <= 700 else f"FontWeight({weight})" - - def render_tokens(tokens: dict) -> tuple[str, str]: - """Translate locked values into platform-resolvable Compose and CSS tokens.""" + """Translate typed source values without choosing new design values.""" lines = [ "// Generated by generate_tokens.py; edit the JSON sources instead.", "package org.justinvisser.wolfskin.design", @@ -63,11 +58,11 @@ def render_tokens(tokens: dict) -> tuple[str, str]: weight = token.get("weight", token["$value"]["fontWeight"]) height = size * token.get("lineHeight", 1.3) tracking = token.get("letterSpacingEm", 0) - lines.append(f" val {name} = TextStyle(fontSize = {number(size)}.sp, fontWeight = {compose_weight(weight)}, lineHeight = {number(height)}.sp, letterSpacing = {number(tracking)}.em)") + lines.append(f" val {name} = TextStyle(fontSize = {number(size)}.sp, fontWeight = FontWeight({weight}), lineHeight = {number(height)}.sp, letterSpacing = {number(tracking)}.em)") css.append(f" --{group_name}-{name}: {number(token['px'])}px;") elif kind == "typography" and "sizes" in token: for context, pixels in token["sizes"].items(): - lines.append(f" val {name}{context[0].upper() + context[1:]} = TextStyle(fontSize = {number(pixels / 2)}.sp, fontWeight = {compose_weight(token['weight'])})") + lines.append(f" val {name}{context[0].upper() + context[1:]} = TextStyle(fontSize = {number(pixels / 2)}.sp, fontWeight = FontWeight({token['weight']}))") elif kind == "duration": lines.append(f" const val {name} = {int(value.removesuffix('ms'))}") css.append(f" --{group_name}-{name}: {value};") diff --git a/wolfskin-design/shell-tokens.json b/wolfskin-design/shell-tokens.json index d5e0c688..c18fc5d7 100644 --- a/wolfskin-design/shell-tokens.json +++ b/wolfskin-design/shell-tokens.json @@ -1,10 +1,5 @@ { "shell": { - "navLabelFade": { - "$type": "duration", - "$value": "200ms", - "source": "index.html:81; nav label color transition uses 200 ms ease-out" - }, "zero": { "$type": "dimension", "px": 0, diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt index 543ac321..c66107e2 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt @@ -2,8 +2,6 @@ package org.justinvisser.wolfskin.design import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.tween import androidx.compose.animation.core.spring import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -18,12 +16,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.tv.material3.Text @@ -52,7 +48,6 @@ fun navBoxes(tabs: List): List { @Composable fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier) { val boxes = remember(state.person.hasAnime) { navBoxes(state.tabs) } - val labelFade = remember { tween(S.navLabelFade, easing = CubicBezierEasing(0f, 0f, .58f, 1f)) } val index = if (state.navFocused) state.navIndex else state.currentTab val box = boxes[index] val avatarFocused = state.navFocused && index == state.tabs.size @@ -78,17 +73,10 @@ fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier) state.tabs.forEachIndexed { itemIndex, tab -> val item = boxes[itemIndex] val focused = state.navFocused && state.navIndex == itemIndex - val labelColor by animateColorAsState( - if (focused) C.onFocus else if (itemIndex == state.currentTab) C.fg else C.muted, - animationSpec = labelFade, - label = "nav-label-$tab", - ) - Box(Modifier.offset(item.left, item.top).size(item.width, item.height) - .focusProperties { canFocus = false } - .clickable { - state.focusNav(itemIndex); state.key(ShellKey.Ok) - }, contentAlignment = Alignment.Center) { - Text(tab.replaceFirstChar { it.uppercase() }, style = WolfskinTokens.Type.nav, color = labelColor) + Box(Modifier.offset(item.left, item.top).size(item.width, item.height).clickable { + state.focusNav(itemIndex); state.key(ShellKey.Ok) + }, contentAlignment = Alignment.Center) { + Text(tab.replaceFirstChar { it.uppercase() }, style = WolfskinTokens.Type.nav, color = if (focused) C.onFocus else if (itemIndex == state.currentTab) C.fg else C.muted) if (itemIndex == state.currentTab && !focused) { Box(Modifier.align(Alignment.BottomCenter).offset(y = -L.navUnderline).size(item.width - L.navItemPaddingX * 2, L.navUnderline).background(C.accent)) } @@ -98,7 +86,6 @@ fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier) Avatar(state.person, L.navAvatar, WolfskinTokens.Type.avatarLetterNav, Modifier.offset(avatar.left, avatar.top) .focusRing(avatarFocused, L.navAvatar / 2, WolfskinTokens.Focus.ringAvatarGap, WolfskinTokens.Focus.ringAvatarRing, C.fg) - .focusProperties { canFocus = false } .clickable { state.focusNav(state.tabs.size); state.key(ShellKey.Ok) }) } } diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt index 9823c319..2963c53b 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt @@ -96,7 +96,7 @@ fun SettingsRow(group: SettingsGroup, focused: Boolean, openRequests: Int?, modi Glyph(group.key, if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsIcon)) Column(Modifier.weight(1f)) { val count = if (openRequests != null && openRequests > 0) " · $openRequests open" else "" - Text(group.title + count, color = foreground, style = WolfskinTokens.Type.listTitle, maxLines = 1) + Text(group.title + count, color = foreground, style = WolfskinTokens.Type.listTitle.copy(fontWeight = FontWeight.Bold), maxLines = 1) Text(group.description, color = if (focused) C.onFocus else C.muted, style = WolfskinTokens.Type.caption, maxLines = 1, overflow = TextOverflow.Ellipsis) } if (group.wolfskin) SettingsPill("Wolfskin", focused) @@ -112,6 +112,6 @@ private fun SettingsPill(label: String, focused: Boolean) { val stroke = WolfskinTokens.Focus.ringButtonRing.toPx() drawRoundRect(foreground.copy(alpha = .4f), cornerRadius = CornerRadius(WolfskinTokens.Radius.pill.toPx()), style = Stroke(stroke, pathEffect = PathEffect.dashPathEffect(floatArrayOf(stroke * 2, stroke * 2)))) }.padding(horizontal = S.settingsPillPadding), contentAlignment = Alignment.Center) { - Text(label, color = foreground, style = WolfskinTokens.Type.caption.copy(fontWeight = WolfskinTokens.Type.label.fontWeight)) + Text(label, color = foreground, style = WolfskinTokens.Type.caption.copy(fontWeight = FontWeight.SemiBold)) } } diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ShellSurface.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ShellSurface.kt index 7132111f..7fea90b8 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ShellSurface.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ShellSurface.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.Key @@ -25,8 +24,6 @@ import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription @@ -59,12 +56,11 @@ fun ShellSurface( ) { val focus = remember { FocusRequester() } val contentFocus = remember { FocusRequester() } - val focusManager = LocalFocusManager.current LaunchedEffect(state.pickerVisible, state.navFocused, handlesContentKeys) { if (state.pickerVisible || state.navFocused || handlesContentKeys) focus.requestFocus() else contentFocus.requestFocus() } - Box(modifier.fillMaxSize().background(if (handlesContentKeys || state.pickerVisible) C.bg else Color.Transparent).testTag("wolfskin-shell") + Box(modifier.fillMaxSize().background(C.bg).testTag("wolfskin-shell") .semantics { stateDescription = "${state.person.key}:${state.route}:${if (state.pickerVisible) "picker" else if (state.navFocused) "nav-${state.navIndex}" else "content-${state.settingsRow}"}" } .focusRequester(focus) .onPreviewKeyEvent { @@ -72,11 +68,7 @@ fun ShellSurface( } .onKeyEvent { if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown) { - if (!focusManager.moveFocus(FocusDirection.Up)) { - state.navFocused = true - state.navIndex = state.currentTab - } - true + state.navFocused = true; state.navIndex = state.currentTab; true } else false }.focusable()) { if (state.pickerVisible) { diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt index 234a5be8..979764b0 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt @@ -65,15 +65,15 @@ object WolfskinTokens { val sheetTitle = TextStyle(fontSize = 22.0.sp, fontWeight = FontWeight(800), lineHeight = 28.6.sp, letterSpacing = -0.01.em) val wordmark = TextStyle(fontSize = 18.0.sp, fontWeight = FontWeight(800), lineHeight = 23.400000000000002.sp, letterSpacing = -0.02.em) val stateTitle = TextStyle(fontSize = 18.0.sp, fontWeight = FontWeight(400), lineHeight = 23.400000000000002.sp, letterSpacing = 0.0.em) - val rowHeading = TextStyle(fontSize = 17.0.sp, fontWeight = FontWeight.Bold, lineHeight = 22.1.sp, letterSpacing = -0.01.em) - val pickerName = TextStyle(fontSize = 17.0.sp, fontWeight = FontWeight.Bold, lineHeight = 22.1.sp, letterSpacing = 0.0.em) - val nav = TextStyle(fontSize = 15.0.sp, fontWeight = FontWeight.Bold, lineHeight = 19.5.sp, letterSpacing = 0.0.em) - val listTitle = TextStyle(fontSize = 15.0.sp, fontWeight = FontWeight.Bold, lineHeight = 19.5.sp, letterSpacing = 0.0.em) + val rowHeading = TextStyle(fontSize = 17.0.sp, fontWeight = FontWeight(700), lineHeight = 22.1.sp, letterSpacing = -0.01.em) + val pickerName = TextStyle(fontSize = 17.0.sp, fontWeight = FontWeight(650), lineHeight = 22.1.sp, letterSpacing = 0.0.em) + val nav = TextStyle(fontSize = 15.0.sp, fontWeight = FontWeight(600), lineHeight = 19.5.sp, letterSpacing = 0.0.em) + val listTitle = TextStyle(fontSize = 15.0.sp, fontWeight = FontWeight(650), lineHeight = 19.5.sp, letterSpacing = 0.0.em) val body = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight(400), lineHeight = 18.2.sp, letterSpacing = 0.0.em) - val button = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight.Bold, lineHeight = 18.2.sp, letterSpacing = 0.0.em) - val label = TextStyle(fontSize = 13.0.sp, fontWeight = FontWeight.Bold, lineHeight = 16.900000000000002.sp, letterSpacing = 0.0.em) + val button = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight(700), lineHeight = 18.2.sp, letterSpacing = 0.0.em) + val label = TextStyle(fontSize = 13.0.sp, fontWeight = FontWeight(650), lineHeight = 16.900000000000002.sp, letterSpacing = 0.0.em) val caption = TextStyle(fontSize = 12.0.sp, fontWeight = FontWeight(400), lineHeight = 15.600000000000001.sp, letterSpacing = 0.0.em) - val ratingPlate = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight.Bold, lineHeight = 16.8.sp, letterSpacing = 0.0.em) + val ratingPlate = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight(700), lineHeight = 16.8.sp, letterSpacing = 0.0.em) val avatarLetterPicker = TextStyle(fontSize = 50.0.sp, fontWeight = FontWeight(800)) val avatarLetterNav = TextStyle(fontSize = 15.0.sp, fontWeight = FontWeight(800)) val avatarLetterSheetRow = TextStyle(fontSize = 14.0.sp, fontWeight = FontWeight(800)) @@ -230,7 +230,6 @@ object WolfskinTokens { const val comingInVisibleRows = 5.0f } object Shell { - const val navLabelFade = 200 val zero = 0.0.dp val stageWidth = 960.0.dp val stageHeight = 540.0.dp diff --git a/wolfskin-design/tokens.css b/wolfskin-design/tokens.css index 120ab55e..ad862688 100644 --- a/wolfskin-design/tokens.css +++ b/wolfskin-design/tokens.css @@ -127,7 +127,6 @@ --motion-scroll: 180ms; --motion-ratingDwell: 350ms; --motion-toast: 3200ms; - --shell-navLabelFade: 200ms; --shell-zero: 0.0px; --shell-stageWidth: 1920.0px; --shell-stageHeight: 1080.0px; diff --git a/wolfskin-design/tokens.sha256 b/wolfskin-design/tokens.sha256 index 8816c8ee..e17d7d62 100644 --- a/wolfskin-design/tokens.sha256 +++ b/wolfskin-design/tokens.sha256 @@ -1,5 +1,5 @@ -1438f7ca4a2b6f951c9e643ab0afacbf2f7fc98e57905295d518c6225e054de4 generate_tokens.py -6d1d19656b40e6a442cb548bb238bb12244adb6b3b717ed9e564af61119559c8 shell-tokens.json -58131daf2bf889dcc2cf2a55f4a179fd6ad9bafa2da798b9a49a65ae3d58ae71 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt -9143548adf883d934ee86d5d4dd140a9a2d8116c6c8a905a41d2bf7d828bc371 tokens.css +252ce7eb35cc07ef2661cba5cec6c2046e0207c17584a5d83ffe038a365e7cdb generate_tokens.py +a066eaaa68e01e6235ffcd525818f0cb20b44e0aed1687cb645e528d8947aa5d shell-tokens.json +d7cc75e1643ebf79204fd5df94e03ad182f7451761d48ca54da140616dd957bc src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +bb99f1f3b51028bbe87b245b4a78a59e55d4b519bc947afe56e5db11406db092 tokens.css 5b939f1f3eb90b428008fda75720f6e3a4e844c486668931db3d52bffb987b53 tokens.json