diff --git a/.detekt-compose.yml b/.detekt-compose.yml index eddd7e6062..54a8a82a46 100644 --- a/.detekt-compose.yml +++ b/.detekt-compose.yml @@ -9,6 +9,7 @@ Compose: active: true ModifierMissing: active: true + excludes: ['**/*.figma.kt'] ModifierNaming: active: true ModifierNotUsedAtRoot: diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 46b6a56123..c6254dadc5 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -2,9 +2,16 @@ name: Build on: workflow_call: + inputs: + figma_enabled: + required: false + type: boolean + default: false secrets: GH_APP_PRIVATE_KEY: required: true + FIGMA_ACCESS_TOKEN: + required: false defaults: @@ -105,6 +112,32 @@ jobs: - name: Unit Tests run: ./gradlew test${{ env.flavour }}${{ env.config }}UnitTest -PdisablePreDex + - name: Check for Code Connect changes + id: code-connect-filter + if: ${{ inputs.figma_enabled }} + uses: dorny/paths-filter@v3 + with: + token: ${{ steps.app-token.outputs.token }} + filters: | + code_connect: + - 'app/src/main/java/net/skyscanner/backpack/demo/figma/**' + - 'figma.config.json' + + - name: Install Figma Code Connect + if: ${{ inputs.figma_enabled && steps.code-connect-filter.outputs.code_connect == 'true' }} + continue-on-error: true + run: npm install -g @figma/code-connect@1.3.11 + + - name: Validate Code Connect files + if: ${{ inputs.figma_enabled && steps.code-connect-filter.outputs.code_connect == 'true' }} + continue-on-error: true + run: | + echo "🔍 Found Code Connect files, validating..." + npx figma connect parse + npx figma connect publish --dry-run + env: + FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }} + Android: name: Android tests runs-on: ubuntu-24.04-16cores-public diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dae569fd97..03fb4a9ca6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,8 +21,11 @@ jobs: pull-requests: write contents: write uses: ./.github/workflows/_build.yml + with: + figma_enabled: true secrets: GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} + FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }} Dependabot: runs-on: ubuntu-24.04-16cores-public diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d70747f212..79bb6b6213 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,8 +20,11 @@ jobs: pull-requests: write contents: write uses: ./.github/workflows/_build.yml + with: + figma_enabled: true secrets: GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} + FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }} Deploy: name: Deploy @@ -81,6 +84,38 @@ jobs: - name: Update Supernova docs run: npx @supernovaio/cli publish-documentation --apiKey=${{ secrets.SUPERNOVA_API_KEY }} --designSystemId=${{ secrets.SUPERNOVA_DESIGN_SYSTEM_ID }} + - name: Check for Code Connect changes + id: code-connect-filter + run: | + # Get the previous tag + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + if [ -z "$PREVIOUS_TAG" ]; then + echo "No previous tag found, will publish Code Connect" + echo "code_connect_changed=true" >> $GITHUB_OUTPUT + else + # Check if any Code Connect files changed since the previous tag + CHANGED=$(git diff --name-only $PREVIOUS_TAG HEAD -- 'app/src/main/java/net/skyscanner/backpack/demo/figma/' 'figma.config.json' | wc -l) + if [ "$CHANGED" -gt 0 ]; then + echo "Code Connect files changed since $PREVIOUS_TAG" + echo "code_connect_changed=true" >> $GITHUB_OUTPUT + else + echo "No Code Connect files changed since $PREVIOUS_TAG" + echo "code_connect_changed=false" >> $GITHUB_OUTPUT + fi + fi + + - name: Install Figma Code Connect + if: steps.code-connect-filter.outputs.code_connect_changed == 'true' + continue-on-error: true + run: npm install -g @figma/code-connect@1.3.11 + + - name: Publish Code Connect to Figma + if: steps.code-connect-filter.outputs.code_connect_changed == 'true' + continue-on-error: true + run: npx figma connect publish --exit-on-unreadable-files + env: + FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }} + - name: Publish release to Runway run: | set -e @@ -89,3 +124,4 @@ jobs: RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} RUNWAY_APP_ID: ${{ secrets.RUNWAY_APP_ID }} RUNWAY_BUCKET_ID: ${{ secrets.RUNWAY_BUCKET_ID }} + diff --git a/.gitignore b/.gitignore index 92ccd93745..1c0d972c89 100644 --- a/.gitignore +++ b/.gitignore @@ -247,3 +247,4 @@ backpack-compose/src/main/res/raw/compose_components.txt /.idea/copilot.data.migration.ask2agent.xml /.idea/copilot.data.migration.edit.xml /.idea/markdown.xml +/tmp/ diff --git a/app/build.gradle b/app/build.gradle index ea80b222f2..f858e282d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,6 +18,7 @@ plugins { alias(libs.plugins.compose.compiler) + alias(libs.plugins.figma.code.connect) } apply plugin: 'com.android.application' @@ -109,6 +110,7 @@ dependencies { implementation libs.compose.activity implementation libs.kotlin.reflection implementation libs.destinations.core + implementation libs.figma.code.connect testImplementation libs.test.junit testImplementation libs.roborazzi.compose testImplementation libs.roborazzi.core diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/button/BpkButton.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/button/BpkButton.figma.kt new file mode 100644 index 0000000000..6abc7c639e --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/button/BpkButton.figma.kt @@ -0,0 +1,261 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.button + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import com.figma.code.connect.FigmaVariant +import net.skyscanner.backpack.compose.button.BpkButton +import net.skyscanner.backpack.compose.button.BpkButtonIconPosition +import net.skyscanner.backpack.compose.button.BpkButtonSize +import net.skyscanner.backpack.compose.button.BpkButtonType +import net.skyscanner.backpack.compose.icon.BpkIcon +import net.skyscanner.backpack.compose.tokens.ArrowRight + +@FigmaConnect("FIGMA_BUTTON") +@FigmaVariant(key = "Icon", value = "None") +class BpkButtonCodeConnect { + + @FigmaProperty(FigmaType.Enum, "Style") + val type: BpkButtonType = Figma.mapping( + "Primary" to BpkButtonType.Primary, + "Secondary" to BpkButtonType.Secondary, + "Featured" to BpkButtonType.Featured, + "Destructive" to BpkButtonType.Destructive, + "Primary on Dark" to BpkButtonType.PrimaryOnDark, + "Primary on Light" to BpkButtonType.PrimaryOnLight, + "Secondary on Dark" to BpkButtonType.SecondaryOnDark, + "Link" to BpkButtonType.Link, + "Link on Dark" to BpkButtonType.LinkOnDark, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val enabled: Boolean = Figma.mapping( + "Normal" to true, + "Disabled" to false, + "Loading" to true, + "Pressed" to true, + "Focused" to true, + "Hover" to true, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val loading: Boolean = Figma.mapping( + "Normal" to false, + "Disabled" to false, + "Loading" to true, + "Pressed" to false, + "Focused" to false, + "Hover" to false, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkButtonSize = Figma.mapping( + "Default" to BpkButtonSize.Default, + "Large" to BpkButtonSize.Large, + ) + + @Composable + fun ComponentExample() { + BpkButton( + text = "Label", + type = type, + size = size, + enabled = enabled, + loading = loading, + onClick = { }, + ) + } +} + +@FigmaConnect("FIGMA_BUTTON") +@FigmaVariant(key = "Icon", value = "Left") +class BpkButtonLeftIconCodeConnect { + + @FigmaProperty(FigmaType.Enum, "Style") + val type: BpkButtonType = Figma.mapping( + "Primary" to BpkButtonType.Primary, + "Secondary" to BpkButtonType.Secondary, + "Featured" to BpkButtonType.Featured, + "Destructive" to BpkButtonType.Destructive, + "Primary on Dark" to BpkButtonType.PrimaryOnDark, + "Primary on Light" to BpkButtonType.PrimaryOnLight, + "Secondary on Dark" to BpkButtonType.SecondaryOnDark, + "Link" to BpkButtonType.Link, + "Link on Dark" to BpkButtonType.LinkOnDark, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val enabled: Boolean = Figma.mapping( + "Normal" to true, + "Disabled" to false, + "Loading" to true, + "Pressed" to true, + "Focused" to true, + "Hover" to true, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val loading: Boolean = Figma.mapping( + "Normal" to false, + "Disabled" to false, + "Loading" to true, + "Pressed" to false, + "Focused" to false, + "Hover" to false, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkButtonSize = Figma.mapping( + "Default" to BpkButtonSize.Default, + "Large" to BpkButtonSize.Large, + ) + + @Composable + fun ComponentExample() { + BpkButton( + text = "Label", + type = type, + size = size, + enabled = enabled, + loading = loading, + icon = BpkIcon.ArrowRight, + position = BpkButtonIconPosition.Start, + onClick = { }, + ) + } +} + +@FigmaConnect("FIGMA_BUTTON") +@FigmaVariant(key = "Icon", value = "Right") +class BpkButtonRightIconCodeConnect { + + @FigmaProperty(FigmaType.Enum, "Style") + val type: BpkButtonType = Figma.mapping( + "Primary" to BpkButtonType.Primary, + "Secondary" to BpkButtonType.Secondary, + "Featured" to BpkButtonType.Featured, + "Destructive" to BpkButtonType.Destructive, + "Primary on Dark" to BpkButtonType.PrimaryOnDark, + "Primary on Light" to BpkButtonType.PrimaryOnLight, + "Secondary on Dark" to BpkButtonType.SecondaryOnDark, + "Link" to BpkButtonType.Link, + "Link on Dark" to BpkButtonType.LinkOnDark, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val enabled: Boolean = Figma.mapping( + "Normal" to true, + "Disabled" to false, + "Loading" to true, + "Pressed" to true, + "Focused" to true, + "Hover" to true, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val loading: Boolean = Figma.mapping( + "Normal" to false, + "Disabled" to false, + "Loading" to true, + "Pressed" to false, + "Focused" to false, + "Hover" to false, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkButtonSize = Figma.mapping( + "Default" to BpkButtonSize.Default, + "Large" to BpkButtonSize.Large, + ) + + @Composable + fun ComponentExample() { + BpkButton( + text = "Label", + type = type, + size = size, + enabled = enabled, + loading = loading, + icon = BpkIcon.ArrowRight, + position = BpkButtonIconPosition.End, + onClick = { }, + ) + } +} + +@FigmaConnect("FIGMA_BUTTON") +@FigmaVariant(key = "Icon", value = "Icon only") +class BpkButtonIconOnlyCodeConnect { + + @FigmaProperty(FigmaType.Enum, "Style") + val type: BpkButtonType = Figma.mapping( + "Primary" to BpkButtonType.Primary, + "Secondary" to BpkButtonType.Secondary, + "Featured" to BpkButtonType.Featured, + "Destructive" to BpkButtonType.Destructive, + "Primary on Dark" to BpkButtonType.PrimaryOnDark, + "Primary on Light" to BpkButtonType.PrimaryOnLight, + "Secondary on Dark" to BpkButtonType.SecondaryOnDark, + "Link" to BpkButtonType.Link, + "Link on Dark" to BpkButtonType.LinkOnDark, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val enabled: Boolean = Figma.mapping( + "Normal" to true, + "Disabled" to false, + "Loading" to true, + "Pressed" to true, + "Focused" to true, + "Hover" to true, + ) + + @FigmaProperty(FigmaType.Enum, "State") + val loading: Boolean = Figma.mapping( + "Normal" to false, + "Disabled" to false, + "Loading" to true, + "Pressed" to false, + "Focused" to false, + "Hover" to false, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkButtonSize = Figma.mapping( + "Default" to BpkButtonSize.Default, + "Large" to BpkButtonSize.Large, + ) + + @Composable + fun ComponentExample() { + BpkButton( + icon = BpkIcon.ArrowRight, + contentDescription = "Label", + type = type, + size = size, + enabled = enabled, + loading = loading, + onClick = { }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/card/BpkCard.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/card/BpkCard.figma.kt new file mode 100644 index 0000000000..924f05bbf0 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/card/BpkCard.figma.kt @@ -0,0 +1,42 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.card + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import net.skyscanner.backpack.compose.card.BpkCard +import net.skyscanner.backpack.compose.text.BpkText +import net.skyscanner.backpack.compose.theme.BpkTheme + +@FigmaConnect("FIGMA_CARD") +public class BpkCardDoc { + @Composable + public fun ComponentExample() { + BpkCard { + Column() { + // Example content inside the card + BpkText( + text = "Card content", + style = BpkTheme.typography.bodyDefault, + ) + } + } + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/cardlistrail/BpkRailCardList.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/cardlistrail/BpkRailCardList.figma.kt new file mode 100644 index 0000000000..42913c23a1 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/cardlistrail/BpkRailCardList.figma.kt @@ -0,0 +1,39 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.skyscanner.backpack.demo.figma.cardlistrail + +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaVariant +import net.skyscanner.backpack.compose.cardlist.rail.BpkRailCardList + +@FigmaConnect("FIGMA_LIST_CARD") +@FigmaVariant("Style", "Rail") +public class BpkRailCardListDoc { + + @Composable + public fun ComponentExample() { + BpkRailCardList( + title = "Title", + totalCards = 5, // this is just an example, number of cards is dynamic + content = { index -> + // pass your content composables here + }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/cardlistrail/BpkStackCardList.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/cardlistrail/BpkStackCardList.figma.kt new file mode 100644 index 0000000000..a6138faa4a --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/cardlistrail/BpkStackCardList.figma.kt @@ -0,0 +1,39 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.skyscanner.backpack.demo.figma.cardlistrail + +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaVariant +import net.skyscanner.backpack.compose.cardlist.stack.BpkStackCardList + +@FigmaConnect("FIGMA_LIST_CARD") +@FigmaVariant("Style", "Stack") +public class BpkStackCardListDoc { + + @Composable + public fun ComponentExample() { + BpkStackCardList( + title = "Title", + totalCount = 5, // this is just an example, number of cards is dynamic + content = { index -> + // pass your content composables here + }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/cardwrapper/BpkCardWrapper.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/cardwrapper/BpkCardWrapper.figma.kt new file mode 100644 index 0000000000..112b195ed5 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/cardwrapper/BpkCardWrapper.figma.kt @@ -0,0 +1,49 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.cardwrapper + +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import net.skyscanner.backpack.compose.card.BpkCardCorner +import net.skyscanner.backpack.compose.card.BpkCardElevation +import net.skyscanner.backpack.compose.card.BpkCardPadding +import net.skyscanner.backpack.compose.card.BpkCardStyle +import net.skyscanner.backpack.compose.cardwrapper.BpkCardWrapper +import net.skyscanner.backpack.compose.theme.BpkTheme + +@FigmaConnect("FIGMA_WRAPPER_CARD") +public class BpkCardWrapperDoc { + @Composable + public fun ComponentExample() { + BpkCardWrapper( + backgroundColor = BpkTheme.colors.coreAccent, + headerContent = { + // Header content goes here + }, + cardContent = { + // Card content goes here + }, + cardPadding = BpkCardPadding.None, // example padding + corner = BpkCardCorner.Small, // example corner + cardStyle = BpkCardStyle.onDefault, // example style + elevation = BpkCardElevation.None, // example elevation + + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/carousel/BpkCarousel.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/carousel/BpkCarousel.figma.kt new file mode 100644 index 0000000000..da487a8558 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/carousel/BpkCarousel.figma.kt @@ -0,0 +1,47 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.carousel + +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.figma.code.connect.FigmaConnect +import net.skyscanner.backpack.compose.carousel.BpkCarousel +import net.skyscanner.backpack.compose.carousel.rememberBpkCarouselState +import net.skyscanner.backpack.compose.tokens.BpkSpacing + +@FigmaConnect("FIGMA_CAROUSEL") +public class BpkCarouselDoc { + @Composable + public fun ComponentExample() { + val pagerState = rememberBpkCarouselState( + totalImages = 5, // number of images in the carousel + initialImage = 0, // starting image index + ) + BpkCarousel( + modifier = Modifier + .aspectRatio(1.9f) + .padding(vertical = BpkSpacing.Base), + state = pagerState, + ) { + // content goes here, could be glide image, painterResource, depending on source of images + } + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/chipgroup/BpkSingleSelectChipGroup.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/chipgroup/BpkSingleSelectChipGroup.figma.kt new file mode 100644 index 0000000000..3f0b1b90c2 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/chipgroup/BpkSingleSelectChipGroup.figma.kt @@ -0,0 +1,55 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.skyscanner.backpack.demo.figma.chipgroup +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import com.figma.code.connect.FigmaVariant +import net.skyscanner.backpack.compose.chip.BpkChipStyle +import net.skyscanner.backpack.compose.chipgroup.single.BpkSingleChipGroupType +import net.skyscanner.backpack.compose.chipgroup.single.BpkSingleChipItem +import net.skyscanner.backpack.compose.chipgroup.single.BpkSingleSelectChipGroup + +@FigmaConnect("FIGMA_GROUP_CHIPS") +@FigmaVariant(key = "Breakpoint", value = "Mobile") +public class BpkSingleSelectChipGroupDoc { + @FigmaProperty(FigmaType.Text, "Label") + public val label: String = "Filter by" + + @FigmaProperty(FigmaType.Enum, "Style") + public val style: BpkChipStyle = Figma.mapping( + "Default" to BpkChipStyle.Default, + "On Dark" to BpkChipStyle.OnDark, + "On Image" to BpkChipStyle.OnImage, + ) + + @Composable + public fun ComponentExample() { + BpkSingleSelectChipGroup( + chips = listOf(BpkSingleChipItem("London"), BpkSingleChipItem("Berlin")), + selectedIndex = 0, // Index of the selected chip + onItemClicked = {}, + style = style, + type = BpkSingleChipGroupType.Rail, + behaviouralEventWrapper = null, + + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/dividedcard/BpkDividedCard.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/dividedcard/BpkDividedCard.figma.kt new file mode 100644 index 0000000000..b39b41dc28 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/dividedcard/BpkDividedCard.figma.kt @@ -0,0 +1,43 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.dividedcard + +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaVariant +import net.skyscanner.backpack.compose.dividedcard.BpkDividedCard + +@FigmaConnect("FIGMA_DIVIDED_CARD") +@FigmaVariant(key = "Size", value = "Mobile") +public class BpkDividedCardDoc { + + @Composable + public fun ComponentExample() { + BpkDividedCard( + primaryContent = { + // Primary content goes here + }, + secondaryContent = { + // Secondary content goes here + }, + onClick = {}, + + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/divider/BpkDivider.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/divider/BpkDivider.figma.kt new file mode 100644 index 0000000000..b4cfabc642 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/divider/BpkDivider.figma.kt @@ -0,0 +1,31 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.divider + +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import net.skyscanner.backpack.compose.divider.BpkDivider + +@FigmaConnect("FIGMA_DIVIDER") +public class BpkDividerDoc { + @Composable + public fun ComponentExample() { + BpkDivider() + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/graphicpromo/BpkGraphicPromo.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/graphicpromo/BpkGraphicPromo.figma.kt new file mode 100644 index 0000000000..2503475b4a --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/graphicpromo/BpkGraphicPromo.figma.kt @@ -0,0 +1,79 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.graphicpromo + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import com.figma.code.connect.FigmaVariant +import net.skyscanner.backpack.compose.graphicpromotion.BpkGraphicPromo +import net.skyscanner.backpack.compose.graphicpromotion.BpkGraphicPromoVerticalAlignment +import net.skyscanner.backpack.compose.graphicpromotion.BpkGraphicsPromoSponsor +import net.skyscanner.backpack.compose.overlay.BpkOverlayType + +@FigmaConnect("FIGMA_GRAPHIC_PROMO") +@FigmaVariant(key = "Size", value = "Mobile") +public class BpkGraphicPromoDoc { + @FigmaProperty(FigmaType.Boolean, "Overlay?") + public val overlay: BpkOverlayType? = Figma.mapping( + true to BpkOverlayType.BottomLow, // Example overlay type + false to null, + ) + + @FigmaProperty(FigmaType.Boolean, "Kicker?") + public val kicker: String? = Figma.mapping( + true to "Kicker", + false to null, + ) + + @FigmaProperty(FigmaType.Enum, "Type") + public val sponsored: BpkGraphicsPromoSponsor? = Figma.mapping( + "Default" to null, + "Sponsored" to BpkGraphicsPromoSponsor( + title = "Sponsored by Skyscanner", + logo = "https://image.skyscanner.net/test-logo.png", + accessibilityLabel = "Skyscanner logo", + ), + ) + + @FigmaProperty(FigmaType.Enum, "Alignment") + public val alignment: BpkGraphicPromoVerticalAlignment = Figma.mapping( + "Top" to BpkGraphicPromoVerticalAlignment.Top, + "Bottom" to BpkGraphicPromoVerticalAlignment.Bottom, + ) + + @Composable + public fun ComponentExample() { + BpkGraphicPromo( + headline = "Three Peaks Challenge", + image = { + // Placeholder for image content + }, + kicker = kicker, + subHeadline = "How to complete the climb in 3 days", + overlayType = overlay, + verticalAlignment = alignment, + sponsor = sponsored, + tapAction = { /* Handle tap action */ }, + + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/link/BpkLink.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/link/BpkLink.figma.kt new file mode 100644 index 0000000000..a97fca923c --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/link/BpkLink.figma.kt @@ -0,0 +1,49 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.link + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.link.BpkLink +import net.skyscanner.backpack.compose.link.BpkLinkStyle +import kotlin.String + +@FigmaConnect("FIGMA_LINK") +public class BpkLinkDoc { + @FigmaProperty(FigmaType.Text, "Text") + public val text: String = "Link" + + @FigmaProperty(FigmaType.Enum, "Style") + public val style: BpkLinkStyle = Figma.mapping( + "Default" to BpkLinkStyle.Default, + "On Contrast" to BpkLinkStyle.OnContrast, + ) + + @Composable + public fun ComponentExample() { + BpkLink( + text = text, + style = style, + onLinkClicked = { /* Handle link click */ }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/panel/BpkPanel.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/panel/BpkPanel.figma.kt new file mode 100644 index 0000000000..3fdaa60a2d --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/panel/BpkPanel.figma.kt @@ -0,0 +1,49 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.panel + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.panel.BpkPanel +import net.skyscanner.backpack.compose.panel.BpkPanelPadding + +@FigmaConnect("FIGMA_PANEL") +public class BpkPanelDoc { + @FigmaProperty(FigmaType.Enum, "Keyline?") + public val propagateMinConstraints: Boolean = Figma.mapping( + "Yes" to true, + "No" to false, + ) + + @Composable + public fun ComponentExample() { + BpkPanel( + propagateMinConstraints = propagateMinConstraints, // example usage of the property + padding = BpkPanelPadding.Base, // example padding + contentAlignment = Alignment.TopStart, // example alignment + content = { + // Content goes here + }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/price/BpkPrice.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/price/BpkPrice.figma.kt new file mode 100644 index 0000000000..bc20069f01 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/price/BpkPrice.figma.kt @@ -0,0 +1,75 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.price + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.icon.BpkIcon +import net.skyscanner.backpack.compose.price.BpkPrice +import net.skyscanner.backpack.compose.price.BpkPriceAlign +import net.skyscanner.backpack.compose.price.BpkPriceSize +import net.skyscanner.backpack.compose.tokens.Share + +@FigmaConnect("FIGMA_PRICE") +class BpkPriceDoc { + + @FigmaProperty(FigmaType.Text, "Price") + val price: String = "£1,830" + + @FigmaProperty(FigmaType.Text, "Trailing text") + val trailingText: String = "per day" + + @FigmaProperty(FigmaType.Text, "Previous price") + val previousPrice: String = "£2,033" + + @FigmaProperty(FigmaType.Boolean, "Icon?") + val icon: BpkIcon? = Figma.mapping( + true to BpkIcon.Share, + false to null, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkPriceSize = Figma.mapping( + "Large" to BpkPriceSize.Large, + "Small" to BpkPriceSize.Small, + "X-Small" to BpkPriceSize.ExtraSmall, + ) + + @FigmaProperty(FigmaType.Enum, "Alignment") + val align: BpkPriceAlign = Figma.mapping( + "Left" to BpkPriceAlign.Start, + "Right" to BpkPriceAlign.End, + ) + + @Composable + fun PriceExample() { + BpkPrice( + price = price, + size = size, + align = align, + icon = icon, + leadingText = "App only deal", + previousPrice = previousPrice, + trailingText = trailingText, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/rating/BpkRating.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/rating/BpkRating.figma.kt new file mode 100644 index 0000000000..6a386ea909 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/rating/BpkRating.figma.kt @@ -0,0 +1,53 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.rating + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.rating.BpkRating +import net.skyscanner.backpack.compose.rating.BpkRatingSize + +@FigmaConnect("FIGMA_RATING") +class BpkRatingDoc { + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkRatingSize = Figma.mapping( + "Default" to BpkRatingSize.Base, + "Large" to BpkRatingSize.Large, + ) + + @FigmaProperty(FigmaType.Enum, "Arrangement") + val subtitle: String? = Figma.mapping( + "Title only" to null, + "Title • Subtitle" to "1,532 reviews", + ) + + @Composable + fun RatingExample() { + BpkRating( + title = "Excellent", + value = 4.5f, + size = size, + subtitle = subtitle, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/savebutton/BpkSaveButton.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/savebutton/BpkSaveButton.figma.kt new file mode 100644 index 0000000000..a8f2b30060 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/savebutton/BpkSaveButton.figma.kt @@ -0,0 +1,65 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.savebutton + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.cardbutton.BpkCardButtonSize +import net.skyscanner.backpack.compose.cardbutton.BpkCardButtonStyle +import net.skyscanner.backpack.compose.cardbutton.BpkSaveButton + +@FigmaConnect("FIGMA_SAVE_BUTTON") +public class BpkSaveButtonDoc { + @FigmaProperty(FigmaType.Enum, "Style") + public val style: BpkCardButtonStyle = Figma.mapping( + "Default" to BpkCardButtonStyle.Default, + "Contained" to BpkCardButtonStyle.Contained, + "On Dark" to BpkCardButtonStyle.OnDark, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + public val size: BpkCardButtonSize = Figma.mapping( + "Default" to BpkCardButtonSize.Default, + "Small" to BpkCardButtonSize.Small, + ) + + @FigmaProperty(FigmaType.Enum, "State") + public val checked: Boolean = Figma.mapping( + "Default" to false, + "Unsaved" to false, + "Saved" to true, + "Transition" to true, + "Hover" to false, + ) + + @Composable + public fun ComponentExample() { + BpkSaveButton( + checked = checked, + contentDescription = "Save", // Example content description for accessibility + onCheckedChange = {}, + size = size, + style = style, + + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/sharebutton/BpkShareButton.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/sharebutton/BpkShareButton.figma.kt new file mode 100644 index 0000000000..41cf39b6e5 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/sharebutton/BpkShareButton.figma.kt @@ -0,0 +1,54 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.sharebutton + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.cardbutton.BpkCardButtonSize +import net.skyscanner.backpack.compose.cardbutton.BpkCardButtonStyle +import net.skyscanner.backpack.compose.cardbutton.BpkShareButton + +@FigmaConnect("FIGMA_SHARE_BUTTON") +public class BpkSaveButtonDoc { + @FigmaProperty(FigmaType.Enum, "Style") + public val style: BpkCardButtonStyle = Figma.mapping( + "Default" to BpkCardButtonStyle.Default, + "Contained" to BpkCardButtonStyle.Contained, + "On Dark" to BpkCardButtonStyle.OnDark, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + public val size: BpkCardButtonSize = Figma.mapping( + "Default" to BpkCardButtonSize.Default, + "Small" to BpkCardButtonSize.Small, + ) + + @Composable + public fun ComponentExample() { + BpkShareButton( + contentDescription = "Share", // Example content description for accessibility + size = size, + style = style, + onClick = {}, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkHotelRating.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkHotelRating.figma.kt new file mode 100644 index 0000000000..587297e80b --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkHotelRating.figma.kt @@ -0,0 +1,56 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.starrating + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.starrating.BpkHotelRating +import net.skyscanner.backpack.compose.starrating.BpkStarRatingSize + +@FigmaConnect("FIGMA_HOTEL_STAR_RATING") +class BpkHotelRatingDoc { + + @FigmaProperty(FigmaType.Enum, "Rating") + val rating: Int = Figma.mapping( + "1 star" to 1, + "2 star" to 2, + "3 star" to 3, + "4 star" to 4, + "5 star" to 5, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkStarRatingSize = Figma.mapping( + "Large" to BpkStarRatingSize.Large, + "Small" to BpkStarRatingSize.Small, + "Extra-large" to BpkStarRatingSize.Large, + ) + + @Composable + fun ComponentExample() { + BpkHotelRating( + rating = rating, + size = size, + contentDescription = { _, _ -> "$rating stars" }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkInteractiveStarRating.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkInteractiveStarRating.figma.kt new file mode 100644 index 0000000000..9857265bb7 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkInteractiveStarRating.figma.kt @@ -0,0 +1,57 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.starrating + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.starrating.BpkInteractiveStarRating +import net.skyscanner.backpack.compose.starrating.BpkStarRatingSize + +@FigmaConnect("FIGMA_INTERACTIVE_STAR_RATING") +class BpkInteractiveStarRatingDoc { + + @FigmaProperty(FigmaType.Enum, "Rating") + val rating: Int = Figma.mapping( + "1 star" to 1, + "2 stars" to 2, + "3 stars" to 3, + "4 stars" to 4, + "5 stars" to 5, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkStarRatingSize = Figma.mapping( + "Large" to BpkStarRatingSize.Large, + "Small" to BpkStarRatingSize.Small, + "Extra-large" to BpkStarRatingSize.Large, + ) + + @Composable + fun ComponentExample() { + BpkInteractiveStarRating( + rating = rating, + size = size, + onRatingChanged = { /* When rating changes */ }, + contentDescription = { _, _ -> "$rating stars" }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkStarRating.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkStarRating.figma.kt new file mode 100644 index 0000000000..98fcae833c --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/starrating/BpkStarRating.figma.kt @@ -0,0 +1,58 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.starrating + +import androidx.compose.runtime.Composable +import com.figma.code.connect.Figma +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import net.skyscanner.backpack.compose.starrating.BpkRatingRounding +import net.skyscanner.backpack.compose.starrating.BpkStarRating +import net.skyscanner.backpack.compose.starrating.BpkStarRatingSize + +@FigmaConnect("FIGMA_STATIC_STAR_RATING") +class BpkStarRatingDoc { + + @FigmaProperty(FigmaType.Enum, "Rating") + val rating: Float = Figma.mapping( + "1 star" to 1f, + "2 stars" to 2f, + "3 stars" to 3f, + "4 stars" to 4f, + "5 stars" to 5f, + ) + + @FigmaProperty(FigmaType.Enum, "Size") + val size: BpkStarRatingSize = Figma.mapping( + "Large" to BpkStarRatingSize.Large, + "Small" to BpkStarRatingSize.Small, + "Extra-large" to BpkStarRatingSize.Large, + ) + + @Composable + fun ComponentExample() { + BpkStarRating( + rating = rating, + size = size, + rounding = BpkRatingRounding.Nearest, + contentDescription = { _, _ -> "$rating stars" }, + ) + } +} diff --git a/app/src/main/java/net/skyscanner/backpack/demo/figma/text/BpkText.figma.kt b/app/src/main/java/net/skyscanner/backpack/demo/figma/text/BpkText.figma.kt new file mode 100644 index 0000000000..b79e736ce3 --- /dev/null +++ b/app/src/main/java/net/skyscanner/backpack/demo/figma/text/BpkText.figma.kt @@ -0,0 +1,297 @@ +/* + * Backpack for Android - Skyscanner's Design System + * + * Copyright 2018 - 2025 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.skyscanner.backpack.demo.figma.text + +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType +import com.figma.code.connect.FigmaVariant +import net.skyscanner.backpack.compose.text.BpkText +import net.skyscanner.backpack.compose.theme.BpkTheme + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Hero 1") +class BpkTextDoc { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.hero1, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Hero 2") +class BpkTextHero2CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.hero2, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Hero 3") +class BpkTextHero3CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.hero3, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Hero 4") +class BpkTextHero4CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.hero4, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Hero 5") +class BpkTextHero5CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.hero5, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Headline 1") +class BpkTextHeading1CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.heading1, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Headline 2") +class BpkTextHeading2CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.heading2, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Headline 3") +class BpkTextHeading3CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.heading3, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Headline 4") +class BpkTextHeading4CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.heading4, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Headline 5") +class BpkTextHeading5CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.heading5, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Subheading") +class BpkTextSubheadingCodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.subheading, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Body Default") +class BpkTextBodyDefaultCodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.bodyDefault, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Body Longform") +class BpkTextBodyLongformCodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.bodyLongform, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Body Footnote") +class BpkTextFootnoteCodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.footnote, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Body Caption") +class BpkTextCaptionCodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.caption, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Label 1") +class BpkTextLabel1CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.label1, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Label 2") +class BpkTextLabel2CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.label2, + ) + } +} + +@FigmaConnect("FIGMA_TEXT") +@FigmaVariant(key = "Style", value = "Label 3") +class BpkTextLabel3CodeConnect { + @FigmaProperty(FigmaType.Text, "Text Prop") + val text: String = "Sample text" + + @Composable + fun ComponentExample() { + BpkText( + text = text, + style = BpkTheme.typography.label3, + ) + } +} diff --git a/backpack-compose/build.gradle.kts b/backpack-compose/build.gradle.kts index 9be51daf2d..8027e54ef8 100644 --- a/backpack-compose/build.gradle.kts +++ b/backpack-compose/build.gradle.kts @@ -70,7 +70,6 @@ dependencies { implementation(libs.androidx.lifecycleViewmodel) implementation(libs.androidx.lifecycleViewmodelKtx) implementation(libs.androidx.coreKts) - androidTestImplementation(libs.test.junitAndroid) androidTestImplementation(libs.test.rules) androidTestImplementation(libs.test.mockitoKotlin) diff --git a/figma-code-connect/README.md b/figma-code-connect/README.md new file mode 100644 index 0000000000..0b43dd2924 --- /dev/null +++ b/figma-code-connect/README.md @@ -0,0 +1,146 @@ +# Figma Code Connect + +Show real Android code snippets in Figma Dev Mode for Backpack components. + +## Setup (One-time) + +**1. Get a Figma token** +- Go to https://www.figma.com/settings → Personal Access Tokens → Create new token + +**2. Set the token** +```bash +export FIGMA_ACCESS_TOKEN="your_token" +``` +Add this to your `.zshrc` or `.bashrc` to persist it. + +**3. Verify it works** +```bash +npx figma connect publish --dry-run +``` + +## Add Code Connect to a Component + +**1. Create the file** + +Create `ComponentName.figma.kt` in `app/src/main/java/net/skyscanner/backpack/demo/figma/componentname/` + +**2. Get the Figma node URL** + +In Figma: Right-click component → Copy link + +**3. Write the code** + +```kotlin +package net.skyscanner.backpack.demo.figma.componentname + +import androidx.compose.runtime.Composable +import com.figma.code.connect.FigmaConnect +import com.figma.code.connect.FigmaProperty +import com.figma.code.connect.FigmaType + +@FigmaConnect(url = "https://www.figma.com/design/xxx?node-id=xxx") +class BpkComponentCodeConnect { + + @FigmaProperty(FigmaType.Text, "Label") // Must match Figma property name exactly + val text: String = "Default" + + @Composable + fun Example() { + BpkComponent(text = text) + } +} +``` + +**4. Test it** +```bash +npx figma connect publish --dry-run +``` + +**5. Publish it** +```bash +npx figma connect publish +``` + +## Property Mappings + +### Text +```kotlin +@FigmaProperty(FigmaType.Text, "Label") +val text: String = "Default" +``` + +### Boolean +```kotlin +@FigmaProperty(FigmaType.Boolean, "Enabled") +val enabled: Boolean = true +``` + +### Enum (dropdown in Figma) +```kotlin +@FigmaProperty(FigmaType.Enum, "Style") +val type: BpkButtonType = Figma.mapping( + "Primary" to BpkButtonType.Primary, + "Secondary" to BpkButtonType.Secondary, +) +``` + +### Multiple states from one property +```kotlin +// Same Figma property can map to different Kotlin properties +@FigmaProperty(FigmaType.Enum, "State") +val enabled: Boolean = Figma.mapping( + "Normal" to true, + "Disabled" to false, +) + +@FigmaProperty(FigmaType.Enum, "State") +val loading: Boolean = Figma.mapping( + "Normal" to false, + "Loading" to true, +) +``` + +## Handling Variants + +When a Figma component has variants (e.g., Button with Icon: None/Left/Right), create separate classes: + +```kotlin +@FigmaConnect(url = "https://...") +@FigmaVariant(key = "Icon", value = "None") +class BpkButtonCodeConnect { + @Composable + fun Example() { + BpkButton(text = "Label", onClick = { }) + } +} + +@FigmaConnect(url = "https://...") +@FigmaVariant(key = "Icon", value = "Left") +class BpkButtonLeftIconCodeConnect { + @Composable + fun Example() { + BpkButton( + text = "Label", + icon = BpkIcon.ArrowRight, + position = BpkButtonIconPosition.Start, + onClick = { }, + ) + } +} +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| "Property not found" | Open Figma Dev Mode, check exact property name (case-sensitive) | +| "Invalid enum value" | Check exact spelling of enum values in Figma | +| Parse errors | Simplify - use separate classes per variant | +| Auth failed | Check `echo $FIGMA_ACCESS_TOKEN` | + +## CI/CD + +- **PRs**: Automatically validated with `--dry-run` +- **Releases**: Automatically published to Figma + +Required secret: `FIGMA_ACCESS_TOKEN` diff --git a/figma.config.json b/figma.config.json new file mode 100644 index 0000000000..af2422722b --- /dev/null +++ b/figma.config.json @@ -0,0 +1,30 @@ +{ + "codeConnect": { + "parser": "compose", + "include": [ + "backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/**/*.kt", + "app/src/main/java/net/skyscanner/backpack/demo/figma/**/*.figma.kt" + ], + "documentUrlSubstitutions": { + "FIGMA_BUTTON": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-8677", + "FIGMA_TEXT": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10911-51565", + "FIGMA_CARD": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-49749", + "FIGMA_WRAPPER_CARD": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-49736", + "FIGMA_DIVIDED_CARD": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-49338", + "FIGMA_CAROUSEL": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-50288", + "FIGMA_DIVIDER": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-59665", + "FIGMA_GRAPHIC_PROMO": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10872-2409", + "FIGMA_LINK": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10885-9743", + "FIGMA_PANEL": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-49752", + "FIGMA_PRICE": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10911-19302", + "FIGMA_RATING": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10911-25511", + "FIGMA_HOTEL_STAR_RATING": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10911-49171", + "FIGMA_INTERACTIVE_STAR_RATING": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10911-49221", + "FIGMA_STATIC_STAR_RATING": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10911-49089", + "FIGMA_SAVE_BUTTON": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-50640", + "FIGMA_SHARE_BUTTON": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-50721", + "FIGMA_GROUP_CHIPS": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-56092", + "FIGMA_LIST_CARD": "https://www.figma.com/design/KXf2gHNLDe2cXWUoHl4cTX/Backpack%E2%80%A8Foundations---Components?node-id=10858-37205" + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad248979ba..f03ea02d41 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,10 @@ ksp = "2.3.3" # depends on Kotlin version compilerTesting = "0.11.1" destinations = "2.3.0" # update this alongside with compiler version +figmaCodeConnect = "1.2.6" +figmaCodeConnectLib = "1.1.3" +kotlinxSerialization = "1.7.3" + androidPlugin = "8.13.1" lint = "31.13.1" # android plugin, lint and sdk-common need to be updated together androidSdkCommon = "31.13.1" # if the Gradle plugin version is X.Y.Z, then the lint version is (X+23).Y.Z. @@ -20,6 +24,7 @@ roborazzi = "1.52.0" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +figma-code-connect = { id = "com.figma.code.connect", version.ref = "figmaCodeConnect" } [libraries] @@ -104,3 +109,7 @@ roborazzi-core = { module = "io.github.takahirom.roborazzi:roborazzi", version.r roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } roborazzi-junit = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } robolectric = "org.robolectric:robolectric:4.16" + +figma-code-connect = { group = "com.figma.code.connect", name = "code-connect-lib", version.ref = "figmaCodeConnectLib" } + +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }