├── .github ├── CODEOWNERS ├── screenshots │ ├── main-dark.png │ ├── main-light.png │ ├── change-color.png │ ├── change-color-pink.png │ └── change-color-updated-main-page.png ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── verification.yml ├── gradle.properties ├── src ├── main │ ├── resources │ │ ├── icon.icns │ │ ├── icon.png │ │ ├── ic_arrow_back.xml │ │ └── ic_android.xml │ └── kotlin │ │ ├── m3components │ │ └── components │ │ │ ├── Text.kt │ │ │ ├── M3Divider.kt │ │ │ ├── ProgressBar.kt │ │ │ ├── Slider.kt │ │ │ ├── Tab.kt │ │ │ ├── Switch.kt │ │ │ ├── Checkbox.kt │ │ │ ├── Chips.kt │ │ │ ├── Cards.kt │ │ │ ├── NavigationBar.kt │ │ │ ├── TextFields.kt │ │ │ ├── TopAppBar.kt │ │ │ └── Buttons.kt │ │ ├── TestTags.kt │ │ ├── FileUtils.kt │ │ ├── ThemeColorPicker.kt │ │ ├── Main.kt │ │ └── ComponentScope.kt └── test │ └── kotlin │ ├── FileUtilsKtTest.kt │ └── m3components │ └── components │ ├── SaveFileDialogTest.kt │ ├── SliderVisibilityTest.kt │ ├── DividerVisibilityTest.kt │ ├── ProgressBarVisibilityTest.kt │ ├── ChipsVisibilityTest.kt │ ├── SwitchVisibilityTest.kt │ ├── CheckboxVisibilityTest.kt │ ├── TextFieldVisibilityTest.kt │ ├── CardVisibilityTest.kt │ ├── TopAppBarVisibilityTest.kt │ ├── ButtonVisibilityTest.kt │ └── NavRailButtonTest.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── renovate.json ├── settings.gradle.kts ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @oas004 @fgiris 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /src/main/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/src/main/resources/icon.icns -------------------------------------------------------------------------------- /src/main/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/src/main/resources/icon.png -------------------------------------------------------------------------------- /.github/screenshots/main-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/main-dark.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/screenshots/main-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/main-light.png -------------------------------------------------------------------------------- /.github/screenshots/change-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/change-color.png -------------------------------------------------------------------------------- /.github/screenshots/change-color-pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/change-color-pink.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/screenshots/change-color-updated-main-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/change-color-updated-main-page.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 5 | } 6 | } 7 | rootProject.name = "Material3Playground" 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/resources/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Text.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.text.TextStyle 7 | 8 | @Composable 9 | fun M3OnSurfaceText( 10 | text: String, 11 | style: TextStyle, 12 | ) { 13 | Text( 14 | color = MaterialTheme.colorScheme.onSurface, 15 | text = text, 16 | style = style, 17 | ) 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/M3Divider.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.foundation.lazy.grid.LazyGridScope 4 | import androidx.compose.material3.Divider 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.platform.testTag 7 | import androidx.compose.ui.unit.dp 8 | 9 | fun LazyGridScope.m3Divider() { 10 | item { 11 | Divider( 12 | modifier = Modifier.testTag(TestTags.Divider.regular), 13 | thickness = 0.5.dp 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/kotlin/FileUtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.graphics.Color 2 | import org.junit.Assert.* 3 | import org.junit.Test 4 | 5 | class FileUtilsKtTest { 6 | @Test 7 | fun testGetColorNameFunction() { 8 | val colorNameLowerCase = "red" 9 | val color = Color.Red 10 | 11 | val result = getColorKotlinProperty(colorNameLowerCase, color) 12 | 13 | // Assert 14 | val expected = "val Red: Color = Color(0xFFFF0000) \n" 15 | assertEquals(expected, result) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | /.idea/markdown-navigator-enh.xml 12 | /.idea/markdown-navigator.xml 13 | /.idea/shelf/ 14 | .DS_Store 15 | */build 16 | /captures 17 | .externalNativeBuild 18 | .cxx 19 | 20 | /src/build 21 | 22 | **/.idea/** 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Build folder 28 | **/build/** 29 | 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Pull requests are welcome! We'd love help improving this library. Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it. 2 | 3 | Steps to start contributing are 4 | 5 | 1. Fork project. 6 | 2. Clone your fork. 7 | 3. Find issues that needs to be solved/make an issue and discuss solutions. 8 | 4. Make a pull request to your forked project. 9 | 5. Sync PR to this project. 10 | 11 | If you have any questions about this process. Please do reach out! :) Have fun! 12 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/ProgressBar.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.lazy.grid.LazyGridScope 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.platform.testTag 8 | 9 | fun LazyGridScope.m3ProgressBar() { 10 | item { 11 | Box { 12 | CircularProgressIndicator( 13 | modifier = Modifier.testTag(TestTags.ProgressBar.circular) 14 | ) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/SaveFileDialogTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import TestTags 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import org.junit.Rule 8 | import org.junit.Test 9 | 10 | class SaveFileDialogTest { 11 | 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testAlertDialogIsNotOpenAtDefault() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.FileSaverDialog.dialog).assertDoesNotExist() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Slider.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.foundation.lazy.grid.LazyGridScope 4 | import androidx.compose.material3.Slider 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.platform.testTag 11 | 12 | fun LazyGridScope.m3Slider() { 13 | item { 14 | var sliderPosition by remember { mutableStateOf(0f) } 15 | 16 | Slider( 17 | modifier = Modifier.testTag(TestTags.Slider.regular), 18 | value = sliderPosition, 19 | onValueChange = { sliderPosition = it } 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/SliderVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import androidx.compose.ui.test.performClick 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class SliderVisibilityTest { 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testSliderIsPresent() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.NavRailButtons.sliders).performClick() 23 | waitForIdle() 24 | onNodeWithTag(TestTags.Slider.regular, false).assertIsDisplayed() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/DividerVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import androidx.compose.ui.test.performClick 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class DividerVisibilityTest { 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testDividerIsPresent() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.NavRailButtons.dividers).performClick() 23 | waitForIdle() 24 | onNodeWithTag(TestTags.Divider.regular, false).assertIsDisplayed() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/ProgressBarVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import androidx.compose.ui.test.performClick 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class ProgressBarVisibilityTest { 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testProgressBarIsPresent() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.NavRailButtons.progressBar).performClick() 23 | waitForIdle() 24 | onNodeWithTag(TestTags.ProgressBar.circular, false).assertIsDisplayed() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/resources/ic_android.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/ChipsVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import androidx.compose.ui.test.assertCountEquals 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onAllNodesWithTag 7 | import androidx.compose.ui.test.onNodeWithTag 8 | import androidx.compose.ui.test.performClick 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | class ChipsVisibilityTest { 13 | @get:Rule 14 | val rule = createComposeRule() 15 | 16 | @Test 17 | fun testChipIsPresent() { 18 | rule.apply { 19 | setContent { 20 | Material3Playground(false) 21 | } 22 | waitForIdle() 23 | onNodeWithTag(TestTags.NavRailButtons.chips).performClick() 24 | waitForIdle() 25 | onAllNodesWithTag(TestTags.CheckBox.regular, false).assertCountEquals(3) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/SwitchVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import androidx.compose.ui.test.assertCountEquals 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onAllNodesWithTag 7 | import androidx.compose.ui.test.onNodeWithTag 8 | import androidx.compose.ui.test.performClick 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | class SwitchVisibilityTest { 13 | @get:Rule 14 | val rule = createComposeRule() 15 | 16 | @Test 17 | fun testSwitchIsPresent() { 18 | rule.apply { 19 | setContent { 20 | Material3Playground(false) 21 | } 22 | waitForIdle() 23 | onNodeWithTag(TestTags.NavRailButtons.switch).performClick() 24 | waitForIdle() 25 | onAllNodesWithTag(TestTags.Switch.regular, false).assertCountEquals(2) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Tab.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.foundation.lazy.grid.LazyGridScope 4 | import androidx.compose.material3.Tab 5 | import androidx.compose.material3.TabRow 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | 12 | fun LazyGridScope.m3Tab() { 13 | item { 14 | var selectedIndex by remember { mutableStateOf(0) } 15 | val tabs = listOf("Accounts", "Cards", "Funds") 16 | 17 | TabRow(selectedTabIndex = selectedIndex) { 18 | tabs.forEachIndexed { index, title -> 19 | Tab( 20 | text = { Text(title) }, 21 | selected = index == selectedIndex, 22 | onClick = { selectedIndex = index } 23 | ) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/CheckboxVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import TestTags 5 | import androidx.compose.ui.test.assertCountEquals 6 | import androidx.compose.ui.test.junit4.createComposeRule 7 | import androidx.compose.ui.test.onAllNodesWithTag 8 | import androidx.compose.ui.test.onNodeWithTag 9 | import androidx.compose.ui.test.performClick 10 | import org.junit.Rule 11 | import org.junit.Test 12 | 13 | class CheckboxVisibilityTest { 14 | @get:Rule 15 | val rule = createComposeRule() 16 | 17 | @Test 18 | fun testCheckboxIsPresent() { 19 | rule.apply { 20 | setContent { 21 | Material3Playground(false) 22 | } 23 | waitForIdle() 24 | onNodeWithTag(TestTags.NavRailButtons.checkBox).performClick() 25 | waitForIdle() 26 | onAllNodesWithTag(TestTags.CheckBox.regular, false).assertCountEquals(2) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Odin Asbjørnsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Switch.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.lazy.grid.LazyGridScope 7 | import androidx.compose.material3.Switch 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.testTag 14 | import androidx.compose.ui.unit.dp 15 | 16 | fun LazyGridScope.m3Switch() { 17 | item { 18 | Row { 19 | var checked by remember { mutableStateOf(false) } 20 | 21 | Switch( 22 | modifier = Modifier.testTag(TestTags.Switch.regular), 23 | checked = checked, 24 | onCheckedChange = { checked = !checked } 25 | ) 26 | Spacer(modifier = Modifier.size(16.dp)) 27 | Switch( 28 | modifier = Modifier.testTag(TestTags.Switch.regular), 29 | checked = !checked, 30 | onCheckedChange = { checked = !checked } 31 | ) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/verification.yml: -------------------------------------------------------------------------------- 1 | name: Build & test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | unitTest: 14 | name: Run Unit Tests 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v5 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v5 21 | with: 22 | distribution: 'zulu' 23 | java-version: 17 24 | - name: Make gradlew executable 25 | run: chmod +x ./gradlew 26 | - name: Run unit tests 27 | run: ./gradlew test 28 | - name: Upload test report 29 | uses: actions/upload-artifact@v5 30 | with: 31 | name: test report 32 | path: build/reports/tests/test/index.html 33 | retention-days: 1 34 | assemble: 35 | name: Assemble Project 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v5 40 | - name: Set up JDK 17 41 | uses: actions/setup-java@v5 42 | with: 43 | distribution: 'zulu' 44 | java-version: 17 45 | - name: Make gradlew executable 46 | run: chmod +x ./gradlew 47 | - name: Run unit tests 48 | run: ./gradlew assemble 49 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Checkbox.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import TestTags 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.lazy.grid.LazyGridScope 8 | import androidx.compose.material3.Checkbox 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.testTag 16 | import androidx.compose.ui.unit.dp 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | fun LazyGridScope.m3Checkbox() { 20 | item { 21 | var checked by remember { mutableStateOf(true) } 22 | Row { 23 | Checkbox( 24 | modifier = Modifier.testTag(TestTags.CheckBox.regular), 25 | checked = checked, 26 | onCheckedChange = { checked = it } 27 | ) 28 | 29 | Spacer(Modifier.size(8.dp)) 30 | 31 | Checkbox( 32 | modifier = Modifier.testTag(TestTags.CheckBox.regular), 33 | checked = !checked, 34 | onCheckedChange = { checked = !it } 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/TextFieldVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import androidx.compose.ui.test.performClick 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class TextFieldVisibilityTest { 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testRegularTextFieldIsPresent() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.NavRailButtons.textFields).performClick() 23 | waitForIdle() 24 | onNodeWithTag(TestTags.TextField.regular, false).assertIsDisplayed() 25 | } 26 | } 27 | 28 | @Test 29 | fun testErrorTextFieldIsPresent() { 30 | rule.apply { 31 | setContent { 32 | Material3Playground(false) 33 | } 34 | waitForIdle() 35 | onNodeWithTag(TestTags.NavRailButtons.textFields).performClick() 36 | waitForIdle() 37 | onNodeWithTag(TestTags.TextField.error, false).assertIsDisplayed() 38 | onNodeWithTag(TestTags.TextField.errorLabel, false).assertIsDisplayed() 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/CardVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import TestTags 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import androidx.compose.ui.test.performClick 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class CardVisibilityTest { 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testOutlinedCardIsPresent() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.NavRailButtons.cards).performClick() 23 | waitForIdle() 24 | onNodeWithTag(TestTags.Cards.outlined, false).assertExists() 25 | } 26 | } 27 | 28 | 29 | @Test 30 | fun testElevatedCardIsPresent() { 31 | rule.apply { 32 | setContent { 33 | Material3Playground(false) 34 | } 35 | waitForIdle() 36 | onNodeWithTag(TestTags.NavRailButtons.cards).performClick() 37 | waitForIdle() 38 | onNodeWithTag(TestTags.Cards.elevated, false).assertExists() 39 | } 40 | } 41 | 42 | @Test 43 | fun testFilledCardIsPresent() { 44 | rule.apply { 45 | setContent { 46 | Material3Playground(false) 47 | } 48 | waitForIdle() 49 | onNodeWithTag(TestTags.NavRailButtons.cards).performClick() 50 | waitForIdle() 51 | onNodeWithTag(TestTags.Cards.filled, false).assertExists() 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Chips.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.lazy.grid.LazyGridScope 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.FilterChip 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.testTag 17 | import androidx.compose.ui.unit.dp 18 | 19 | @OptIn(ExperimentalMaterial3Api::class) 20 | fun LazyGridScope.m3Chips() { 21 | item { 22 | Row(Modifier.padding(8.dp)) { 23 | var selectedId by remember { mutableStateOf(1) } 24 | 25 | FilterChip( 26 | modifier = Modifier.testTag(TestTags.Chips.regular), 27 | label = { Text("Android") }, 28 | onClick = { selectedId = 1 }, 29 | selected = selectedId == 1 30 | ) 31 | 32 | Spacer(modifier = Modifier.size(4.dp)) 33 | 34 | FilterChip( 35 | modifier = Modifier.testTag(TestTags.Chips.regular), 36 | label = { Text("Material") }, 37 | onClick = { selectedId = 2 }, 38 | selected = selectedId == 2 39 | ) 40 | 41 | Spacer(modifier = Modifier.size(4.dp)) 42 | 43 | FilterChip( 44 | modifier = Modifier.testTag(TestTags.Chips.regular), 45 | label = { Text("Compose") }, 46 | onClick = { selectedId = 3 }, 47 | selected = selectedId == 3 48 | ) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Cards.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import TestTags 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.lazy.grid.LazyGridScope 9 | import androidx.compose.material3.Card 10 | import androidx.compose.material3.ElevatedCard 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.OutlinedCard 13 | import androidx.compose.material3.Text 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.testTag 17 | import androidx.compose.ui.unit.dp 18 | 19 | @OptIn(ExperimentalMaterial3Api::class) 20 | fun LazyGridScope.m3Cards() { 21 | item { 22 | ElevatedCard( 23 | onClick = {}, 24 | modifier = Modifier.size(width = 180.dp, height = 100.dp).padding(8.dp).testTag(TestTags.Cards.elevated) 25 | ) { 26 | Box(Modifier.fillMaxSize()) { 27 | Text("Elevated card", Modifier.align(Alignment.Center)) 28 | } 29 | } 30 | } 31 | 32 | item { 33 | OutlinedCard( 34 | onClick = {}, 35 | modifier = Modifier.size(width = 180.dp, height = 100.dp).padding(8.dp).testTag(TestTags.Cards.outlined) 36 | ) { 37 | Box(Modifier.fillMaxSize()) { 38 | Text("Outlined card", Modifier.align(Alignment.Center)) 39 | } 40 | } 41 | } 42 | 43 | 44 | item { 45 | Card( 46 | onClick = {}, 47 | modifier = Modifier.size(width = 180.dp, height = 100.dp).padding(8.dp).testTag(TestTags.Cards.filled) 48 | ) { 49 | Box(Modifier.fillMaxSize()) { 50 | Text("Filled card", Modifier.align(Alignment.Center)) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/NavigationBar.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.width 5 | import androidx.compose.foundation.lazy.grid.LazyGridScope 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.Favorite 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.NavigationBar 10 | import androidx.compose.material3.NavigationBarItem 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.unit.dp 18 | 19 | fun LazyGridScope.m3NavigationBars() { 20 | 21 | item { 22 | val navbarItems = listOf("Home", "Payments", "Spending") 23 | var selectedItem by remember { mutableStateOf(0) } 24 | 25 | NavigationBar(Modifier.width(600.dp).padding(8.dp)) { 26 | navbarItems.forEachIndexed { index, title -> 27 | NavigationBarItem( 28 | icon = { Icon(Icons.Filled.Favorite, contentDescription = null) }, 29 | label = { Text(title) }, 30 | selected = selectedItem == index, 31 | onClick = { selectedItem = index } 32 | ) 33 | } 34 | } 35 | } 36 | 37 | item { 38 | val navbarItems = listOf("Home", "Payments", "Spending") 39 | var selectedItem by remember { mutableStateOf(0) } 40 | 41 | NavigationBar(Modifier.width(600.dp).padding(8.dp)) { 42 | navbarItems.forEachIndexed { index, title -> 43 | NavigationBarItem( 44 | icon = { Icon(Icons.Filled.Favorite, contentDescription = null) }, 45 | label = { Text(title) }, 46 | selected = selectedItem == index, 47 | onClick = { selectedItem = index }, 48 | alwaysShowLabel = false, 49 | ) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/TestTags.kt: -------------------------------------------------------------------------------- 1 | object TestTags { 2 | object FileSaverDialog { 3 | const val dialog = "FileSaverDialog" 4 | } 5 | 6 | object Components { 7 | const val navRail = "NavRail" 8 | } 9 | 10 | object Buttons { 11 | const val regular = "RegularButton" 12 | const val outlined = "OutlinedButton" 13 | const val text = "TextButton" 14 | const val floating = "FloatingActionButton" 15 | const val extendedFloating = "ExtendedFloatingActionButton" 16 | const val icon = "IconButton" 17 | const val filledIcon = "FilledIconButton" 18 | const val iconToggle = "IconToggleButton" 19 | } 20 | 21 | object Cards { 22 | const val elevated = "ElevatedCard" 23 | const val outlined = "OutlinedCard" 24 | const val filled = "FilledCard" 25 | } 26 | 27 | object CheckBox { 28 | const val regular = "Regular" 29 | } 30 | 31 | object Chips { 32 | const val regular = "Regular" 33 | } 34 | 35 | object Divider { 36 | const val regular = "Regular" 37 | } 38 | 39 | object ProgressBar { 40 | const val circular = "CircularProgressBar" 41 | } 42 | 43 | object Slider { 44 | const val regular = "Regular" 45 | } 46 | 47 | object Switch { 48 | const val regular = "Regular" 49 | } 50 | 51 | object TextField { 52 | const val regular = "RegularTextField" 53 | const val error = "ErrorTextField" 54 | const val errorLabel = "ErrorLabel" 55 | } 56 | 57 | object TopAppBar { 58 | const val small = "SmallTopAppBar" 59 | const val navigationIconSmall = "NavigationIconSmallTopAppBar" 60 | const val centerAligned = "CenterAlignedTopAppBar" 61 | const val navigationIconCenterAligned = "NavigationIconCenterAlignedTopAppBar" 62 | const val smallScroll = "SmallScrollTopAppBar" 63 | const val navigationIconsmallScroll = "NavigationIconSmallScrollTopAppBar" 64 | const val centerAlignedScroll = "CenterAlignedScrollTopAppBar" 65 | const val navigationIconCenterAlignedScroll = "NavigationIconCenterAlignedScrollTopAppBar" 66 | } 67 | 68 | object NavRailButtons { 69 | const val buttons = "Buttons" 70 | const val appBars = "AppBars" 71 | const val cards = "Cards" 72 | const val textFields = "TextFields" 73 | const val chips = "Chips" 74 | const val switch = "Switch" 75 | const val checkBox = "CheckBox" 76 | const val sliders = "Sliders" 77 | const val progressBar = "ProgressBar" 78 | const val dividers = "Dividers" 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/TextFields.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import TestTags 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.calculateStartPadding 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.grid.LazyGridScope 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.OutlinedTextField 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TextFieldDefaults 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalLayoutDirection 18 | import androidx.compose.ui.platform.testTag 19 | import androidx.compose.ui.unit.dp 20 | 21 | fun LazyGridScope.m3TextFields() { 22 | 23 | item { 24 | 25 | val text = remember { 26 | mutableStateOf("") 27 | } 28 | 29 | OutlinedTextField( 30 | modifier = Modifier.padding(8.dp).testTag(TestTags.TextField.regular), 31 | value = text.value, 32 | onValueChange = { 33 | text.value = it 34 | }, 35 | placeholder = { 36 | Text("Please type a text") 37 | } 38 | ) 39 | } 40 | 41 | item { 42 | 43 | Column( 44 | modifier = Modifier.padding(8.dp), 45 | ) { 46 | val text = remember { 47 | mutableStateOf("") 48 | } 49 | 50 | OutlinedTextField( 51 | modifier = Modifier.testTag(TestTags.TextField.error), 52 | value = text.value, 53 | onValueChange = { 54 | text.value = it 55 | }, 56 | placeholder = { 57 | Text("Please type a text") 58 | }, 59 | isError = true, 60 | ) 61 | TextFieldErrorMessage( 62 | modifier = Modifier.testTag(TestTags.TextField.errorLabel), 63 | message = "Something went wrong" 64 | ) 65 | } 66 | } 67 | } 68 | 69 | @OptIn(ExperimentalMaterial3Api::class) 70 | @Composable 71 | private fun TextFieldErrorMessage(modifier: Modifier = Modifier, message: String) { 72 | // This will ensure that the error message will be aligned with the input text. Since we do 73 | // not have a built-in label supported for our text fields, using textFieldWithoutLabelPadding 74 | // is just fine here. 75 | val startPadding = TextFieldDefaults.textFieldWithoutLabelPadding() 76 | .calculateStartPadding(layoutDirection = LocalLayoutDirection.current) 77 | 78 | Text( 79 | modifier = modifier 80 | .padding( 81 | top = 4.dp, 82 | start = startPadding 83 | ), 84 | text = message, 85 | style = MaterialTheme.typography.labelSmall, 86 | color = MaterialTheme.colorScheme.error, 87 | ) 88 | } -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/TopAppBarVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import TestTags 5 | import androidx.compose.ui.test.SemanticsNodeInteractionsProvider 6 | import androidx.compose.ui.test.assertIsDisplayed 7 | import androidx.compose.ui.test.junit4.createComposeRule 8 | import androidx.compose.ui.test.onNodeWithTag 9 | import androidx.compose.ui.test.performClick 10 | import org.junit.Rule 11 | import org.junit.Test 12 | 13 | class TopAppBarVisibilityTest { 14 | @get:Rule 15 | val rule = createComposeRule() 16 | 17 | @Test 18 | fun testSmallAppBarIsPresent() { 19 | rule.apply { 20 | setContent { 21 | Material3Playground(false) 22 | } 23 | waitForIdle() 24 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick() 25 | waitForIdle() 26 | assertTopAppBarDisplayed( 27 | navigationIconTestTag = TestTags.TopAppBar.navigationIconSmall, 28 | appBarTestTag = TestTags.TopAppBar.small 29 | ) 30 | } 31 | } 32 | 33 | @Test 34 | fun testCenterAlignedAppBarIsPresent() { 35 | rule.apply { 36 | setContent { 37 | Material3Playground(false) 38 | } 39 | waitForIdle() 40 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick() 41 | waitForIdle() 42 | assertTopAppBarDisplayed( 43 | navigationIconTestTag = TestTags.TopAppBar.navigationIconCenterAligned, 44 | appBarTestTag = TestTags.TopAppBar.centerAligned 45 | ) 46 | } 47 | } 48 | 49 | @Test 50 | fun testSmallAppBarWithScrollStateIsPresent() { 51 | rule.apply { 52 | setContent { 53 | Material3Playground(false) 54 | } 55 | waitForIdle() 56 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick() 57 | waitForIdle() 58 | assertTopAppBarDisplayed( 59 | navigationIconTestTag = TestTags.TopAppBar.navigationIconsmallScroll, 60 | appBarTestTag = TestTags.TopAppBar.smallScroll 61 | ) 62 | } 63 | } 64 | 65 | @Test 66 | fun testCenterAlignedAppBarWithScrollStateIsPresent() { 67 | rule.apply { 68 | setContent { 69 | Material3Playground(false) 70 | } 71 | waitForIdle() 72 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick() 73 | waitForIdle() 74 | assertTopAppBarDisplayed( 75 | navigationIconTestTag = TestTags.TopAppBar.navigationIconCenterAlignedScroll, 76 | appBarTestTag = TestTags.TopAppBar.centerAlignedScroll 77 | ) 78 | } 79 | } 80 | } 81 | 82 | private fun SemanticsNodeInteractionsProvider.assertTopAppBarDisplayed( 83 | navigationIconTestTag: String, 84 | appBarTestTag: String 85 | ) { 86 | onNodeWithTag(navigationIconTestTag, false).assertIsDisplayed() 87 | onNodeWithTag(appBarTestTag, false).assertIsDisplayed() 88 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/TopAppBar.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import TestTags 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.foundation.lazy.grid.LazyGridScope 7 | import androidx.compose.material3.CenterAlignedTopAppBar 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.IconButton 11 | import androidx.compose.material3.SmallTopAppBar 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TopAppBarDefaults 14 | import androidx.compose.material3.TopAppBarScrollState 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.testTag 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.unit.dp 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | fun LazyGridScope.m3TopAppBars() { 23 | item { 24 | SmallTopAppBar( 25 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.small), 26 | navigationIcon = { 27 | NavigationIcon( 28 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconSmall) 29 | ) 30 | }, 31 | title = { Text("Small top app bar") }, 32 | ) 33 | } 34 | item { 35 | CenterAlignedTopAppBar( 36 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.centerAligned), 37 | navigationIcon = { 38 | NavigationIcon( 39 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconCenterAligned) 40 | ) 41 | }, 42 | title = { Text("Center aligned top app bar") }, 43 | ) 44 | } 45 | item { 46 | 47 | // Hacky scroll state to show how the toolbar will look if its scrolled state 48 | val scrollState = TopAppBarScrollState(-1f, 0f, -2f) 49 | 50 | SmallTopAppBar( 51 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.smallScroll), 52 | navigationIcon = { 53 | NavigationIcon( 54 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconsmallScroll) 55 | ) 56 | }, 57 | title = { Text("Small top app bar") }, 58 | scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(scrollState) 59 | ) 60 | } 61 | 62 | item { 63 | 64 | // Hacky scroll state to show how the toolbar will look if its scrolled state 65 | val scrollState = TopAppBarScrollState(-1f, 0f, -2f) 66 | 67 | CenterAlignedTopAppBar( 68 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.centerAlignedScroll), 69 | navigationIcon = { 70 | NavigationIcon( 71 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconCenterAlignedScroll) 72 | ) 73 | }, 74 | title = { Text("Center aligned top app bar") }, 75 | scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(scrollState) 76 | ) 77 | } 78 | } 79 | 80 | @Composable 81 | private fun NavigationIcon(modifier: Modifier = Modifier) { 82 | IconButton( 83 | modifier = modifier, 84 | onClick = {} 85 | ) { 86 | Icon( 87 | painter = painterResource("ic_arrow_back.xml"), 88 | contentDescription = null 89 | ) 90 | } 91 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build & test](https://github.com/oas004/Material3Playground/actions/workflows/verification.yml/badge.svg?branch=main)](https://github.com/oas004/Material3Playground/actions/workflows/verification.yml) 2 | 3 | # Material3Playground 4 | Desktop Playground to check how changing the color system can affect the different components in Material 3. Mainly used to learn and to get an overview of the Material 3 design components. 5 | 6 | ## Setup 🔧 7 | 8 | You want to check out the code, you can clone the project with 9 | ``` 10 | git clone git@github.com:oas004/Material3Playground.git 11 | ``` 12 | Make sure that you are running a JDK version higher than 15. You can run the application either by running 13 | ``` 14 | ./gradlew run 15 | ``` 16 | or pressing the play gutter icon button in the `Main.kt` file. 17 | 18 | ## Gameplay 🔥 19 | 20 | ### UI Mode 21 | 22 | You can toggle the UI Mode in the window menu bar at the top left. This will update all the colors to the dark/light theme. 23 | 24 | | Light Mode | Dark Mode | 25 | |-----------------------------------------------------------|:-------------------------------------------------------:| 26 | | ![main display light](.github/screenshots/main-light.png) | ![main display dark](.github/screenshots/main-dark.png) | 27 | 28 | You can interact with the components as you would on a mobile application. However, note that this is build with compose for desktop and there will be some differences on mobile. 29 | 30 | ### Interaction 🌻 31 | You can interact with the colors on the right side. If you press a color, you can change the hexcode for this color. Pressing the "OK" button will update the components with the new color. 32 | 33 | ![Change color first time](.github/screenshots/change-color.png) 34 | 35 | For instance changing the primary color to pink will update the main display with pink as primary color like this: 36 | 37 | 38 | ![Change color updated main display](.github/screenshots/change-color-updated-main-page.png) 39 | 40 | The second time you change this color, the pink color will be remembered as a recently used color. 41 | 42 | 43 | ![Change color second time](.github/screenshots/change-color-pink.png) 44 | 45 | ## Upcoming features 👷 46 | - We are still missing some key components from Material 3. We are planing on adding them. If there are someone that you miss, please file an issue or even better; make a PR on it. 47 | 48 | ## Contribution ❄️ ⚡ 49 | Pull requests are welcome! We'd love help improving this library. Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it. 50 | 51 | ## License 52 | MIT License 53 | 54 | Copyright (c) 2025 Odin Asbjørnsen 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy 57 | of this software and associated documentation files (the "Software"), to deal 58 | in the Software without restriction, including without limitation the rights 59 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 60 | copies of the Software, and to permit persons to whom the Software is 61 | furnished to do so, subject to the following conditions: 62 | 63 | The above copyright notice and this permission notice shall be included in all 64 | copies or substantial portions of the Software. 65 | 66 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 67 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 68 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 69 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 70 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 71 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 72 | SOFTWARE. 73 | -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/ButtonVisibilityTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import TestTags 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import androidx.compose.ui.test.performClick 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class ButtonVisibilityTest { 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testIconButtonIsPresent() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 23 | waitForIdle() 24 | onNodeWithTag(TestTags.Buttons.icon, false).assertExists() 25 | } 26 | } 27 | 28 | 29 | @Test 30 | fun testRegularButtonIsPresent() { 31 | rule.apply { 32 | setContent { 33 | Material3Playground(false) 34 | } 35 | waitForIdle() 36 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 37 | waitForIdle() 38 | onNodeWithTag(TestTags.Buttons.regular, false).assertExists() 39 | } 40 | } 41 | 42 | 43 | @Test 44 | fun testOutlinedButtonIsPresent() { 45 | rule.apply { 46 | setContent { 47 | Material3Playground(false) 48 | } 49 | waitForIdle() 50 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 51 | waitForIdle() 52 | onNodeWithTag(TestTags.Buttons.outlined, false).assertExists() 53 | } 54 | } 55 | 56 | 57 | @Test 58 | fun testTextButtonIsPresent() { 59 | rule.apply { 60 | setContent { 61 | Material3Playground(false) 62 | } 63 | waitForIdle() 64 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 65 | waitForIdle() 66 | onNodeWithTag(TestTags.Buttons.text, false).assertExists() 67 | } 68 | } 69 | 70 | @Test 71 | fun testFloatingButtonIsPresent() { 72 | rule.apply { 73 | setContent { 74 | Material3Playground(false) 75 | } 76 | waitForIdle() 77 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 78 | waitForIdle() 79 | onNodeWithTag(TestTags.Buttons.floating, false).assertExists() 80 | } 81 | } 82 | 83 | 84 | @Test 85 | fun testExtendedFloatingButtonIsPresent() { 86 | rule.apply { 87 | setContent { 88 | Material3Playground(false) 89 | } 90 | waitForIdle() 91 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 92 | waitForIdle() 93 | onNodeWithTag(TestTags.Buttons.extendedFloating, false).assertExists() 94 | } 95 | } 96 | 97 | 98 | @Test 99 | fun testFilledIconButtonIsPresent() { 100 | rule.apply { 101 | setContent { 102 | Material3Playground(false) 103 | } 104 | waitForIdle() 105 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 106 | waitForIdle() 107 | onNodeWithTag(TestTags.Buttons.filledIcon, false).assertExists() 108 | } 109 | } 110 | 111 | 112 | @Test 113 | fun testIconToggleButtonIsPresent() { 114 | rule.apply { 115 | setContent { 116 | Material3Playground(false) 117 | } 118 | waitForIdle() 119 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick() 120 | waitForIdle() 121 | onNodeWithTag(TestTags.Buttons.iconToggle, false).assertExists() 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/kotlin/m3components/components/NavRailButtonTest.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import Material3Playground 4 | import TestTags 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import org.junit.Rule 8 | import org.junit.Test 9 | 10 | class NavRailButtonTest { 11 | 12 | @get:Rule 13 | val rule = createComposeRule() 14 | 15 | @Test 16 | fun testNavRailIsPresent() { 17 | rule.apply { 18 | setContent { 19 | Material3Playground(false) 20 | } 21 | waitForIdle() 22 | onNodeWithTag(TestTags.Components.navRail, false).assertExists() 23 | } 24 | } 25 | 26 | 27 | @Test 28 | fun testNavRailButtonsIsPresent() { 29 | rule.apply { 30 | setContent { 31 | Material3Playground(false) 32 | } 33 | waitForIdle() 34 | onNodeWithTag(TestTags.NavRailButtons.buttons, false).assertExists() 35 | } 36 | } 37 | 38 | @Test 39 | fun testNavRailAppBarIsPresent() { 40 | rule.apply { 41 | setContent { 42 | Material3Playground(false) 43 | } 44 | waitForIdle() 45 | onNodeWithTag(TestTags.NavRailButtons.appBars, false).assertExists() 46 | } 47 | } 48 | 49 | @Test 50 | fun testNavRailCardsIsPresent() { 51 | rule.apply { 52 | setContent { 53 | Material3Playground(false) 54 | } 55 | waitForIdle() 56 | onNodeWithTag(TestTags.NavRailButtons.cards, false).assertExists() 57 | } 58 | } 59 | 60 | @Test 61 | fun testNavRailTextFieldIsPresent() { 62 | rule.apply { 63 | setContent { 64 | Material3Playground(false) 65 | } 66 | waitForIdle() 67 | onNodeWithTag(TestTags.NavRailButtons.textFields, false).assertExists() 68 | } 69 | } 70 | 71 | @Test 72 | fun testNavRailChipsIsPresent() { 73 | rule.apply { 74 | setContent { 75 | Material3Playground(false) 76 | } 77 | waitForIdle() 78 | onNodeWithTag(TestTags.NavRailButtons.chips, false).assertExists() 79 | } 80 | } 81 | 82 | @Test 83 | fun testNavRailSwitchIsPresent() { 84 | rule.apply { 85 | setContent { 86 | Material3Playground(false) 87 | } 88 | waitForIdle() 89 | onNodeWithTag(TestTags.NavRailButtons.switch, false).assertExists() 90 | } 91 | } 92 | 93 | @Test 94 | fun testNavRailCheckBoxIsPresent() { 95 | rule.apply { 96 | setContent { 97 | Material3Playground(false) 98 | } 99 | waitForIdle() 100 | onNodeWithTag(TestTags.NavRailButtons.checkBox, false).assertExists() 101 | } 102 | } 103 | 104 | @Test 105 | fun testNavRailSlidersIsPresent() { 106 | rule.apply { 107 | setContent { 108 | Material3Playground(false) 109 | } 110 | waitForIdle() 111 | onNodeWithTag(TestTags.NavRailButtons.sliders, false).assertExists() 112 | } 113 | } 114 | 115 | @Test 116 | fun testNavRailProgressBarIsPresent() { 117 | rule.apply { 118 | setContent { 119 | Material3Playground(false) 120 | } 121 | waitForIdle() 122 | onNodeWithTag(TestTags.NavRailButtons.progressBar, false).assertExists() 123 | } 124 | } 125 | @Test 126 | fun testNavRailDividerIsPresent() { 127 | rule.apply { 128 | setContent { 129 | Material3Playground(false) 130 | } 131 | waitForIdle() 132 | onNodeWithTag(TestTags.NavRailButtons.dividers, false).assertExists() 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/kotlin/m3components/components/Buttons.kt: -------------------------------------------------------------------------------- 1 | package m3components.components 2 | 3 | import TestTags 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.lazy.grid.LazyGridScope 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.CardTravel 9 | import androidx.compose.material.icons.filled.Lock 10 | import androidx.compose.material.icons.outlined.Lock 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.ExtendedFloatingActionButton 14 | import androidx.compose.material3.FilledIconButton 15 | import androidx.compose.material3.FloatingActionButton 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.IconButton 18 | import androidx.compose.material3.IconToggleButton 19 | import androidx.compose.material3.OutlinedButton 20 | import androidx.compose.material3.Text 21 | import androidx.compose.material3.TextButton 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.platform.testTag 29 | import androidx.compose.ui.unit.dp 30 | 31 | 32 | @OptIn(ExperimentalMaterial3Api::class) 33 | fun LazyGridScope.m3Buttons() { 34 | 35 | item { 36 | Button( 37 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.regular), 38 | onClick = {}, 39 | enabled = true 40 | ) { 41 | Text("Primary button") 42 | } 43 | } 44 | 45 | item { 46 | OutlinedButton( 47 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.outlined), 48 | onClick = {}, 49 | enabled = true 50 | ) { 51 | Text("Secondary button") 52 | } 53 | } 54 | 55 | item { 56 | TextButton( 57 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.text), 58 | onClick = {}, 59 | enabled = true 60 | ) { 61 | Text("Text button") 62 | } 63 | } 64 | 65 | item { 66 | Box { 67 | FloatingActionButton( 68 | modifier = Modifier.align(Alignment.Center).padding(8.dp).testTag(TestTags.Buttons.floating), 69 | onClick = {}) { 70 | Text("FAB") 71 | } 72 | } 73 | } 74 | 75 | item { 76 | Box { 77 | ExtendedFloatingActionButton( 78 | modifier = Modifier.testTag(TestTags.Buttons.extendedFloating) 79 | .align(Alignment.Center) 80 | .padding(8.dp), 81 | onClick = {} 82 | ) { 83 | Text("Extended FAB") 84 | } 85 | } 86 | } 87 | 88 | item { 89 | IconButton( 90 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.icon), 91 | onClick = {}, enabled = true 92 | ) { 93 | Icon(imageVector = Icons.Default.CardTravel, contentDescription = null) 94 | } 95 | } 96 | 97 | item { 98 | var checked by remember { mutableStateOf(false) } 99 | IconToggleButton( 100 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.iconToggle), 101 | checked = checked, onCheckedChange = { checked = it } 102 | ) { 103 | if (checked) { 104 | Icon(Icons.Filled.Lock, contentDescription = "Localized description") 105 | } else { 106 | Icon(Icons.Outlined.Lock, contentDescription = "Localized description") 107 | } 108 | } 109 | } 110 | 111 | item { 112 | Box { 113 | FilledIconButton( 114 | modifier = Modifier.align(Alignment.Center).padding(8.dp).testTag(TestTags.Buttons.filledIcon), 115 | onClick = {}, enabled = true 116 | ) { 117 | Icon(imageVector = Icons.Default.CardTravel, contentDescription = null) 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/kotlin/FileUtils.kt: -------------------------------------------------------------------------------- 1 | import ColorFileType.Kotlin 2 | import ColorFileType.XML 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import java.io.File 7 | import java.io.FileNotFoundException 8 | import java.nio.file.FileAlreadyExistsException 9 | import java.util.Locale 10 | import javax.swing.JFileChooser 11 | import javax.swing.filechooser.FileSystemView 12 | import kotlin.io.path.Path 13 | import kotlin.io.path.createFile 14 | 15 | @Composable 16 | internal fun FileChooserDialog( 17 | title: String, 18 | onResult: (result: File) -> Unit, 19 | onCancel: () -> Unit 20 | ) { 21 | val fileChooser = JFileChooser(FileSystemView.getFileSystemView()).apply { 22 | currentDirectory = File(System.getProperty("user.dir")) 23 | dialogTitle = title 24 | fileSelectionMode = JFileChooser.DIRECTORIES_ONLY 25 | isAcceptAllFileFilterUsed = true 26 | selectedFile = null 27 | currentDirectory = null 28 | } 29 | if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { 30 | val file = fileChooser.selectedFile 31 | onResult(file) 32 | } else { 33 | onCancel.invoke() 34 | } 35 | } 36 | 37 | internal fun saveColorsToFile( 38 | currentColorPalette: ColorScheme, 39 | file: File, 40 | fileType: ColorFileType, 41 | onResult: (success: Boolean, message: String) -> Unit 42 | ) { 43 | when (fileType) { 44 | Kotlin -> saveColorsToKotlinFile(file = file, currentColorPalette = currentColorPalette, onResult = onResult) 45 | XML -> saveColorsToXMLFile(file = file, currentColorPalette = currentColorPalette, onResult = onResult) 46 | } 47 | } 48 | 49 | private fun saveColorsToXMLFile( 50 | file: File, 51 | currentColorPalette: ColorScheme, 52 | onResult: (success: Boolean, message: String) -> Unit 53 | ) { 54 | tryWriteToFile( 55 | onResult = onResult 56 | ) { 57 | val path = file.path 58 | val outputFile = Path(path, "colors.xml").createFile().toFile() 59 | outputFile.apply { 60 | 61 | getColorList(colorPalette = currentColorPalette).forEach { color -> 62 | appendText(getColorsXMLProperty(colorNameLowerCase = color.key, color = color.value)) 63 | } 64 | 65 | } 66 | onResult(true, "Saving colors worked") 67 | } 68 | } 69 | 70 | 71 | private fun saveColorsToKotlinFile( 72 | file: File, 73 | currentColorPalette: ColorScheme, 74 | onResult: (success: Boolean, message: String) -> Unit 75 | ) { 76 | tryWriteToFile( 77 | onResult = onResult 78 | ) { 79 | val path = file.path 80 | val outputFile = Path(path, "colors.kt").createFile().toFile() 81 | outputFile.apply { 82 | 83 | getColorList(colorPalette = currentColorPalette).forEach { color -> 84 | appendText(getColorKotlinProperty(colorNameLowerCase = color.key, color = color.value)) 85 | } 86 | 87 | } 88 | onResult(true, "Saving colors worked") 89 | } 90 | } 91 | private fun tryWriteToFile( 92 | onResult: (success: Boolean, message: String) -> Unit, 93 | block: () -> Unit, 94 | ) { 95 | try { 96 | block.invoke() 97 | } catch (fileNotFound: FileNotFoundException) { 98 | onResult(false, "Could not save to this folder as it does not exist.") 99 | } catch (securityException: SecurityException) { 100 | onResult(false, "Can't save this file due to security reasons.") 101 | } catch (fileAlreadyExists: FileAlreadyExistsException) { 102 | onResult(false, "A file with this name already exists here.") 103 | } 104 | } 105 | 106 | internal fun getColorsXMLProperty(colorNameLowerCase: String, color: Color): String { 107 | return "${color.hexCode} \n" 108 | } 109 | 110 | 111 | internal fun getColorKotlinProperty(colorNameLowerCase: String, color: Color): String { 112 | val capitalizedColorName = colorNameLowerCase.replaceFirstChar { 113 | if (it.isLowerCase()) { 114 | it.titlecase(Locale.getDefault()) 115 | } else { 116 | it.toString() 117 | } 118 | } 119 | return "val $capitalizedColorName: Color = Color(0x${color.hexCode.takeLast(8)}) \n" 120 | } 121 | 122 | private fun getColorList(colorPalette: ColorScheme): Map { 123 | return mapOf( 124 | "primary" to colorPalette.primary, 125 | "onPrimary" to colorPalette.onPrimary, 126 | "primaryContainer" to colorPalette.primaryContainer, 127 | "onPrimaryContainer" to colorPalette.onPrimaryContainer, 128 | "inversePrimary" to colorPalette.inversePrimary, 129 | "secondary" to colorPalette.secondary, 130 | "onSecondary" to colorPalette.onSecondary, 131 | "secondaryContainer" to colorPalette.secondaryContainer, 132 | "onSecondaryContainer" to colorPalette.onSecondaryContainer, 133 | "tertiary" to colorPalette.tertiary, 134 | "onTertiary" to colorPalette.onTertiary, 135 | "tertiaryContainer" to colorPalette.tertiaryContainer, 136 | "onTertiaryContainer" to colorPalette.onTertiaryContainer, 137 | "background" to colorPalette.background, 138 | "onBackground" to colorPalette.onBackground, 139 | "surface" to colorPalette.surface, 140 | "onSurface" to colorPalette.onSurface, 141 | "surfaceVariant" to colorPalette.surfaceVariant, 142 | "onSurfaceVariant" to colorPalette.onSurfaceVariant, 143 | "surfaceTint" to colorPalette.surfaceTint, 144 | "inverseSurface" to colorPalette.inverseSurface, 145 | "inverseOnSurface" to colorPalette.inverseOnSurface, 146 | "error" to colorPalette.error, 147 | "onError" to colorPalette.onError, 148 | "errorContainer" to colorPalette.errorContainer, 149 | "onErrorContainer" to colorPalette.onErrorContainer, 150 | "outline" to colorPalette.outline, 151 | ) 152 | } 153 | 154 | internal enum class ColorFileType { 155 | Kotlin, XML 156 | } 157 | -------------------------------------------------------------------------------- /src/main/kotlin/ThemeColorPicker.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.background 2 | import androidx.compose.foundation.clickable 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.rememberScrollState 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.foundation.verticalScroll 17 | import androidx.compose.material3.Button 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Text 20 | import androidx.compose.material3.TextField 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.graphics.toArgb 31 | import androidx.compose.ui.text.TextStyle 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.window.Dialog 34 | import java.awt.Dimension 35 | 36 | @Composable 37 | internal fun ThemeColorPicker( 38 | modifier: Modifier = Modifier, 39 | colorName: String, 40 | currentColor: Color, 41 | onColorPicked: (colorName: String, pickedColor: Color) -> Unit, 42 | recentlyUsedColors: List = listOf(), 43 | ) { 44 | var isDialogVisible by remember { mutableStateOf(false) } 45 | 46 | Column( 47 | modifier = modifier then Modifier.clickable { isDialogVisible = true } 48 | .padding(8.dp), 49 | horizontalAlignment = Alignment.CenterHorizontally 50 | ) { 51 | Box( 52 | modifier = Modifier.size(40.dp) 53 | .background(color = currentColor) 54 | .clip(shape = RoundedCornerShape(8.dp)) 55 | ) 56 | M3OnSurfaceText( 57 | text = colorName, 58 | style = MaterialTheme.typography.titleMedium 59 | ) 60 | } 61 | 62 | Dialog( 63 | visible = isDialogVisible, 64 | onCloseRequest = { isDialogVisible = false }, 65 | title = colorName, 66 | ) { 67 | window.size = Dimension(400, 400) 68 | 69 | ColorPickerDialogContent( 70 | currentColor, 71 | onOkClicked = { pickedColor -> 72 | // Update the color palette and hide the dialog 73 | onColorPicked(colorName, pickedColor) 74 | isDialogVisible = false 75 | }, 76 | recentlyUsedColors = recentlyUsedColors 77 | ) 78 | } 79 | } 80 | 81 | @Composable 82 | private fun ColorPickerDialogContent( 83 | currentColor: Color, 84 | onOkClicked: (pickedColor: Color) -> Unit, 85 | modifier: Modifier = Modifier, 86 | recentlyUsedColors: List = listOf(), 87 | ) { 88 | var pickedColor by remember { mutableStateOf(currentColor) } 89 | val scrollState = rememberScrollState() 90 | 91 | Column( 92 | modifier = modifier.fillMaxSize() 93 | .padding(16.dp) 94 | .verticalScroll(scrollState), 95 | horizontalAlignment = Alignment.CenterHorizontally 96 | ) { 97 | Box( 98 | modifier = Modifier.size(80.dp) 99 | .background(color = pickedColor) 100 | .clip(shape = RoundedCornerShape(8.dp)) 101 | ) 102 | 103 | Spacer(Modifier.size(8.dp)) 104 | 105 | val currentColorHexString = currentColor.value.toString(16).substring(0, 8) 106 | val text = remember { mutableStateOf("#$currentColorHexString") } 107 | 108 | TextField( 109 | modifier = Modifier.width(120.dp), 110 | value = text.value, 111 | onValueChange = { 112 | text.value = it 113 | 114 | val newColor = getColorFromText(it) 115 | if (newColor != null) pickedColor = newColor 116 | }, 117 | placeholder = { Text("Please enter a color hex") } 118 | ) 119 | 120 | Spacer(modifier = Modifier.size(16.dp)) 121 | 122 | if (recentlyUsedColors.isNotEmpty()) { 123 | M3OnSurfaceText( 124 | "Recently used colors", 125 | MaterialTheme.typography.labelSmall 126 | ) 127 | 128 | RecentlyUsedColors( 129 | modifier = Modifier.fillMaxWidth().padding(top = 8.dp), 130 | colors = recentlyUsedColors, 131 | onColorClick = { 132 | text.value = it.hexCode 133 | val newColor = getColorFromText(it.hexCode) 134 | if (newColor != null) pickedColor = newColor 135 | } 136 | ) 137 | } 138 | 139 | Spacer(modifier = Modifier.size(32.dp)) 140 | 141 | Button( 142 | onClick = { onOkClicked(pickedColor) } 143 | ) { 144 | Text("OK") 145 | } 146 | } 147 | } 148 | 149 | @Composable 150 | fun M3OnSurfaceText( 151 | text: String, 152 | style: TextStyle, 153 | ) { 154 | Text( 155 | color = MaterialTheme.colorScheme.onSurface, 156 | text = text, 157 | style = style, 158 | ) 159 | } 160 | 161 | @Composable 162 | private fun RecentlyUsedColors( 163 | colors: List, 164 | onColorClick: (color: Color) -> Unit, 165 | modifier: Modifier = Modifier, 166 | ) { 167 | Row( 168 | modifier = modifier, 169 | horizontalArrangement = Arrangement.Center 170 | ) { 171 | colors.forEach { 172 | Box( 173 | modifier = Modifier 174 | .size(30.dp) 175 | .padding(8.dp) 176 | .clickable { onColorClick(it) } 177 | .clip(CircleShape) 178 | .background(it) 179 | ) 180 | } 181 | } 182 | } 183 | 184 | private fun getColorFromText(hexCode: String): Color? { 185 | return try { 186 | val hexString = hexCode.removePrefix("#").lowercase() 187 | val updatedHexString = if (hexString.length == 6) "ff$hexString" else hexString 188 | 189 | Color(updatedHexString.toLong(16)) 190 | } catch (e: Exception) { 191 | null 192 | } 193 | } 194 | 195 | internal val Color.hexCode: String 196 | get() = String.format("#%08X", toArgb()) -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import ColorFileType.Kotlin 2 | import ColorFileType.XML 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.defaultMinSize 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material.AlertDialog 14 | import androidx.compose.material.Button 15 | import androidx.compose.material.ExperimentalMaterialApi 16 | import androidx.compose.material.Text 17 | import androidx.compose.material3.ColorScheme 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.darkColorScheme 20 | import androidx.compose.material3.lightColorScheme 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.ExperimentalComposeUiApi 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.input.key.Key 31 | import androidx.compose.ui.input.key.KeyShortcut 32 | import androidx.compose.ui.platform.testTag 33 | import androidx.compose.ui.res.painterResource 34 | import androidx.compose.ui.text.style.TextAlign 35 | import androidx.compose.ui.unit.dp 36 | import androidx.compose.ui.window.MenuBar 37 | import androidx.compose.ui.window.Tray 38 | import androidx.compose.ui.window.Window 39 | import androidx.compose.ui.window.WindowPosition 40 | import androidx.compose.ui.window.application 41 | import androidx.compose.ui.window.rememberWindowState 42 | import java.awt.Dimension 43 | 44 | private const val windowMinWidth = 500 45 | private const val windowMinHeight = 600 46 | 47 | @OptIn(ExperimentalComposeUiApi::class) 48 | fun main() = application { 49 | 50 | Tray( 51 | icon = painterResource("icon.png"), 52 | menu = { 53 | Item("Quit App", onClick = ::exitApplication) 54 | } 55 | ) 56 | 57 | Window( 58 | onCloseRequest = ::exitApplication, 59 | title = "Material 3 Playground", 60 | state = rememberWindowState( 61 | width = 1200.dp, height = 800.dp, position = WindowPosition( 62 | Alignment.Center 63 | ) 64 | ), 65 | resizable = true, 66 | icon = painterResource("icon.png") 67 | ) { 68 | var darkmode by remember { mutableStateOf(false) } 69 | 70 | MenuBar { 71 | Menu(text = "UI Mode", mnemonic = 'U') { 72 | val darkModeMenuText = if (darkmode) { 73 | "Toggle Dark Mode Off" 74 | } else { 75 | "Toggle Dark Mode On" 76 | } 77 | Item( 78 | text = darkModeMenuText, 79 | shortcut = KeyShortcut(key = Key.D, meta = true), 80 | onClick = { 81 | darkmode = !darkmode 82 | }, 83 | ) 84 | } 85 | } 86 | 87 | window.minimumSize = Dimension(windowMinWidth, windowMinHeight) 88 | 89 | 90 | Material3Playground(darkmode = darkmode) 91 | } 92 | } 93 | 94 | @OptIn(ExperimentalMaterialApi::class) 95 | @Composable 96 | internal fun Material3Playground(darkmode: Boolean) { 97 | var lightColorScheme by remember { mutableStateOf(lightColorScheme()) } 98 | var darkColorScheme by remember { mutableStateOf(darkColorScheme()) } 99 | 100 | MaterialTheme( 101 | colorScheme = if (darkmode) darkColorScheme else lightColorScheme 102 | ) { 103 | Row( 104 | modifier = Modifier 105 | .fillMaxSize() 106 | .background(color = MaterialTheme.colorScheme.surface) 107 | ) { 108 | 109 | val currentColorPalette = MaterialTheme.colorScheme 110 | var shouldShowFileChooser by remember { mutableStateOf(false) } 111 | var fileSaverDialog: String? by remember { mutableStateOf(null) } 112 | var fileTypeDialog by remember { mutableStateOf(false) } 113 | var savableFileType by remember { mutableStateOf(Kotlin) } 114 | 115 | ComponentScope( 116 | onColorPicked = { colorName, color -> 117 | val updatedColorPalette = updateColorPalette( 118 | currentColorPalette = currentColorPalette, 119 | colorName = colorName, 120 | color = color, 121 | ) 122 | 123 | if (darkmode) { 124 | darkColorScheme = updatedColorPalette 125 | } else { 126 | lightColorScheme = updatedColorPalette 127 | } 128 | }, 129 | onPrintColors = { 130 | fileTypeDialog = true 131 | } 132 | ) 133 | 134 | if (shouldShowFileChooser) { 135 | FileChooserDialog( 136 | title = "Select directory to save file", 137 | onResult = { 138 | saveColorsToFile( 139 | currentColorPalette, file = it, onResult = { success, message -> 140 | // Implement success / failure screens for this when VM is implemented, 141 | // and we can easier manage states. 142 | fileSaverDialog = message 143 | }, 144 | fileType = savableFileType 145 | ) 146 | shouldShowFileChooser = false 147 | }, 148 | onCancel = { 149 | shouldShowFileChooser = false 150 | } 151 | ) 152 | } 153 | 154 | if (fileSaverDialog != null) { 155 | val title = when (savableFileType) { 156 | Kotlin -> "Saving Colors to Colors.kt" 157 | XML -> "Saving Colors to Colors.xml" 158 | } 159 | AlertDialog( 160 | modifier = Modifier.defaultMinSize(minWidth = 400.dp, minHeight = 150.dp) 161 | .background(color = MaterialTheme.colorScheme.surface).testTag(TestTags.FileSaverDialog.dialog), 162 | shape = RoundedCornerShape(8.dp), 163 | title = { 164 | Text( 165 | modifier = Modifier.fillMaxWidth().padding(12.dp), 166 | text = title, 167 | textAlign = TextAlign.Center, 168 | style = MaterialTheme.typography.headlineMedium 169 | ) 170 | }, 171 | buttons = { 172 | Button( 173 | modifier = Modifier.align(Alignment.CenterVertically), 174 | onClick = { 175 | fileSaverDialog = null 176 | } 177 | ) { 178 | Text( 179 | text = "OK", 180 | textAlign = TextAlign.Center, 181 | style = MaterialTheme.typography.labelMedium, 182 | color = MaterialTheme.colorScheme.onPrimary 183 | ) 184 | } 185 | }, 186 | onDismissRequest = { 187 | fileSaverDialog = null 188 | }, 189 | text = { 190 | Text( 191 | modifier = Modifier.fillMaxWidth().padding(12.dp), 192 | text = fileSaverDialog ?: "", 193 | textAlign = TextAlign.Center, 194 | style = MaterialTheme.typography.bodyMedium 195 | ) 196 | } 197 | ) 198 | } 199 | 200 | if (fileTypeDialog) { 201 | ChooseFileTypeDialog( 202 | onTypeChosen = { type -> 203 | savableFileType = type 204 | shouldShowFileChooser = true 205 | fileTypeDialog = false 206 | }, 207 | onDismiss = { 208 | fileTypeDialog = false 209 | } 210 | ) 211 | } 212 | 213 | } 214 | } 215 | } 216 | 217 | @OptIn(ExperimentalMaterialApi::class) 218 | @Composable 219 | private fun ChooseFileTypeDialog(onTypeChosen: (colorType: ColorFileType) -> Unit, onDismiss: () -> Unit) { 220 | AlertDialog( 221 | modifier = Modifier.defaultMinSize(minWidth = 400.dp, minHeight = 150.dp) 222 | .background(color = MaterialTheme.colorScheme.surface).testTag(TestTags.FileSaverDialog.dialog), 223 | shape = RoundedCornerShape(8.dp), 224 | title = { 225 | Text( 226 | modifier = Modifier.fillMaxWidth().padding(12.dp), 227 | text = "Do you want to save colors as XML or Kotlin?", 228 | textAlign = TextAlign.Center, 229 | style = MaterialTheme.typography.headlineMedium 230 | ) 231 | }, 232 | buttons = { 233 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { 234 | Button( 235 | modifier = Modifier.align(Alignment.CenterVertically), 236 | onClick = { 237 | onTypeChosen(Kotlin) 238 | } 239 | ) { 240 | Text( 241 | text = "Kotlin", 242 | textAlign = TextAlign.Center, 243 | style = MaterialTheme.typography.labelMedium, 244 | color = MaterialTheme.colorScheme.onPrimary 245 | ) 246 | } 247 | Spacer(Modifier.size(20.dp)) 248 | Button( 249 | modifier = Modifier.align(Alignment.CenterVertically), 250 | onClick = { 251 | onTypeChosen(XML) 252 | } 253 | ) { 254 | Text( 255 | text = "XML", 256 | textAlign = TextAlign.Center, 257 | style = MaterialTheme.typography.labelMedium, 258 | color = MaterialTheme.colorScheme.onPrimary 259 | ) 260 | } 261 | } 262 | }, 263 | onDismissRequest = { 264 | onDismiss.invoke() 265 | }, 266 | text = { 267 | Text( 268 | modifier = Modifier.fillMaxWidth().padding(12.dp), 269 | text = "Please choose a file type to save the color files as", 270 | textAlign = TextAlign.Center, 271 | style = MaterialTheme.typography.bodyMedium 272 | ) 273 | } 274 | ) 275 | } 276 | 277 | private fun updateColorPalette( 278 | currentColorPalette: ColorScheme, 279 | colorName: String, 280 | color: Color 281 | ): ColorScheme { 282 | return when (colorName) { 283 | "primary" -> currentColorPalette.copy(primary = color) 284 | "onPrimary" -> currentColorPalette.copy(onPrimary = color) 285 | "primaryContainer" -> currentColorPalette.copy(primaryContainer = color) 286 | "onPrimaryContainer" -> currentColorPalette.copy(onPrimaryContainer = color) 287 | "inversePrimary" -> currentColorPalette.copy(inversePrimary = color) 288 | "secondary" -> currentColorPalette.copy(secondary = color) 289 | "onSecondary" -> currentColorPalette.copy(onSecondary = color) 290 | "secondaryContainer" -> currentColorPalette.copy(secondaryContainer = color) 291 | "onSecondaryContainer" -> currentColorPalette.copy(onSecondaryContainer = color) 292 | "tertiary" -> currentColorPalette.copy(tertiary = color) 293 | "onTertiary" -> currentColorPalette.copy(onTertiary = color) 294 | "tertiaryContainer" -> currentColorPalette.copy(tertiaryContainer = color) 295 | "onTertiaryContainer" -> currentColorPalette.copy(onTertiaryContainer = color) 296 | "background" -> currentColorPalette.copy(background = color) 297 | "onBackground" -> currentColorPalette.copy(onBackground = color) 298 | "surface" -> currentColorPalette.copy(surface = color) 299 | "onSurface" -> currentColorPalette.copy(onSurface = color) 300 | "surfaceVariant" -> currentColorPalette.copy(surfaceVariant = color) 301 | "onSurfaceVariant" -> currentColorPalette.copy(onSurfaceVariant = color) 302 | "surfaceTint" -> currentColorPalette.copy(surfaceTint = color) 303 | "inverseSurface" -> currentColorPalette.copy(inverseSurface = color) 304 | "inverseOnSurface" -> currentColorPalette.copy(inverseOnSurface = color) 305 | "error" -> currentColorPalette.copy(error = color) 306 | "onError" -> currentColorPalette.copy(onError = color) 307 | "errorContainer" -> currentColorPalette.copy(errorContainer = color) 308 | "onErrorContainer" -> currentColorPalette.copy(onErrorContainer = color) 309 | "outline" -> currentColorPalette.copy(outline = color) 310 | else -> throw IllegalArgumentException("Color name is wrong $colorName") 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/main/kotlin/ComponentScope.kt: -------------------------------------------------------------------------------- 1 | 2 | import androidx.compose.foundation.background 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxHeight 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.grid.GridCells 9 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.AppShortcut 12 | import androidx.compose.material.icons.filled.CallToAction 13 | import androidx.compose.material.icons.filled.CheckBox 14 | import androidx.compose.material.icons.filled.CreditCard 15 | import androidx.compose.material.icons.filled.Download 16 | import androidx.compose.material.icons.filled.Poll 17 | import androidx.compose.material.icons.filled.Slideshow 18 | import androidx.compose.material.icons.filled.SpatialAudio 19 | import androidx.compose.material.icons.filled.SwitchLeft 20 | import androidx.compose.material.icons.filled.TextFields 21 | import androidx.compose.material.icons.filled.TouchApp 22 | import androidx.compose.material3.FloatingActionButton 23 | import androidx.compose.material3.Icon 24 | import androidx.compose.material3.MaterialTheme 25 | import androidx.compose.material3.NavigationRail 26 | import androidx.compose.material3.NavigationRailItem 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateOf 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.runtime.setValue 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.graphics.Color 35 | import androidx.compose.ui.platform.testTag 36 | import androidx.compose.ui.text.style.TextAlign 37 | import androidx.compose.ui.unit.dp 38 | import components.m3Cards 39 | import components.m3Checkbox 40 | import components.m3Chips 41 | import components.m3Divider 42 | import components.m3NavigationBars 43 | import components.m3ProgressBar 44 | import components.m3Slider 45 | import components.m3Switch 46 | import components.m3Tab 47 | import components.m3TextFields 48 | import components.m3TopAppBars 49 | import m3components.components.m3Buttons 50 | 51 | enum class Page { 52 | Buttons, AppBars, Cards, TextFields, Chips, Switch, Checkbox, Sliders, ProgressBars, Dividers 53 | } 54 | 55 | @Composable 56 | fun ComponentScope( 57 | onColorPicked: (name: String, color: Color) -> Unit, 58 | onPrintColors: () -> Unit, 59 | ) { 60 | var selectedPage by remember { mutableStateOf(Page.Buttons) } 61 | 62 | Row( 63 | Modifier 64 | .fillMaxSize() 65 | .background(color = MaterialTheme.colorScheme.background) 66 | ) { 67 | NavigationRail( 68 | modifier = Modifier.fillMaxHeight().padding(4.dp).testTag(TestTags.Components.navRail), 69 | header = { 70 | FloatingActionButton(onClick = onPrintColors) { 71 | Icon(imageVector = Icons.Default.CallToAction, contentDescription = "Save Current Colors to File") 72 | } 73 | } 74 | ) { 75 | 76 | NavigationRailItem( 77 | modifier = Modifier.testTag(TestTags.NavRailButtons.buttons), 78 | selected = selectedPage == Page.Buttons, 79 | onClick = { 80 | selectedPage = Page.Buttons 81 | }, 82 | icon = { 83 | Icon(imageVector = Icons.Default.TouchApp, contentDescription = "Buttons") 84 | }, 85 | label = { 86 | Text( 87 | text = "Buttons", 88 | style = MaterialTheme.typography.labelMedium 89 | ) 90 | } 91 | ) 92 | 93 | NavigationRailItem( 94 | modifier = Modifier.testTag(TestTags.NavRailButtons.appBars), 95 | selected = selectedPage == Page.AppBars, 96 | onClick = { 97 | selectedPage = Page.AppBars 98 | }, 99 | icon = { 100 | Icon(imageVector = Icons.Default.AppShortcut, contentDescription = "App Bars") 101 | }, 102 | label = { 103 | Text( 104 | text = "App Bars", 105 | style = MaterialTheme.typography.labelMedium 106 | ) 107 | } 108 | ) 109 | 110 | NavigationRailItem( 111 | modifier = Modifier.testTag(TestTags.NavRailButtons.cards), 112 | selected = selectedPage == Page.Cards, 113 | onClick = { 114 | selectedPage = Page.Cards 115 | }, 116 | icon = { 117 | Icon(imageVector = Icons.Default.CreditCard, contentDescription = "Cards") 118 | }, 119 | label = { 120 | Text( 121 | text = "Cards", 122 | style = MaterialTheme.typography.labelMedium 123 | ) 124 | } 125 | ) 126 | 127 | NavigationRailItem( 128 | modifier = Modifier.testTag(TestTags.NavRailButtons.textFields), 129 | selected = selectedPage == Page.TextFields, 130 | onClick = { 131 | selectedPage = Page.TextFields 132 | }, 133 | icon = { 134 | Icon(imageVector = Icons.Default.TextFields, contentDescription = "TextFields") 135 | }, 136 | label = { 137 | Text( 138 | text = "TextFields", 139 | style = MaterialTheme.typography.labelMedium 140 | ) 141 | } 142 | ) 143 | 144 | NavigationRailItem( 145 | modifier = Modifier.testTag(TestTags.NavRailButtons.chips), 146 | selected = selectedPage == Page.Chips, 147 | onClick = { 148 | selectedPage = Page.Chips 149 | }, 150 | icon = { 151 | Icon(imageVector = Icons.Default.SpatialAudio, contentDescription = "Chips") 152 | }, 153 | label = { 154 | Text( 155 | text = "Chips", 156 | style = MaterialTheme.typography.labelMedium 157 | ) 158 | } 159 | ) 160 | NavigationRailItem( 161 | modifier = Modifier.testTag(TestTags.NavRailButtons.switch), 162 | selected = selectedPage == Page.Switch, 163 | onClick = { 164 | selectedPage = Page.Switch 165 | }, 166 | icon = { 167 | Icon(imageVector = Icons.Default.SwitchLeft, contentDescription = "Switch") 168 | }, 169 | label = { 170 | Text( 171 | text = "Switch", 172 | style = MaterialTheme.typography.labelMedium 173 | ) 174 | } 175 | ) 176 | NavigationRailItem( 177 | modifier = Modifier.testTag(TestTags.NavRailButtons.checkBox), 178 | selected = selectedPage == Page.Checkbox, 179 | onClick = { 180 | selectedPage = Page.Checkbox 181 | }, 182 | icon = { 183 | Icon(imageVector = Icons.Default.CheckBox, contentDescription = "Checkbox") 184 | }, 185 | label = { 186 | Text( 187 | text = "Checkbox", 188 | style = MaterialTheme.typography.labelMedium 189 | ) 190 | } 191 | ) 192 | NavigationRailItem( 193 | modifier = Modifier.testTag(TestTags.NavRailButtons.sliders), 194 | selected = selectedPage == Page.Sliders, 195 | onClick = { 196 | selectedPage = Page.Sliders 197 | }, 198 | icon = { 199 | Icon(imageVector = Icons.Default.Slideshow, contentDescription = "Sliders") 200 | }, 201 | label = { 202 | Text( 203 | text = "Sliders", 204 | style = MaterialTheme.typography.labelMedium 205 | ) 206 | } 207 | ) 208 | 209 | NavigationRailItem( 210 | modifier = Modifier.testTag(TestTags.NavRailButtons.progressBar), 211 | selected = selectedPage == Page.ProgressBars, 212 | onClick = { 213 | selectedPage = Page.ProgressBars 214 | }, 215 | icon = { 216 | Icon(imageVector = Icons.Default.Download, contentDescription = "Progress Bars") 217 | }, 218 | label = { 219 | Text( 220 | textAlign = TextAlign.Center, 221 | text = "ProgressBars", 222 | style = MaterialTheme.typography.labelMedium 223 | ) 224 | } 225 | ) 226 | 227 | NavigationRailItem( 228 | modifier = Modifier.testTag(TestTags.NavRailButtons.dividers), 229 | selected = selectedPage == Page.Dividers, 230 | onClick = { 231 | selectedPage = Page.Dividers 232 | }, 233 | icon = { 234 | Icon(imageVector = Icons.Default.Poll, contentDescription = "Dividers") 235 | }, 236 | label = { 237 | Text( 238 | text = "Dividers", 239 | style = MaterialTheme.typography.labelMedium 240 | ) 241 | } 242 | ) 243 | } 244 | 245 | // Components 246 | M3Components( 247 | modifier = Modifier.fillMaxSize() 248 | .weight(2f) 249 | .padding(16.dp), 250 | page = selectedPage, 251 | ) 252 | 253 | // Color picking area 254 | ColorPickers( 255 | modifier = Modifier.fillMaxSize() 256 | .weight(1f) 257 | .padding(16.dp), 258 | onColorPicked = onColorPicked 259 | ) 260 | } 261 | } 262 | 263 | @Composable 264 | private fun M3Components(modifier: Modifier, page: Page) { 265 | LazyVerticalGrid( 266 | modifier = modifier, 267 | columns = GridCells.Adaptive(300.dp), 268 | contentPadding = PaddingValues(20.dp) 269 | ) { 270 | when (page) { 271 | Page.Buttons -> { 272 | m3Buttons() 273 | } 274 | 275 | Page.AppBars -> { 276 | m3TopAppBars() 277 | m3NavigationBars() 278 | m3Tab() 279 | } 280 | 281 | Page.Cards -> { 282 | m3Cards() 283 | } 284 | 285 | Page.TextFields -> { 286 | m3TextFields() 287 | } 288 | 289 | Page.Chips -> { 290 | m3Chips() 291 | } 292 | 293 | Page.Switch -> { 294 | m3Switch() 295 | } 296 | 297 | Page.Checkbox -> { 298 | m3Checkbox() 299 | } 300 | 301 | Page.Sliders -> { 302 | m3Slider() 303 | } 304 | 305 | Page.ProgressBars -> { 306 | m3ProgressBar() 307 | } 308 | 309 | Page.Dividers -> { 310 | m3Divider() 311 | } 312 | } 313 | } 314 | } 315 | 316 | @Composable 317 | private fun ColorPickers( 318 | modifier: Modifier, 319 | onColorPicked: (name: String, color: Color) -> Unit, 320 | ) { 321 | // Do not use MutableList since it will not be stable. 322 | var recentlyUsedColors: List by remember { mutableStateOf(listOf()) } 323 | 324 | val colorPickedCallback: (colorName: String, pickedColor: Color) -> Unit = 325 | { colorName, pickedColor -> 326 | onColorPicked(colorName, pickedColor) 327 | recentlyUsedColors = recentlyUsedColors.toMutableList().also { 328 | if (!it.contains(pickedColor)) { 329 | it.add(pickedColor) 330 | } 331 | } 332 | } 333 | 334 | LazyVerticalGrid( 335 | modifier = modifier, 336 | columns = GridCells.Adaptive(minSize = 150.dp), 337 | contentPadding = PaddingValues(40.dp) 338 | ) { 339 | 340 | item { 341 | ThemeColorPicker( 342 | colorName = "primary", 343 | currentColor = MaterialTheme.colorScheme.primary, 344 | onColorPicked = colorPickedCallback, 345 | recentlyUsedColors = recentlyUsedColors, 346 | ) 347 | } 348 | item { 349 | ThemeColorPicker( 350 | colorName = "onPrimary", 351 | currentColor = MaterialTheme.colorScheme.onPrimary, 352 | onColorPicked = colorPickedCallback, 353 | recentlyUsedColors = recentlyUsedColors, 354 | ) 355 | } 356 | item { 357 | ThemeColorPicker( 358 | colorName = "primaryContainer", 359 | currentColor = MaterialTheme.colorScheme.primaryContainer, 360 | onColorPicked = colorPickedCallback, 361 | recentlyUsedColors = recentlyUsedColors, 362 | ) 363 | } 364 | item { 365 | ThemeColorPicker( 366 | colorName = "onPrimaryContainer", 367 | currentColor = MaterialTheme.colorScheme.onPrimaryContainer, 368 | onColorPicked = colorPickedCallback, 369 | recentlyUsedColors = recentlyUsedColors, 370 | ) 371 | } 372 | item { 373 | ThemeColorPicker( 374 | colorName = "inversePrimary", 375 | currentColor = MaterialTheme.colorScheme.inversePrimary, 376 | onColorPicked = colorPickedCallback, 377 | recentlyUsedColors = recentlyUsedColors, 378 | ) 379 | } 380 | item { 381 | ThemeColorPicker( 382 | colorName = "secondary", 383 | currentColor = MaterialTheme.colorScheme.secondary, 384 | onColorPicked = colorPickedCallback, 385 | recentlyUsedColors = recentlyUsedColors, 386 | ) 387 | } 388 | item { 389 | ThemeColorPicker( 390 | colorName = "onSecondary", 391 | currentColor = MaterialTheme.colorScheme.onSecondary, 392 | onColorPicked = colorPickedCallback, 393 | recentlyUsedColors = recentlyUsedColors, 394 | ) 395 | } 396 | item { 397 | ThemeColorPicker( 398 | colorName = "secondaryContainer", 399 | currentColor = MaterialTheme.colorScheme.secondaryContainer, 400 | onColorPicked = colorPickedCallback, 401 | recentlyUsedColors = recentlyUsedColors, 402 | ) 403 | } 404 | item { 405 | ThemeColorPicker( 406 | colorName = "onSecondaryContainer", 407 | currentColor = MaterialTheme.colorScheme.onSecondaryContainer, 408 | onColorPicked = colorPickedCallback, 409 | recentlyUsedColors = recentlyUsedColors, 410 | ) 411 | } 412 | item { 413 | ThemeColorPicker( 414 | colorName = "tertiary", 415 | currentColor = MaterialTheme.colorScheme.tertiary, 416 | onColorPicked = colorPickedCallback, 417 | recentlyUsedColors = recentlyUsedColors, 418 | ) 419 | } 420 | item { 421 | ThemeColorPicker( 422 | colorName = "onTertiary", 423 | currentColor = MaterialTheme.colorScheme.onTertiary, 424 | onColorPicked = colorPickedCallback, 425 | recentlyUsedColors = recentlyUsedColors, 426 | ) 427 | } 428 | item { 429 | ThemeColorPicker( 430 | colorName = "tertiaryContainer", 431 | currentColor = MaterialTheme.colorScheme.tertiaryContainer, 432 | onColorPicked = colorPickedCallback, 433 | recentlyUsedColors = recentlyUsedColors, 434 | ) 435 | } 436 | item { 437 | ThemeColorPicker( 438 | colorName = "onTertiaryContainer", 439 | currentColor = MaterialTheme.colorScheme.onTertiaryContainer, 440 | onColorPicked = colorPickedCallback, 441 | recentlyUsedColors = recentlyUsedColors, 442 | ) 443 | } 444 | item { 445 | ThemeColorPicker( 446 | colorName = "background", 447 | currentColor = MaterialTheme.colorScheme.background, 448 | onColorPicked = colorPickedCallback, 449 | recentlyUsedColors = recentlyUsedColors, 450 | ) 451 | } 452 | item { 453 | ThemeColorPicker( 454 | colorName = "onBackground", 455 | currentColor = MaterialTheme.colorScheme.onBackground, 456 | onColorPicked = colorPickedCallback, 457 | recentlyUsedColors = recentlyUsedColors, 458 | ) 459 | } 460 | item { 461 | ThemeColorPicker( 462 | colorName = "surface", 463 | currentColor = MaterialTheme.colorScheme.surface, 464 | onColorPicked = colorPickedCallback, 465 | recentlyUsedColors = recentlyUsedColors, 466 | ) 467 | } 468 | item { 469 | ThemeColorPicker( 470 | colorName = "onSurface", 471 | currentColor = MaterialTheme.colorScheme.onSurface, 472 | onColorPicked = colorPickedCallback, 473 | recentlyUsedColors = recentlyUsedColors, 474 | ) 475 | } 476 | item { 477 | ThemeColorPicker( 478 | colorName = "surfaceVariant", 479 | currentColor = MaterialTheme.colorScheme.surfaceVariant, 480 | onColorPicked = colorPickedCallback, 481 | recentlyUsedColors = recentlyUsedColors, 482 | ) 483 | } 484 | item { 485 | ThemeColorPicker( 486 | colorName = "onSurfaceVariant", 487 | currentColor = MaterialTheme.colorScheme.onSurfaceVariant, 488 | onColorPicked = colorPickedCallback, 489 | recentlyUsedColors = recentlyUsedColors, 490 | ) 491 | } 492 | item { 493 | ThemeColorPicker( 494 | colorName = "surfaceTint", 495 | currentColor = MaterialTheme.colorScheme.surfaceTint, 496 | onColorPicked = colorPickedCallback, 497 | recentlyUsedColors = recentlyUsedColors, 498 | ) 499 | } 500 | item { 501 | ThemeColorPicker( 502 | colorName = "inverseSurface", 503 | currentColor = MaterialTheme.colorScheme.inverseSurface, 504 | onColorPicked = colorPickedCallback, 505 | recentlyUsedColors = recentlyUsedColors, 506 | ) 507 | } 508 | item { 509 | ThemeColorPicker( 510 | colorName = "inverseOnSurface", 511 | currentColor = MaterialTheme.colorScheme.inverseOnSurface, 512 | onColorPicked = colorPickedCallback, 513 | recentlyUsedColors = recentlyUsedColors, 514 | ) 515 | } 516 | item { 517 | ThemeColorPicker( 518 | colorName = "error", 519 | currentColor = MaterialTheme.colorScheme.error, 520 | onColorPicked = colorPickedCallback, 521 | recentlyUsedColors = recentlyUsedColors, 522 | ) 523 | } 524 | item { 525 | ThemeColorPicker( 526 | colorName = "onError", 527 | currentColor = MaterialTheme.colorScheme.onError, 528 | onColorPicked = colorPickedCallback, 529 | recentlyUsedColors = recentlyUsedColors, 530 | ) 531 | } 532 | item { 533 | ThemeColorPicker( 534 | colorName = "errorContainer", 535 | currentColor = MaterialTheme.colorScheme.errorContainer, 536 | onColorPicked = colorPickedCallback, 537 | recentlyUsedColors = recentlyUsedColors, 538 | ) 539 | } 540 | item { 541 | ThemeColorPicker( 542 | colorName = "onErrorContainer", 543 | currentColor = MaterialTheme.colorScheme.onErrorContainer, 544 | onColorPicked = colorPickedCallback, 545 | recentlyUsedColors = recentlyUsedColors, 546 | ) 547 | } 548 | item { 549 | ThemeColorPicker( 550 | colorName = "outline", 551 | currentColor = MaterialTheme.colorScheme.outline, 552 | onColorPicked = colorPickedCallback, 553 | recentlyUsedColors = recentlyUsedColors, 554 | ) 555 | } 556 | } 557 | } 558 | --------------------------------------------------------------------------------