Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Take your shot of Vitamin!

Gerard
March 23, 2023

Take your shot of Vitamin!

Decathlon has more than 160 frontend products, including 50 dedicated to mobile applications. Due to this context, it is hard to align the user interface across all these projects while respecting the platform.

Vitamin is a Design System developed by Decathlon as a product which can be adapted to any context and with multiple technical implementations for Android, iOS and Web. In theory, you can use this Design System in your application and customize it to fit your theme and your needs.

In this presentation, I'll focus on Vitamin Compose, the design and technical architecture, biases and what are the next steps.

Gerard

March 23, 2023
Tweet

More Decks by Gerard

Other Decks in Technology

Transcript

  1. @GerardPaligot
    TAKE YOUR SHOT OF VITAMIN!
    Cross platform Design System

    View full-size slide

  2. VITAMIN? CROSS DESIGN SYSTEM!

    View full-size slide

  3. IMPLEMENTATIONS
    2,026,487 Figma instances
    519 Editors
    4411 Viewers
    1,501,741 Npm downloads
    4 variants (CSS, Svelte, Vue, React)
    237 stars
    356,041 Maven downloads
    2 variants (XML, Compose)
    275 stars
    2 variants (UIKit, SwiftUI)
    39 stars

    View full-size slide

  4. ATOMIC DESIGN
    Atom Molecule Organism Template Pages

    View full-size slide

  5. TEAM ORGANISATION
    + Web

    View full-size slide

  6. COMPOSE IMPLEMENTATION

    View full-size slide

  7. Foundation
    Foundation Assets
    Foundation Icons
    Atoms
    COMPOSE ARCHITECTURE

    View full-size slide

  8. menus
    badges
    buttons
    buttons
    cards
    Molecules
    COMPOSE ARCHITECTURE
    Foundation
    Foundation Assets
    Foundation Icons

    View full-size slide

  9. appbars
    modals
    quantity-pickers
    ratings
    tabs
    Organisms
    COMPOSE ARCHITECTURE
    Foundation
    Foundation Assets
    Foundation Icons
    progressbars
    chips
    skeleton
    snackbars
    switches

    View full-size slide

  10. Artifact
    COMPOSE ARCHITECTURE
    Foundation
    Foundation Assets
    Foundation Icons
    modals vitamin
    quantity-pickers
    ratings
    tabs
    text-inputs
    progressbars
    chips
    skeleton
    snackbars
    switches

    View full-size slide

  11. COLOR PALETTE
    object VitaminPalette {
    val vtmnPurple50 = Color(242, 237, 242)
    val vtmnPurple100 = Color(220, 207, 221)
    val vtmnPurple200 = Color(172, 141, 175)
    val vtmnPurple300 = Color(150, 111, 154)
    val vtmnPurple400 = Color(108, 78, 111)
    val vtmnPurple500 = Color(91, 65, 93)
    val vtmnPurple600 = Color(73, 53, 75)
    val vtmnPurple700 = Color(44, 32, 45)
    !" same for blue, green, conifer, yellow,
    !" orange, red and grey
    }

    View full-size slide

  12. SEMANTIC COLORS
    val vtmnLightColors = VitaminColors(
    vtmnBackgroundPrimary = VitaminPalette.vtmnWhite,
    vtmnBackgroundSecondary = VitaminPalette.vtmnGrey50,
    vtmnBackgroundTertiary = VitaminPalette.vtmnGrey100,
    vtmnBackgroundBrandPrimary = VitaminPalette.vtmnBlue400,
    vtmnBackgroundBrandSecondary = VitaminPalette.vtmnBlue50,
    vtmnBackgroundAccent = VitaminPalette.vtmnYellow400,
    vtmnBackgroundDiscount = VitaminPalette.vtmnRed400,
    vtmnBackgroundPrimaryReversed = VitaminPalette.vtmnBlack,
    vtmnBackgroundBrandPrimaryReversed = VitaminPalette.vtmnWhite,
    !" same for contents, borders and decoratives
    )
    !" same for vtmnDarkColors

    View full-size slide

  13. FONT FAMILY
    private val robotoCondensed = FontFamily(
    Font(R.font.roboto_condensed_regular, FontWeight.Normal, FontStyle.Normal),
    Font(R.font.roboto_condensed_regularitalic, FontWeight.Normal, FontStyle.Italic),
    Font(R.font.roboto_condensed_bold, FontWeight.Bold, FontStyle.Normal),
    Font(R.font.roboto_condensed_bolditalic, FontWeight.Bold, FontStyle.Italic),
    Font(R.font.roboto_condensed_light, FontWeight.Light, FontStyle.Normal),
    Font(R.font.roboto_condensed_lightitalic, FontWeight.Light, FontStyle.Italic),
    )
    private val roboto = FontFamily(
    Font(R.font.roboto_regular, FontWeight.Normal, FontStyle.Normal),
    Font(R.font.roboto_italic, FontWeight.Normal, FontStyle.Italic),
    Font(R.font.roboto_bold, FontWeight.Bold, FontStyle.Normal),
    Font(R.font.roboto_bolditalic, FontWeight.Bold, FontStyle.Italic),
    Font(R.font.roboto_light, FontWeight.Light, FontStyle.Normal),
    Font(R.font.roboto_lightitalic, FontWeight.Light, FontStyle.Italic),
    )

    View full-size slide

  14. SEMANTIC TYPOGRAPHY
    val vtmnTypography = VitaminTypography(
    h1 = TextStyle(
    fontFamily = robotoCondensed,
    fontSize = 42.sp,
    fontWeight = FontWeight.W700,
    lineHeight = 44.sp
    ),
    !" h2, h3, h4, h5, h6, subtitle1, subtitle2
    text1 = TextStyle(
    fontFamily = roboto,
    fontSize = 17.sp,
    fontWeight = FontWeight.W400,
    lineHeight = 28.sp
    ),
    !" text2, text3, text1 bold, text2 bold and text3 bold
    button = TextStyle(
    fontFamily = roboto,
    fontSize = 16.sp,
    fontWeight = FontWeight.W700,
    lineHeight = 16.sp
    ),
    !" caption, overline
    )

    View full-size slide

  15. SEMANTIC SHAPES
    private val radius100 = 4.dp
    private val radius200 = 8.dp
    private val radius300 = 12.dp
    private val radius400 = 16.dp
    private val radius500 = 20.dp
    private val radius600 = 24.dp
    private val radius700 = 32.dp
    private val radius800 = 48.dp
    val vtmnShapes = VitaminShapes(
    radius100 = RoundedCornerShape(radius100),
    radius200 = RoundedCornerShape(radius200),
    radius300 = RoundedCornerShape(radius300),
    radius400 = RoundedCornerShape(radius400),
    radius500 = RoundedCornerShape(radius500),
    radius600 = RoundedCornerShape(radius600),
    radius700 = RoundedCornerShape(radius700),
    radius800 = RoundedCornerShape(radius800)
    )
    Radius 100
    Radius 200
    Radius 300
    Radius 400
    Radius 500
    Radius 600
    Radius 700
    Radius 800

    View full-size slide

  16. LOCALS
    internal val LocalVitaminColors = compositionLocalOf {
    error("No VitaminColorPalette provided")
    }
    internal val LocalVitaminTypographies = compositionLocalOf {
    error("No VitaminTypography provided")
    }
    internal val LocalVitaminShapes = compositionLocalOf {
    error("No VitaminShapes provided")
    }

    View full-size slide

  17. LOCALS
    internal val LocalVitaminColors = compositionLocalOf { !# !!$ !% }
    internal val LocalVitaminTypographies = compositionLocalOf { !# !!$ !% }
    internal val LocalVitaminShapes = compositionLocalOf { !" !!# !$ }
    object VitaminTheme {
    val colors: VitaminColors
    @Composable
    get() = LocalVitaminColors.current
    val typography: VitaminTypography
    @Composable
    get() = LocalVitaminTypographies.current
    val shapes: VitaminShapes
    @Composable
    get() = LocalVitaminShapes.current
    }

    View full-size slide

  18. LOCALS
    @Composable
    internal fun ProvideVitaminResources(
    colors: VitaminColors,
    typography: VitaminTypography,
    shapes: VitaminShapes,
    content: @Composable () !& Unit
    ) {
    val colors = remember { colors }
    colors.update(colors)
    CompositionLocalProvider(
    LocalVitaminColors provides colors,
    LocalVitaminTypographies provides typography,
    LocalVitaminShapes provides shapes
    ) {
    ProvideTextStyle(value = typography.text1, content = content)
    }
    }

    View full-size slide

  19. VITAMIN THEME
    @Composable
    internal fun ProvideVitaminResources(!# !!$ !%) { !# !!$ !% }
    @Composable
    fun VitaminTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () !& Unit
    ) {
    val colors = if (darkTheme) vtmnDarkColors else vtmnLightColors
    ProvideVitaminResources(colors, vtmnTypography, vtmnShapes) {
    MaterialTheme(
    colors = mdColors(darkTheme, colors),
    typography = mdTypography,
    shapes = shapes,
    content = content
    )
    }
    }

    View full-size slide

  20. TopAppBar
    Vitamin Specification

    View full-size slide

  21. MATERIAL 2
    @Composable
    fun TopAppBar(
    title: @Composable () !& Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable (() !& Unit)? = null,
    actions: @Composable RowScope.() !& Unit = {},
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: Dp = AppBarDefaults.TopAppBarElevation
    )

    View full-size slide

  22. MATERIAL 2
    @Composable
    fun TopAppBar(
    title: @Composable () !& Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable (() !& Unit)? = null,
    actions: @Composable RowScope.() !& Unit = {},
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: Dp = AppBarDefaults.TopAppBarElevation
    )

    View full-size slide

  23. MATERIAL 2
    @Composable
    fun TopAppBar(
    title: @Composable () !& Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable (() !& Unit)? = null,
    actions: @Composable RowScope.() !& Unit = {},
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: Dp = AppBarDefaults.TopAppBarElevation
    )

    View full-size slide

  24. VITAMIN
    object VitaminTopBars {
    private const val MAX_ACTIONS = 3
    @Composable
    fun Primary(
    title: String,
    modifier: Modifier = Modifier,
    maxActions: Int = MAX_ACTIONS,
    actions: List = emptyList(),
    expandedMenu: MutableState = remember { mutableStateOf(false) },
    colors: TopBarColors = VitaminTopBarColors.primary(),
    onDismissOverflowMenu: (() !& Unit)? = null,
    overflowIcon: (@Composable VitaminMenuIconButtons.() !& Unit)? = null,
    navigationIcon: (@Composable VitaminNavigationIconButtons.() !& Unit)? = null
    )
    }

    View full-size slide

  25. VITAMIN
    object VitaminTopBars {
    private const val MAX_ACTIONS = 3
    @Composable
    fun Primary(
    title: String,
    modifier: Modifier = Modifier,
    maxActions: Int = MAX_ACTIONS,
    actions: List = emptyList(),
    expandedMenu: MutableState = remember { mutableStateOf(false) },
    colors: TopBarColors = VitaminTopBarColors.primary(),
    onDismissOverflowMenu: (() !& Unit)? = null,
    overflowIcon: (@Composable VitaminMenuIconButtons.() !& Unit)? = null,
    navigationIcon: (@Composable VitaminNavigationIconButtons.() !& Unit)? = null
    )
    }

    View full-size slide

  26. VITAMIN
    object VitaminTopBars {
    private const val MAX_ACTIONS = 3
    @Composable
    fun Primary(
    title: String,
    modifier: Modifier = Modifier,
    maxActions: Int = MAX_ACTIONS,
    actions: List = emptyList(),
    expandedMenu: MutableState = remember { mutableStateOf(false) },
    colors: TopBarColors = VitaminTopBarColors.primary(),
    onDismissOverflowMenu: (() !& Unit)? = null,
    overflowIcon: (@Composable VitaminMenuIconButtons.() !& Unit)? = null,
    navigationIcon: (@Composable VitaminNavigationIconButtons.() !& Unit)? = null
    )
    }

    View full-size slide

  27. VITAMIN
    open class ActionItem(
    val icon: Painter? = null,
    val contentDescription: String?,
    val content: @Composable () !& Unit = {},
    val onClick: () !& Unit,
    )

    View full-size slide

  28. VITAMIN
    VitaminTopBars.Primary(
    title = "Title",
    actions = arrayListOf(
    ActionItem(
    icon = painterResource(R.drawable.ic_vtmn_android_line),
    contentDescription = "Android",
    onClick = { }
    ),
    ActionItem(
    contentDescription = null,
    content = { Text("Windows") },
    onClick = { }
    ),
    ActionItem(
    icon = painterResource(R.drawable.ic_vtmn_apple_line),
    contentDescription = "Apple",
    onClick = { }
    )
    )
    )

    View full-size slide

  29. MATERIAL 2
    TopAppBar(
    title = { Text(text = "Title") },
    actions = {
    IconButton(onClick = { }) {
    Icon(
    painter = painterResource(R.drawable.ic_vtmn_android_line),
    contentDescription = "Android"
    )
    }
    TextButton(onClick = { }) {
    Text(text = "Windows")
    }
    IconButton(onClick = { }) {
    Icon(
    painter = painterResource(R.drawable.ic_vtmn_apple_line),
    contentDescription = "Apple"
    )
    }
    }
    )

    View full-size slide

  30. VITAMIN
    object VitaminTopBars {
    private const val MAX_ACTIONS = 3
    @Composable
    fun Primary(
    title: String,
    modifier: Modifier = Modifier,
    maxActions: Int = MAX_ACTIONS,
    actions: List = emptyList(),
    expandedMenu: MutableState = remember { mutableStateOf(false) },
    colors: TopBarColors = VitaminTopBarColors.primary(),
    onDismissOverflowMenu: (() !& Unit)? = null,
    overflowIcon: (@Composable VitaminMenuIconButtons.() !& Unit)? = null,
    navigationIcon: (@Composable VitaminNavigationIconButtons.() !& Unit)? = null
    )
    }

    View full-size slide

  31. VITAMIN
    object VitaminTopBarColors {
    @Composable
    fun primary(
    background: Color = VitaminTheme.colors.vtmnBackgroundPrimary,
    contentColor: Color = VitaminTheme.colors.vtmnContentPrimary,
    iconColor: Color = VitaminTheme.colors.vtmnContentPrimary
    ): TopBarColors = !" !!$
    @Composable
    fun contextual(
    background: Color = VitaminTheme.colors.vtmnContentActive,
    contentColor: Color = VitaminTheme.colors.vtmnContentPrimaryReversed,
    iconColor: Color = VitaminTheme.colors.vtmnContentPrimaryReversed
    ): TopBarColors = !" !!$
    }

    View full-size slide

  32. MATERIAL 2
    !" primary
    TopAppBar(
    title = { Text(text = "Title") },
    backgroundColor = MaterialTheme.colors.surface,
    contentColor = MaterialTheme.colors.onSurface
    )
    !" contextual
    TopAppBar(
    title = { Text(text = "Title") },
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = MaterialTheme.colors.onPrimary
    )

    View full-size slide

  33. VITAMIN
    object VitaminTopBars {
    private const val MAX_ACTIONS = 3
    @Composable
    fun Primary(
    title: String,
    modifier: Modifier = Modifier,
    maxActions: Int = MAX_ACTIONS,
    actions: List = emptyList(),
    expandedMenu: MutableState = remember { mutableStateOf(false) },
    colors: TopBarColors = VitaminTopBarColors.primary(),
    onDismissOverflowMenu: (() !& Unit)? = null,
    overflowIcon: (@Composable VitaminMenuIconButtons.() !& Unit)? = null,
    navigationIcon: (@Composable VitaminNavigationIconButtons.() !& Unit)? = null
    )
    }

    View full-size slide

  34. VITAMIN
    object VitaminNavigationIconButtons {
    @Composable
    fun PreviousPage(
    !" !!$
    )
    @Composable
    fun Drawer(
    !" !!$
    )
    @Composable
    fun Context(
    !" !!$
    )
    @Composable
    fun Close(
    !" !!$
    )
    }

    View full-size slide

  35. VITAMIN
    object VitaminNavigationIconButtons {
    @Composable
    fun PreviousPage(
    onClick: () !& Unit,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    ) {
    IconButtons(
    onClick = onClick,
    modifier = modifier,
    enabled = enabled,
    interactionSource = interactionSource,
    icon = {
    Icon(
    painter = painterResource(R.drawable.ic_vtmn_chevron_left_line),
    contentDescription = contentDescription
    )
    }
    )
    }
    }

    View full-size slide

  36. VITAMIN
    VitaminTopBars.Primary(
    title = "Title",
    navigationIcon = {
    PreviousPage(
    onClick = { },
    contentDescription = "Come back to previous page"
    )
    }
    )

    View full-size slide

  37. MATERIAL 2
    TopAppBar(
    title = { Text(text = "Title") },
    navigationIcon = {
    IconButton(
    onClick = { },
    content = {
    Icon(
    painter = painterResource(R.drawable.ic_vtmn_chevron_left_line),
    contentDescription = "Come back to previous page"
    )
    }
    )
    }
    )

    View full-size slide

  38. VITAMIN
    object VitaminTopBars {
    private const val MAX_ACTIONS = 3
    @Composable
    fun Primary(
    title: String,
    modifier: Modifier = Modifier,
    maxActions: Int = MAX_ACTIONS,
    actions: List = emptyList(),
    expandedMenu: MutableState = remember { mutableStateOf(false) },
    colors: TopBarColors = VitaminTopBarColors.primary(),
    onDismissOverflowMenu: (() !& Unit)? = null,
    overflowIcon: (@Composable VitaminMenuIconButtons.() !& Unit)? = null,
    navigationIcon: (@Composable VitaminNavigationIconButtons.() !& Unit)? = null
    )
    }

    View full-size slide

  39. VITAMIN
    object VitaminMenuIconButtons {
    @Composable
    fun More(
    onClick: () !& Unit,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    ) {
    IconButtons(
    onClick = onClick,
    modifier = modifier,
    enabled = enabled,
    interactionSource = interactionSource,
    icon = {
    Icon(
    painter = painterResource(R.drawable.ic_vtmn_more_2_line),
    contentDescription = contentDescription
    )
    }
    )
    }
    }

    View full-size slide

  40. VITAMIN
    @Composable
    internal fun OverflowMenu(
    actions: List,
    modifier: Modifier = Modifier,
    expanded: MutableState = remember { mutableStateOf(false) },
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    onDismissRequest: (() !& Unit)? = null,
    overflowIcon: @Composable VitaminMenuIconButtons.() !& Unit
    )

    View full-size slide

  41. VITAMIN
    @Composable
    internal fun OverflowMenu(
    actions: List,
    modifier: Modifier = Modifier,
    expanded: MutableState = remember { mutableStateOf(false) },
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    onDismissRequest: (() !& Unit)? = null,
    overflowIcon: @Composable VitaminMenuIconButtons.() !& Unit
    )

    View full-size slide

  42. VITAMIN
    object VitaminMenus {
    @Composable
    fun Dropdown(
    anchor: @Composable () !& Unit,
    modifier: Modifier = Modifier,
    expanded: MutableState = remember { mutableStateOf(false) },
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    onDismissRequest: () !& Unit = {},
    children: @Composable VitaminMenuItems.() !& Unit
    )
    }

    View full-size slide

  43. VITAMIN
    object VitaminMenus {
    @Composable
    fun Dropdown(
    anchor: @Composable () !& Unit,
    modifier: Modifier = Modifier,
    expanded: MutableState = remember { mutableStateOf(false) },
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    onDismissRequest: () !& Unit = {},
    children: @Composable VitaminMenuItems.() !& Unit
    )
    }

    View full-size slide

  44. VITAMIN
    val expanded = remember { mutableStateOf(false) }
    VitaminTopBars.Primary(
    title = "Title",
    overflowIcon = {
    More(
    onClick = { expanded.value = true },
    contentDescription = "More",
    )
    },
    expandedMenu = expanded
    )

    View full-size slide

  45. MATERIAL 2
    @Composable
    fun OverflowMenu() {
    val expanded = remember { mutableStateOf(false) }
    Box {
    IconButton(onClick = { expanded.value = true }) {
    Icon(
    painter = painterResource(id = R.drawable.ic_vtmn_more_2_line),
    contentDescription = "More"
    )
    }
    DropdownMenu(
    expanded = expanded.value,
    onDismissRequest = { expanded.value = false },
    content = {
    DropdownMenuItem(
    onClick = { },
    content = {
    Text(text = "Sub menu 1")
    }
    )
    }
    )
    }
    }

    View full-size slide

  46. MATERIAL 2
    TopAppBar(
    title = { Text(text = "Title") },
    actions = {
    OverflowMenu()
    }
    )

    View full-size slide

  47. Product Page
    Vitamin implementation

    View full-size slide

  48. @Composable
    fun TopAppBar(
    onNavigationClick: () !& Unit,
    onShareClick: () !& Unit,
    onFavoriteClick: () !& Unit,
    modifier: Modifier = Modifier,
    isFavorite: Boolean = false
    ) {
    !% !!#
    }

    View full-size slide

  49. @Composable
    fun TopAppBar(
    onNavigationClick: () !& Unit,
    onShareClick: () !& Unit,
    onFavoriteClick: () !& Unit,
    modifier: Modifier = Modifier,
    isFavorite: Boolean = false
    ) {
    VitaminTopBars.Primary(
    title = "Product page",
    modifier = modifier
    )
    }

    View full-size slide

  50. @Composable
    fun TopAppBar(
    onNavigationClick: () !& Unit,
    onShareClick: () !& Unit,
    onFavoriteClick: () !& Unit,
    modifier: Modifier = Modifier,
    isFavorite: Boolean = false
    ) {
    VitaminTopBars.Primary(
    title = "Product page",
    modifier = modifier,
    navigationIcon = {
    Context(
    onClick = onNavigationClick,
    contentDescription = "Back to product list"
    )
    }
    )
    }

    View full-size slide

  51. @Composable
    fun TopAppBar(
    onNavigationClick: () !& Unit,
    onShareClick: () !& Unit,
    onFavoriteClick: () !& Unit,
    modifier: Modifier = Modifier,
    isFavorite: Boolean = false
    ) {
    VitaminTopBars.Primary(
    !% !!#
    actions = listOf(
    ActionItem(
    icon =
    rememberVectorPainter(VitaminIcons.Line.Share),
    contentDescription = "Share product",
    onClick = onShareClick
    ),
    ActionItem(
    icon = rememberVectorPainter(if (isFavorite)
    VitaminIcons.Fill.Heart else VitaminIcons.Line.Heart),
    contentDescription = "Favorite",
    onClick = onFavoriteClick
    )
    )

    View full-size slide

  52. @Composable
    fun ProductInfo(
    productUi: ProductPageUi,
    modifier: Modifier = Modifier
    ) {
    !% !!#
    }

    View full-size slide

  53. @Composable
    fun ProductInfo(
    productUi: ProductPageUi,
    modifier: Modifier = Modifier
    ) {
    Column(
    modifier = modifier.padding(horizontal = 16.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
    FlowRow(
    horizontalArrangement = Arrangement.spacedBy(16.dp),
    verticalAlignment = Alignment.CenterVertically
    ) {
    productUi.tags.forEach { tag !"
    VitaminTags.DecorativeGravel(label = tag)
    }
    }
    }
    }

    View full-size slide

  54. @Composable
    fun ProductInfo(
    productUi: ProductPageUi,
    modifier: Modifier = Modifier
    ) {
    Column(
    modifier = modifier.padding(horizontal = 16.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
    !" !!$
    Text(
    text = productUi.title,
    style = VitaminTheme.typography.h4
    )
    }
    }

    View full-size slide

  55. @Composable
    fun ProductInfo(
    productUi: ProductPageUi,
    modifier: Modifier = Modifier
    ) {
    Column(
    modifier = modifier.padding(horizontal = 16.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
    !" !!$
    Row(
    horizontalArrangement = Arrangement.SpaceBetween,
    modifier = Modifier.fillMaxWidth()
    ) {
    VitaminRatings.ReadOnlyCompact(
    number = productUi.ratingNote,
    maxValue = productUi.ratingMax,
    nbComments = productUi.nbComments,
    sizes = VitaminRatingSizes.small()
    )
    VitaminPrices.Accent(
    text = productUi.price
    )
    }

    View full-size slide

  56. @Composable
    fun ProductActions(
    onBasketClick: (ProductPageUi) !& Unit,
    onOneClick: (ProductPageUi) !& Unit,
    modifier: Modifier = Modifier
    ) {
    !% !!#
    }

    View full-size slide

  57. @Composable
    fun ProductActions(
    onBasketClick: (ProductPageUi) !& Unit,
    onOneClick: (ProductPageUi) !& Unit,
    modifier: Modifier = Modifier
    ) {
    Column(
    modifier = modifier.padding(horizontal = 16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
    VitaminButtons.Conversion(
    text = "Add to basket",
    modifier = Modifier.fillMaxWidth(),
    onClick = { onBasketClick(productUi) },
    icon =
    rememberVectorPainter(VitaminIcons.Line.ShoppingCart)
    )
    }
    }

    View full-size slide

  58. @Composable
    fun ProductActions(
    onBasketClick: (ProductPageUi) !& Unit,
    onOneClick: (ProductPageUi) !& Unit,
    modifier: Modifier = Modifier
    ) {
    Column(
    modifier = modifier.padding(horizontal = 16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
    VitaminButtons.Conversion(
    text = "Add to basket",
    modifier = Modifier.fillMaxWidth(),
    onClick = { onBasketClick(productUi) },
    icon =
    rememberVectorPainter(VitaminIcons.Line.ShoppingCart)
    )
    VitaminButtons.PrimaryReversed(
    text = "Buy in one click",
    modifier = Modifier.fillMaxWidth(),
    onClick = { onOneClick(productUi) },
    icon =
    rememberVectorPainter(VitaminIcons.Line.Flashlight)
    )

    View full-size slide

  59. @Composable
    fun ProductPage(
    productUi: ProductPageUi,
    onNavigationClick: () !& Unit,
    onShareClick: () !& Unit,
    onFavoriteClick: () !& Unit,
    onBasketClick: (ProductPageUi) !& Unit,
    onOneClick: (ProductPageUi) !& Unit,
    picturePagersState: PagerState = rememberPagerState()
    ) {
    !% !!#
    }

    View full-size slide

  60. @Composable
    fun ProductPage(
    !# parameters !%
    ) {
    !% !!#
    }

    View full-size slide

  61. @Composable
    fun ProductPage(
    !# parameters !%
    ) {
    Scaffold(
    topBar = {
    TopAppBar(
    onNavigationClick = onNavigationClick,
    onShareClick = onShareClick,
    onFavoriteClick = onFavoriteClick,
    isFavorite = productUi.isFavorite
    )
    },
    ) {
    !" !!#
    }
    }

    View full-size slide

  62. @Composable
    fun ProductPage(
    !# parameters !%
    ) {
    Scaffold(topBar = { !# parameters !% }) {
    LazyColumn(
    modifier = Modifier.padding(it),
    verticalArrangement = Arrangement.spacedBy(32.dp)
    ) {
    item {
    HorizontalPager(
    index = productUi.selectedPicture,
    count = productUi.pictures.size,
    state = picturePagersState,
    modifier=Modifier.height(300.dp).fillMaxWidth()
    ) { index !"
    Image(painter = rememberAsyncImagePainter(
    model = productUi.pictures[index],
    ),
    contentDescription = null,
    contentScale = ContentScale.FillWidth,
    modifier = Modifier.fillMaxWidth()
    )
    }

    View full-size slide

  63. @Composable
    fun ProductPage(
    !# parameters !%
    ) {
    Scaffold(topBar = { !# parameters !% }) {
    LazyColumn(
    modifier = Modifier.padding(it),
    verticalArrangement = Arrangement.spacedBy(32.dp)
    ) {
    !# !!$
    item {
    ProductInfo(productUi = productUi)
    }
    }
    }
    }

    View full-size slide

  64. @Composable
    fun ProductPage(
    !# parameters !%
    ) {
    Scaffold(topBar = { !# parameters !% }) {
    LazyColumn(
    modifier = Modifier.padding(it),
    verticalArrangement = Arrangement.spacedBy(32.dp)
    ) {
    !# !!$
    item {
    ProductActions(
    onBasketClick = onBasketClick,
    onOneClick = onOneClick
    )
    }
    }
    }
    }

    View full-size slide

  65. WHAT’S NEXT?!
    Tokenisation

    View full-size slide

  66. ATOMIC DESIGN
    Atom Molecule Organism Template Pages

    View full-size slide

  67. ATOMIC DESIGN
    Atom Molecule Organism Template Pages
    Ions
    core.color.blue
    50 #E7F3F9
    100 #BEDEEF
    400 #007DBC
    500 #00689D
    700 #012B49
    core.spacing
    core.typography

    View full-size slide

  68. BUTTON TOKENS
    Spacings
    Button
    component.button-medium.primary.spacing.padding.top
    component.button-medium.primary.spacing.padding.left component.button-medium.primary.spacing.padding.right
    component.button-medium.primary.spacing.padding.bottom

    View full-size slide

  69. BUTTON TOKENS
    Radius
    Button
    component.button-medium.primary
    .border-radius.container.top-right
    component.button-medium.primary
    .border-radius.container.top-left
    component.button-medium.primary
    .border-radius.container.bottom-left
    component.button-medium.primary
    .border-radius.container.bottom-right

    View full-size slide

  70. BUTTON TOKENS
    Colors
    Button
    component.button-medium.primary.color.background.default
    .hover
    .active
    component.button-medium.primary.color.content.text
    .icon

    View full-size slide

  71. Button
    component.button-medium.primary.color.background.default
    component.button-medium.primary.color.content.inverse
    BUTTON COLOR TOKENS
    core.color.blue
    50 #E7F3F9
    100 #BEDEEF
    core.color
    white #FFFFFF
    semantic.color.container
    default {core.color.blue.400}
    semantic.color.content
    component.button-medium.primary.color.background
    default {semantic.color.container.highlight}
    hover
    active
    black #001018
    400 #007DBC
    500 #00689D
    700 #012B49
    {semantic.color.container.default}
    highlight {core.color.blue.400}
    tertiary {core.color.blue.50}
    component.button-medium.primary.color.content
    text
    icon {semantic.color.content.inverse}
    {semantic.color.content.inverse}
    highlight
    inverse {core.color.white}
    {core.color.blue.500}
    {semantic.color.container.default}

    View full-size slide

  72. Button
    component.button-medium.tertiary.color.background.default
    BUTTON COLOR TOKENS
    core.color.blue
    50 #E7F3F9
    100 #BEDEEF
    core.color
    white #FFFFFF
    semantic.color.container
    default {core.color.blue.400}
    semantic.color.content
    component.button-medium.tertiary.color.background
    default
    hover
    active
    black #001018
    400 #007DBC
    500 #00689D
    700 #012B49
    {semantic.color.container.default}
    highlight {core.color.blue.400}
    tertiary {core.color.blue.50}
    component.button-medium.tertiary.color.content
    text
    icon
    {semantic.color.content.highlight}
    highlight
    inverse {core.color.white}
    {core.color.blue.500}
    {semantic.color.container.default}
    {semantic.color.container.tertiary}
    {semantic.color.content.highlight}
    component.button-medium.tertiary.color.content.inverse

    View full-size slide

  73. Button
    component.button-medium.tertiary.color.background.default
    BUTTON COLOR TOKENS
    core.color.blue
    50 #E7F3F9
    100 #BEDEEF
    core.color
    white #FFFFFF
    semantic.color.container
    default {core.color.blue.500}
    semantic.color.content
    component.button-medium.tertiary.color.background
    default
    hover
    active
    black #001018
    400 #007DBC
    500 #00689D
    700 #012B49
    {semantic.color.container.default}
    highlight {core.color.blue.500}
    tertiary
    component.button-medium.tertiary.color.content
    text
    icon
    {semantic.color.content.highlight}
    highlight
    inverse {core.color.white}
    {core.color.blue.400}
    {semantic.color.container.default}
    {semantic.color.container.tertiary}
    {semantic.color.content.highlight}
    {core.color.blue.700}
    component.button-medium.tertiary.color.content.inverse

    View full-size slide

  74. Button
    BUTTON COLOR TOKENS
    core.color.blue
    50 #E7F3F9
    100 #BEDEEF
    core.color
    white #FFFFFF
    semantic.color.container
    default {core.color.blue.500}
    semantic.color.content
    component.button-medium.tertiary.color.background
    default
    hover
    active
    black #001018
    400 #007DBC
    500 #00689D
    700 #012B49
    {semantic.color.container.default}
    highlight {core.color.blue.500}
    tertiary
    component.button-medium.tertiary.color.content
    text
    icon
    {semantic.color.content.highlight}
    highlight
    inverse {core.color.white}
    {core.color.blue.400}
    {semantic.color.container.default}
    {semantic.color.container.tertiary}
    {semantic.color.content.highlight}
    {core.color.blue.700}
    component.button-medium.tertiary.color.content.inverse
    component.button-medium.tertiary.color.background.default

    View full-size slide

  75. interface ButtonMediumTokens {
    val containerColor: ColorToken
    val containerCornerRadius: RadiusToken
    val containerPaddingTop: SpacingToken
    val containerPaddingBottom: SpacingToken
    val containerPaddingStart: SpacingToken
    val containerPaddingEnd: SpacingToken
    val containerSpaceBetween: SpacingToken
    val containerElevation: SizingToken
    val startIconSize: SizingToken
    val startIconColor: ColorToken
    val endIconSize: SizingToken
    val endIconColor: ColorToken
    val textColor: ColorToken
    val textStyle: TypographyToken
    val borderWidth: SizingToken
    val borderColor: ColorToken
    }

    View full-size slide

  76. interface ButtonMediumTokens {
    val containerColor: ColorToken
    val containerCornerRadius: RadiusToken
    val containerPaddingTop: SpacingToken
    val containerPaddingBottom: SpacingToken
    val containerPaddingStart: SpacingToken
    val containerPaddingEnd: SpacingToken
    val containerSpaceBetween: SpacingToken
    val containerElevation: SizingToken
    val startIconSize: SizingToken
    val startIconColor: ColorToken
    val endIconSize: SizingToken
    val endIconColor: ColorToken
    val textColor: ColorToken
    val textStyle: TypographyToken
    val borderWidth: SizingToken
    val borderColor: ColorToken
    }

    View full-size slide

  77. object ButtonMediumPrimaryDefaults : ButtonMediumTokens {
    override val containerColor: ColorToken =
    VitaminColorToken.BackgroundPrimary
    override val startIconColor: ColorToken =
    VitaminColorToken.ContentInverse
    override val endIconColor: ColorToken =
    VitaminColorToken.ContentInverse
    override val textColor: ColorToken =
    VitaminColorToken.ContentInverse
    !% other component tokens
    }

    View full-size slide

  78. Button
    BUTTON COLOR TOKENS
    core.color.blue
    50 #E7F3F9
    100 #BEDEEF
    core.color
    white #FFFFFF
    semantic.color.container
    default {core.color.blue.500}
    semantic.color.content
    component.button-medium.tertiary.color.background
    default
    hover
    active
    black #001018
    400 #007DBC
    500 #00689D
    700 #012B49
    {semantic.color.container.default}
    highlight {core.color.blue.500}
    tertiary
    component.button-medium.tertiary.color.content
    text
    icon
    {semantic.color.content.highlight}
    highlight
    inverse {core.color.white}
    {core.color.blue.400}
    {semantic.color.container.default}
    {semantic.color.container.tertiary}
    {semantic.color.content.highlight}
    {core.color.blue.700}
    component.button-medium.tertiary.color.content.inverse
    component.button-medium.tertiary.color.background.default

    View full-size slide

  79. enum class VitaminColorToken {
    BackgroundPrimary,
    BackgroundSecondary,
    BackgroundTertiary,
    BackgroundBrandPrimary,
    BackgroundBrandSecondary,
    BackgroundAccent,
    BackgroundDiscount,
    BackgroundPrimaryReversed,
    BackgroundBrandPrimaryReversed,
    !" same for contents, borders and decoratives
    }

    View full-size slide

  80. class VitaminColors(
    backgroundPrimary: Color,
    backgroundSecondary: Color,
    backgroundTertiary: Color,
    backgroundBrandPrimary: Color,
    backgroundBrandSecondary: Color,
    backgroundAccent: Color,
    backgroundDiscount: Color,
    backgroundPrimaryReversed: Color,
    backgroundBrandPrimaryReversed: Color,
    !" same for content, border and decorative
    ) {
    var backgroundPrimary by mutableStateOf(backgroundPrimary)
    internal set
    !" same for other properties
    }

    View full-size slide

  81. fun VitaminColors.fromToken(value: VitaminColorToken): Color =
    when (value) {
    VitaminColorToken.BackgroundPrimary !& backgroundPrimary
    VitaminColorToken.BackgroundSecondary !&
    backgroundSecondary
    VitaminColorToken.BackgroundTertiary !& backgroundTertiary
    VitaminColorToken.BackgroundBrandPrimary !&
    backgroundBrandPrimary
    VitaminColorToken.BackgroundBrandSecondary !&
    backgroundBrandSecondary
    VitaminColorToken.BackgroundAccent !& backgroundAccent
    VitaminColorToken.BackgroundDiscount !& backgroundDiscount
    VitaminColorToken.BackgroundPrimaryReversed !&
    backgroundPrimaryReversed
    VitaminColorToken.BackgroundBrandPrimaryReversed !&
    backgroundBrandPrimaryReversed
    }
    @Composable
    fun VitaminColorToken.toColor(): Color =
    LocalVitaminColors.current.fromToken(this)

    View full-size slide

  82. Button
    BUTTON COLOR TOKENS
    core.color.blue
    50 #E7F3F9
    100 #BEDEEF
    core.color
    white #FFFFFF
    semantic.color.container
    default {core.color.blue.500}
    semantic.color.content
    component.button-medium.tertiary.color.background
    default
    hover
    active
    black #001018
    400 #007DBC
    500 #00689D
    700 #012B49
    {semantic.color.container.default}
    highlight {core.color.blue.500}
    tertiary
    component.button-medium.tertiary.color.content
    text
    icon
    {semantic.color.content.highlight}
    highlight
    inverse {core.color.white}
    {core.color.blue.400}
    {semantic.color.container.default}
    {semantic.color.container.tertiary}
    {semantic.color.content.highlight}
    {core.color.blue.700}
    component.button-medium.tertiary.color.content.inverse
    component.button-medium.tertiary.color.background.default

    View full-size slide

  83. object VitaminPalette {
    val vtmnPurple50 = Color(242, 237, 242)
    val vtmnPurple100 = Color(220, 207, 221)
    val vtmnPurple200 = Color(172, 141, 175)
    val vtmnPurple300 = Color(150, 111, 154)
    val vtmnPurple400 = Color(108, 78, 111)
    val vtmnPurple500 = Color(91, 65, 93)
    val vtmnPurple600 = Color(73, 53, 75)
    val vtmnPurple700 = Color(44, 32, 45)
    !" same for blue, green, conifer, yellow,
    !" orange, red and grey
    }

    View full-size slide

  84. val darkTheme = VitaminColors(
    backgroundPrimary = VitaminPalette.grey900,
    backgroundSecondary = VitaminPalette.grey950,
    backgroundTertiary = VitaminPalette.purple400,
    backgroundBrandPrimary = VitaminPalette.grey900,
    backgroundBrandSecondary = VitaminPalette.grey50,
    backgroundAccent = VitaminPalette.grey50,
    backgroundDiscount = VitaminPalette.white,
    backgroundPrimaryReversed = VitaminPalette.white,
    backgroundBrandPrimaryReversed = VitaminPalette.purple400
    )

    View full-size slide

  85. WHAT IS THE DIFFERENCE?
    • One source of truth about the structure of components
    • Can build an instance of tokens adapted for each platform
    • The structure can be read and generate core, semantic and component tokens
    • You are focus on the external contract of the component

    View full-size slide

  86. class ButtonMediumColors(
    val containerColor: Color,
    val startIconColor: Color,
    val endIconColor: Color,
    val textColor: Color
    )
    @Composable
    fun ButtonMediumTokens.colors(
    containerColor: Color = this.containerColor.toColor(),
    startIconColor: Color = this.startIconColor.toColor(),
    endIconColor: Color = this.endIconColor.toColor(),
    textColor: Color = this.textColor.toColor()
    ): ButtonMediumColors {
    return ButtonMediumColors(
    containerColor = containerColor,
    startIconColor = startIconColor,
    endIconColor = endIconColor,
    textColor = textColor
    )
    }

    View full-size slide

  87. class ButtonMediumColors(
    val containerColor: Color,
    val startIconColor: Color,
    val endIconColor: Color,
    val textColor: Color
    )
    @Composable
    fun ButtonMediumTokens.colors(
    containerColor: Color = this.containerColor.toColor(),
    startIconColor: Color = this.startIconColor.toColor(),
    endIconColor: Color = this.endIconColor.toColor(),
    textColor: Color = this.textColor.toColor()
    ): ButtonMediumColors {
    return ButtonMediumColors(
    containerColor = containerColor,
    startIconColor = startIconColor,
    endIconColor = endIconColor,
    textColor = textColor
    )
    }

    View full-size slide

  88. @Composable
    fun ButtonMedium(
    text: String,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    icon: Painter? = null,
    iconSide: IconSide = IconSide.START,
    interactionSource: MutableInteractionSource =
    remember { MutableInteractionSource() },
    style: TextStyle =
    ButtonMediumPrimaryDefaults.textStyle.textStyle(),
    colors: ButtonMediumColors =
    ButtonMediumPrimaryDefaults.colors(),
    sizes: ButtonMediumSizes =
    ButtonMediumPrimaryDefaults.sizes(),
    shape: Shape = ButtonMediumPrimaryDefaults.shape(),
    border: BorderStroke = ButtonMediumPrimaryDefaults.border(),
    elevation: ButtonElevation =
    ButtonMediumPrimaryDefaults.elevation(),
    onClick: () !& Unit
    )

    View full-size slide

  89. @Composable
    fun ButtonMedium(
    text: String,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    icon: Painter? = null,
    iconSide: IconSide = IconSide.START,
    interactionSource: MutableInteractionSource =
    remember { MutableInteractionSource() },
    style: TextStyle =
    ButtonMediumPrimaryDefaults.textStyle.textStyle(),
    colors: ButtonMediumColors =
    ButtonMediumPrimaryDefaults.colors(),
    sizes: ButtonMediumSizes =
    ButtonMediumPrimaryDefaults.sizes(),
    shape: Shape = ButtonMediumPrimaryDefaults.shape(),
    border: BorderStroke = ButtonMediumPrimaryDefaults.border(),
    elevation: ButtonElevation =
    ButtonMediumPrimaryDefaults.elevation(),
    onClick: () !& Unit
    )

    View full-size slide

  90. Disclaimer
    Tokens are not yet open source

    View full-size slide

  91. REFERENCES
    Vitamin Figma Community https://www.figma.com/@decathlon
    Vitamin Compose https://github.com/Decathlon/vitamin-compose
    Vitamin Android https://github.com/Decathlon/vitamin-android
    Vitamin iOS https://github.com/Decathlon/vitamin-ios
    Vitamin Web https://github.com/Decathlon/vitamin-web
    Slides: https://speakerdeck.com/gerardpaligot/take-your-shot-of-vitamin
    GitHub Project https://github.com/GerardPaligot/take-your-shot-of-vitamin
    Decathlon Outdoor https://play.google.com/store/apps/details?id=com.decathlon.quechuafinder

    View full-size slide

  92. @GerardPaligot
    TAKE YOUR SHOT OF VITAMIN!
    Thank you! Any questions?

    View full-size slide