From 8f4ea8616cf3f0e231bb9a8e8b054ec471a5379d Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 14 May 2026 19:48:43 +0900 Subject: [PATCH 1/6] feat(demo): Implement collapsible top bar and enhance state persistence - Hoist scroll state to manage dynamic top bar animations. - Refactor TopBar into modular components (FilterRow, FilterChip). - Use rememberSaveable for UI state persistence across configuration changes. --- .../io/github/komodgn/example/Application.kt | 16 +-- .../component/DemoComponentFilterRow.kt | 49 +++++++++ .../example/component/DemoFilterChip.kt | 48 +++++++++ .../komodgn/example/component/DemoTopBar.kt | 102 ++++++++++-------- 4 files changed, 167 insertions(+), 48 deletions(-) create mode 100644 example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoComponentFilterRow.kt create mode 100644 example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoFilterChip.kt diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/Application.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/Application.kt index 8f0cbb5..48da641 100644 --- a/example/src/commonMain/kotlin/io/github/komodgn/example/Application.kt +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/Application.kt @@ -15,15 +15,18 @@ */ package io.github.komodgn.example +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding @@ -34,7 +37,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -50,10 +52,11 @@ import io.github.komodgn.example.util.getInitialCode @Composable fun App() { + val scrollState = rememberScrollState() var isDark by rememberSaveable { mutableStateOf(true) } var selectedComponent by rememberSaveable { mutableStateOf(DemoComponent.CODE_VIEW) } - var currentLang by remember { mutableStateOf(CodeLanguage.KOTLIN) } - var userInput by remember { mutableStateOf(getInitialCode(currentLang, AppConfig.LIBRARY_VERSION)) } + var currentLang by rememberSaveable { mutableStateOf(CodeLanguage.KOTLIN) } + var userInput by rememberSaveable { mutableStateOf(getInitialCode(currentLang, AppConfig.LIBRARY_VERSION)) } CodeViewTheme(isDarkTheme = isDark) { Scaffold( @@ -62,6 +65,7 @@ fun App() { contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { DemoTopBar( + scrollState = scrollState, isDark = isDark, onToggleTheme = { isDark = !isDark }, selectedComponent = selectedComponent, @@ -74,10 +78,11 @@ fun App() { .fillMaxSize() .padding(innerPadding) .background(MaterialTheme.colorScheme.background) - .windowInsetsPadding(WindowInsets.safeDrawing) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)) .imePadding(), ) { MainContent( + scrollState = scrollState, isCompact = maxWidth < 600.dp, selectedDemoComponent = selectedComponent, code = userInput, @@ -95,6 +100,7 @@ fun App() { @Composable private fun MainContent( + scrollState: ScrollState, isCompact: Boolean, selectedDemoComponent: DemoComponent, code: String, @@ -102,8 +108,6 @@ private fun MainContent( onCodeChange: (String) -> Unit, onLanguageChange: (CodeLanguage) -> Unit, ) { - val scrollState = rememberScrollState() - Column( modifier = Modifier .fillMaxWidth() diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoComponentFilterRow.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoComponentFilterRow.kt new file mode 100644 index 0000000..5df8536 --- /dev/null +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoComponentFilterRow.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2026 komodgn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.komodgn.example.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.komodgn.example.DemoComponent + +@Composable +fun DemoComponentFilterRow( + selectedComponent: DemoComponent, + onComponentSelect: (DemoComponent) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DemoComponent.entries.forEach { component -> + val isSelected = selectedComponent == component + DemoFilterChip( + label = when (component) { + DemoComponent.CODE_VIEW -> "CodeView" + DemoComponent.CODE_EDITOR -> "CodeEditor" + }, + isSelected = isSelected, + onClick = { onComponentSelect(component) }, + ) + } + } +} diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoFilterChip.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoFilterChip.kt new file mode 100644 index 0000000..5c600d6 --- /dev/null +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoFilterChip.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2026 komodgn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.komodgn.example.component + +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun DemoFilterChip( + label: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + FilterChip( + selected = isSelected, + onClick = onClick, + label = { Text(text = label) }, + modifier = modifier, + colors = FilterChipDefaults.filterChipColors( + labelColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f), + selectedLabelColor = MaterialTheme.colorScheme.primary, + selectedContainerColor = MaterialTheme.colorScheme.onPrimary, + ), + border = FilterChipDefaults.filterChipBorder( + enabled = true, + selected = isSelected, + borderColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f), + ), + ) +} diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt index 10f2ce0..9c8dd3a 100644 --- a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt @@ -15,18 +15,21 @@ */ package io.github.komodgn.example.component +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.LightMode -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -34,37 +37,67 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp import io.github.komodgn.example.DemoComponent +@Suppress("FrequentlyChangingValue") @Composable fun DemoTopBar( + scrollState: ScrollState, isDark: Boolean, onToggleTheme: () -> Unit, selectedComponent: DemoComponent, onComponentSelect: (DemoComponent) -> Unit, ) { + val scrollThreshold = 120f + val collapseFraction = (scrollState.value / scrollThreshold).coerceIn(0f, 1f) + Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.primary) - .statusBarsPadding(), + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)) + .padding(bottom = lerp(8.dp, 0.dp, collapseFraction)), ) { - Row( + Box( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 0.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + .height(lerp(50.dp, 48.dp, collapseFraction)) + .padding(horizontal = 16.dp), + contentAlignment = Alignment.Center, ) { Text( text = "Compose CodeView Demo", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.padding(8.dp), + modifier = Modifier + .align(Alignment.CenterStart) + .graphicsLayer { + alpha = 1f - collapseFraction + translationY = -20f * collapseFraction + }, + style = MaterialTheme.typography.titleMedium, + ) + + Text( + text = if (selectedComponent == DemoComponent.CODE_VIEW) " CodeView" else "CodeEditor", + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .graphicsLayer { + alpha = collapseFraction + translationY = 20f * (1f - collapseFraction) + }, + style = MaterialTheme.typography.bodyMedium, ) - IconButton(onClick = onToggleTheme) { + + IconButton( + onClick = onToggleTheme, + modifier = Modifier.align(Alignment.CenterEnd), + ) { Icon( imageVector = if (isDark) Icons.Default.LightMode else Icons.Default.DarkMode, contentDescription = "Toggle Theme", @@ -73,37 +106,22 @@ fun DemoTopBar( } } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 8.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - DemoComponent.entries.forEach { component -> - val isSelected = selectedComponent == component - FilterChip( - selected = isSelected, - onClick = { onComponentSelect(component) }, - label = { - Text( - text = when (component) { - DemoComponent.CODE_VIEW -> "CodeView" - DemoComponent.CODE_EDITOR -> "CodeEditor" - }, - ) + val chipAreaHeight = lerp(48.dp, 0.dp, collapseFraction) + + if (collapseFraction < 0.9f) { + DemoComponentFilterRow( + selectedComponent = selectedComponent, + onComponentSelect = onComponentSelect, + modifier = Modifier + .fillMaxWidth() + .height(chipAreaHeight) + .padding(horizontal = 16.dp) + .graphicsLayer { + alpha = (1f - collapseFraction * 2f).coerceIn(0f, 1f) + scaleY = 1f - collapseFraction + translationY = -10f * collapseFraction }, - colors = FilterChipDefaults.filterChipColors( - labelColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f), - selectedLabelColor = MaterialTheme.colorScheme.primary, - selectedContainerColor = MaterialTheme.colorScheme.onPrimary, - ), - border = FilterChipDefaults.filterChipBorder( - enabled = true, - selected = isSelected, - borderColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f), - ), - ) - } + ) } } } From 7061fb4fc9b8257411e2fc143ca635ae5e58027a Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 14 May 2026 19:50:27 +0900 Subject: [PATCH 2/6] ci: Rename workflows file --- .github/workflows/{deploy.yml => docs.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{deploy.yml => docs.yml} (100%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/docs.yml similarity index 100% rename from .github/workflows/deploy.yml rename to .github/workflows/docs.yml From 63904bef8e89fafc09f6764e13f7ed395eaf1679 Mon Sep 17 00:00:00 2001 From: HyunJin Choi Date: Thu, 14 May 2026 19:55:55 +0900 Subject: [PATCH 3/6] Update example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../kotlin/io/github/komodgn/example/component/DemoTopBar.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt index 9c8dd3a..3e1cc24 100644 --- a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt @@ -83,7 +83,9 @@ fun DemoTopBar( ) Text( - text = if (selectedComponent == DemoComponent.CODE_VIEW) " CodeView" else "CodeEditor", + Text( + text = if (selectedComponent == DemoComponent.CODE_VIEW) "CodeView" else "CodeEditor", + fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier From 3b209fd0ebc8fbf9013c898362c9923bf7052906 Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 14 May 2026 19:59:24 +0900 Subject: [PATCH 4/6] fix(demo): Remove duplicated Text block --- .../kotlin/io/github/komodgn/example/component/DemoTopBar.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt index 3e1cc24..36b013b 100644 --- a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt @@ -82,7 +82,6 @@ fun DemoTopBar( style = MaterialTheme.typography.titleMedium, ) - Text( Text( text = if (selectedComponent == DemoComponent.CODE_VIEW) "CodeView" else "CodeEditor", fontWeight = FontWeight.Bold, From 30db0e904cfeacf28555855edcbc2cc2703dedd5 Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 14 May 2026 20:03:28 +0900 Subject: [PATCH 5/6] fix(demo): Remove redundant code --- .../kotlin/io/github/komodgn/example/component/DemoTopBar.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt index 36b013b..f7d4473 100644 --- a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt @@ -85,7 +85,6 @@ fun DemoTopBar( Text( text = if (selectedComponent == DemoComponent.CODE_VIEW) "CodeView" else "CodeEditor", fontWeight = FontWeight.Bold, - fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier .graphicsLayer { From 10254b5ba7c17934abee5f735335d2de3f3ea04e Mon Sep 17 00:00:00 2001 From: komodgn Date: Thu, 14 May 2026 20:45:07 +0900 Subject: [PATCH 6/6] refactor(demo): Apply code review - Optimize top bar performance using derivedStateOf and deferred reading. --- .../komodgn/example/component/DemoTopBar.kt | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt index f7d4473..43f2df9 100644 --- a/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt +++ b/example/src/commonMain/kotlin/io/github/komodgn/example/component/DemoTopBar.kt @@ -35,15 +35,16 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.lerp import io.github.komodgn.example.DemoComponent -@Suppress("FrequentlyChangingValue") @Composable fun DemoTopBar( scrollState: ScrollState, @@ -53,19 +54,23 @@ fun DemoTopBar( onComponentSelect: (DemoComponent) -> Unit, ) { val scrollThreshold = 120f - val collapseFraction = (scrollState.value / scrollThreshold).coerceIn(0f, 1f) + val getCollapseFraction = { (scrollState.value / scrollThreshold).coerceIn(0f, 1f) } + + val isFilterVisible by remember { + derivedStateOf { (scrollState.value / scrollThreshold) < 0.9f } + } Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.primary) .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)) - .padding(bottom = lerp(8.dp, 0.dp, collapseFraction)), + .padding(bottom = 8.dp), ) { Box( modifier = Modifier .fillMaxWidth() - .height(lerp(50.dp, 48.dp, collapseFraction)) + .height(40.dp) .padding(horizontal = 16.dp), contentAlignment = Alignment.Center, ) { @@ -76,8 +81,9 @@ fun DemoTopBar( modifier = Modifier .align(Alignment.CenterStart) .graphicsLayer { - alpha = 1f - collapseFraction - translationY = -20f * collapseFraction + val fraction = getCollapseFraction() + alpha = 1f - fraction + translationY = -20f * fraction }, style = MaterialTheme.typography.titleMedium, ) @@ -88,8 +94,9 @@ fun DemoTopBar( color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier .graphicsLayer { - alpha = collapseFraction - translationY = 20f * (1f - collapseFraction) + val fraction = getCollapseFraction() + alpha = fraction + translationY = 20f * (1f - fraction) }, style = MaterialTheme.typography.bodyMedium, ) @@ -106,20 +113,19 @@ fun DemoTopBar( } } - val chipAreaHeight = lerp(48.dp, 0.dp, collapseFraction) - - if (collapseFraction < 0.9f) { + if (isFilterVisible) { DemoComponentFilterRow( selectedComponent = selectedComponent, onComponentSelect = onComponentSelect, modifier = Modifier .fillMaxWidth() - .height(chipAreaHeight) + .height(48.dp) .padding(horizontal = 16.dp) .graphicsLayer { - alpha = (1f - collapseFraction * 2f).coerceIn(0f, 1f) - scaleY = 1f - collapseFraction - translationY = -10f * collapseFraction + val fraction = getCollapseFraction() + alpha = (1f - fraction * 2f).coerceIn(0f, 1f) + scaleY = 1f - fraction + translationY = -10f * fraction }, ) }