telus / universal-design-system-android

TELUS Brand-agnostic, Cross-platform Design System for Android
1 stars 0 forks source link

Investigate how we're going to approach handling the design tokens #1

Closed leiakim closed 2 years ago

jenncoop commented 2 years ago

Design Tokens Architecture

Notes

Architecture Example

ProgressBar Example ProgressBar Example with BaseComponent abstract class and component wrapper

Preview

Image

ProgressBar Design Token Spec (Allium)

    "ProgressBar": {
      "appearances": {
        "inactive": "{appearances.ProgressBar.inactive}",
        "negative": {
          "type": "variant",
          "values": [true]
        }
      },
      "rules": [
        {
          "if": {
            "inactive": true
          },
          "tokens": {
            "backgroundColor": "{palette.color.greyCloud}",
            "gradient": "{system.gradient.none}",
            "outlineColor": "{palette.color.greyThunder}"
          }
        },
        {
          "if": {
            "negative": true
          },
          "tokens": {
            "backgroundColor": "{palette.color.redDark}",
            "gradient": "{system.gradient.none}",
            "outlineColor": "{palette.color.redDark}"
          }
        }
      ],
      "tokens": {
        "backgroundColor": "{palette.color.greenAccessible}",
        "borderRadius": "{palette.radius.radius12}",
        "gradient": "{palette.gradient.green}",
        "outlineColor": "{palette.color.greenAccessible}",
        "outlineWidth": "{palette.border.border1}"
      }
    }

Next Steps

Questions

leiakim commented 2 years ago

@nadaltelus @QuickChangeSpidey (Not sure if this is the right tag for Akshay) Questions^

nadaltelus commented 2 years ago

Its the right one! Will read and comment here.

nadaltelus commented 2 years ago

On the questions:

"Link": {
      "appearances": {
        "alternative": {
          "description": "Replaces the default green colour with a more subtle grey.",
          "type": "variant",
          "values": [true]
        },
        "focus": "{appearances.Link.focus}",
        "hover": "{appearances.Link.hover}",
        "iconPosition": "{appearances.Link.iconPosition}",
        "inverse": {
          "description": "Styles the link white for use on dark backgrounds.",
          "type": "variant",
          "values": [true]
        },
        "light": {
          "description": "A lighter grey variant, used to lower the emphasis on a given Link (e.g. in Breadcrumbs)",
          "type": "variant",
          "values": [true]
        },
        "pressed": "{appearances.Link.pressed}",
        "size": {
          "description": "Text sizes for block links; similar but not always identical to Typography sizes.",
          "type": "variant",
          "values": ["large", "small", "micro"]
        }
      },
      "rules": [
        {
          "if": {
            "iconPosition": "left"
          },
          "tokens": {
            "iconSpace": "{system.integer.2}"
          }
        },
        {
          "if": {
            "focus": true
          },
          "tokens": {
            "outerBorderColor": "{palette.color.greenAccessible}",
            "outerBorderOutline": "{system.borderStyle.none}"
          }
        },
        {
          "if": {
            "hover": true
          },
          "tokens": {
            "color": "{palette.color.greenSanFelix}",
            "textLine": "{system.textLine.none}"
          }
        },
        {
          "if": {
            "pressed": true
          },
          "tokens": {
            "color": "{palette.color.greenDarkFern}",
            "textLine": "{system.textLine.none}"
          }
        },
        {
          "if": {
            "focus": true,
            "pressed": true
          },
          "tokens": {
            "outerBorderColor": "{palette.color.greenDarkFern}"
          }
        },
        {
          "if": {
            "size": "large"
          },
          "tokens": {
            "blockFontSize": "{palette.fontSize.size20}",
            "blockLineHeight": "{palette.lineHeight.ratio8to5}",
            "iconSize": "{palette.size.size24}"
          }
        },
        {
          "if": {
            "size": "small"
          },
          "tokens": {
            "blockFontSize": "{palette.fontSize.size14}",
            "blockLineHeight": "{palette.lineHeight.ratio10to7}",
            "iconSize": "{palette.size.size20}"
          }
        },
        {
          "if": {
            "size": "micro"
          },
          "tokens": {
            "blockFontName": "{palette.fontName.HelveticaNow}",
            "blockFontSize": "{palette.fontSize.size12}",
            "blockFontWeight": "{palette.fontWeight.weight500}",
            "blockLineHeight": "{palette.lineHeight.ratio4to3}",
            "iconSize": "{palette.size.size16}"
          }
        },
        {
          "if": {
            "alternative": true
          },
          "tokens": {
            "color": "{palette.color.greyThunder}"
          }
        },
        {
          "if": {
            "alternative": true,
            "focus": true
          },
          "tokens": {
            "color": "{palette.color.greyCharcoal}",
            "outerBorderColor": "{palette.color.greyCharcoal}"
          }
        },
        {
          "if": {
            "alternative": true,
            "hover": true
          },
          "tokens": {
            "color": "{palette.color.greyThunder}"
          }
        },
        {
          "if": {
            "alternative": true,
            "pressed": true
          },
          "tokens": {
            "color": "{palette.color.greyThunder}"
          }
        },
        {
          "if": {
            "inverse": true
          },
          "tokens": {
            "color": "{palette.color.white}"
          }
        },
        {
          "if": {
            "hover": true,
            "inverse": true
          },
          "tokens": {
            "color": "{palette.color.greyAthens}"
          }
        },
        {
          "if": {
            "focus": true,
            "inverse": true
          },
          "tokens": {
            "outerBorderColor": "{palette.color.white}",
            "outerBorderOutline": "{system.borderStyle.none}"
          }
        },
        {
          "if": {
            "inverse": true,
            "pressed": true
          },
          "tokens": {
            "color": "{palette.color.greyMystic}"
          }
        },
        {
          "if": {
            "focus": true,
            "inverse": true,
            "pressed": true
          },
          "tokens": {
            "color": "{palette.color.greyMystic}",
            "outerBorderColor": "{palette.color.greyMystic}"
          }
        },
        {
          "if": {
            "light": true
          },
          "tokens": {
            "color": "{palette.color.greyShuttle}"
          }
        },
        {
          "if": {
            "focus": true,
            "light": true
          },
          "tokens": {
            "color": "{palette.color.greyShuttle}",
            "outerBorderColor": "{palette.color.greyShuttle}"
          }
        },
        {
          "if": {
            "hover": true,
            "light": true
          },
          "tokens": {
            "color": "{palette.color.greyCharcoal}",
            "textLine": "{system.textLine.none}"
          }
        }
      ],
      "tokens": {
        "alignSelf": "{system.flexAlign.flexStart}",
        "blockFontName": "{palette.fontName.HelveticaNow}",
        "blockFontSize": "{palette.fontSize.size16}",
        "blockFontWeight": "{palette.fontWeight.weight400}",
        "blockLineHeight": "{palette.lineHeight.ratio3to2}",
        "borderRadius": "{palette.radius.radius4}",
        "color": "{palette.color.greenAccessible}",
        "icon": "{system.icon.none}",
        "iconSize": "{palette.size.size24}",
        "iconSpace": "{system.integer.1}",
        "iconTranslateX": "{system.size.zero}",
        "iconTranslateY": "{system.size.zero}",
        "outerBorderColor": "{palette.color.transparent}",
        "outerBorderGap": "{palette.size.size2}",
        "outerBorderOutline": "{system.borderStyle.none}",
        "outerBorderWidth": "{palette.border.border2}",
        "textLine": "{system.textLine.underline}",
        "textLineStyle": "{system.textLineStyle.solid}"
      }
    },
eslimaf commented 2 years ago

Sorry for crashing in this discussion. I think the current theming structure complicates the implementation of native components and that was one of the reasons that VaderKit opted out of consuming those. The feeling that I have is that the control was inverted, instead of the component reaching out for theme, the theme is bringing logic into the component, which is not ideal. When my team started to write VaderKit we did an exercise to understand the dependency flow and by understanding the dependency flow we were able to identify the boundaries drawing a clear separation of concerns. Below is the compilation of the annotations that I took during our workshop. I hope it's useful

Token consumption and theming

Dependency layers

The dependency flow is very important for the proper separation of concerns when it comes to token design token hierarchy. The image below represents the dependencies flowing out, meaning that the otter layers should only depend on the inner layers. Dependency Flow

Global Tokens

These are the definitions of colours, the font files, font names. Global Token should be provided by UDS either via a package manager or submodules. It should be versioned and delivered in a format that can be consumed by the platform.

Opinionated Tokens

Some of the global tokens are not applicable or not do not map to mobile hence the need to bridge or transform those tokens into usable definitions for the platforms consuming them. An example is the definition of primary, secondary colours that are very common in the taxonomy of mobile UI components.

Themes

Themes are composed of a group of opinionated and global tokens and define a specific styling that can be consumed by the UI components.

Components

These are the actual implementations of a design. A component should avoid having static styling definitions and rely as much as possible on the Themes to receive the tokens used for taxonomy values definitions.

Component Taxonomy

To make it easier to visualize and define the necessary tokens that will drive the styling of a component a good taxonomy breakdown needs to be created. Button Taxonomy

With the above image as an example, it is noticeable that for each component a different set of semantic tokens will be necessary in order to represent the mutable values available for styling.

Implementation Example

Class Diagram Class Diagram Button Example

In the example, Colours and Fonts would be provided by UDS and can be mapped to Global Tokens. Theme and Typography should be provided by the library because the implementation differs on the platform level, and the tokens will start to have a more opinionated approach. To give a more concrete example of why the themes should be platform specific, if we look into the UDS theme posted in @nadaltelus comment

"Link": {
        "focus": "{appearances.Link.focus}",
        "hover": "{appearances.Link.hover}",
        "iconPosition": "{appearances.Link.iconPosition}"
}

those tokens are not used in the mobile, there's no focus or hover. So tying the theme definitions to a definition that was made for a different platform IMHO will only add to the complexity of maintaining a file that doesn't reflect the actual implementation. CustomTheme is the actual implementation of a Theme, this can be mapped to the Allium theme. Button will receive a Theme as constructor parameter, that allows for abstraction of the values that are now hidden using the opinionated tokens provided by theme. The component is responsible for all the logic and can trigger different variants based on the modifiers or additional parameter passed on the constructor method.

eslimaf commented 2 years ago

Can we assume that the structure of the tokens object is consistent across themes? -- Currently it is not, but that's where we wanna be. I think its valid to build it based on Telus (Allium) theme and then bring the other themes closer to it because its our main use case.

It will be very difficult if not impossible to have a single token structure to define both mobile and web. The jargons and monikers are different, the ways of setting values or even to represent values can differ. Using the current theme as a base can lead to unnecessary complexity just for the sake of generalization. Also, I don't think it's negotiable that the other implementations of a theme have a different structure, that would break the abstraction and require the components to account for those "missing" or "extra" tokens. The components should have a well defined behaviour that can be styled differently (variants) but maintain the same functionality.

jenncoop commented 2 years ago

Thank you so much for your insight/knowledge @eslimaf, this is very helpful!

Can we assume that the structure of the tokens object is consistent across themes? -- Currently it is not, but that's where we wanna be. I think its valid to build it based on Telus (Allium) theme and then bring >the other themes closer to it because its our main use case.

I think maybe it’s worth making a distinction between cross-theme/brand consistency and cross-platform consistency (ie. web vs. mobile). My original question was focused on understanding whether there was structural consistency across the Allium theme.json/palette.json files and the Koodo theme.json/palette.json files (and any other brands) to understand whether we can build general abstractions for these, especially for the set of default tokens for each component.

Regarding cross-platform web vs. mobile, @nadaltelus should we expect that UDS will provide mobile-specific versions of theme.json and palette.json for the various brands (ie. Allium, Koodo etc.), or will the native components library need to use the web-specific version of these files and adapt it for mobile UI conventions?

abraini-Nascent commented 2 years ago

It will be very difficult if not impossible to have a single token structure to define both mobile and web.

I agree. You would need to transpile the the tokens and themes, for each platform. Base tokens are defined differently on the platforms from size to font weight, then on the theme itself there are properties as Evandro mentioned that don't exist while other elements are named or applied differently. Not to mention rules...

will the native components library need to use the web-specific version of these files and adapt it for mobile UI conventions?

you commented while I was writing this :). yes this is what I was just typing up, who's responsibility is it too do this conversion? would a more plain domain language that transpiles to web&android&ios be another universal approach, tho more effort...

eslimaf commented 2 years ago

From @nbrooke

For reference, when I use the terms “palette” or “palette file” below, i’m generally referring to this file: https://github.com/telus/universal-design-system/blob/main/packages/palette-allium/palette.json, that gives names to actual numerical values for things (“Amber” is “#FACA69") and for “theme” or “theme file” I’m talking about this file: https://github.com/telus/universal-design-system/blob/main/packages/theme-allium/theme.json, which specifies bindings between components and those “raw” named values (the “backgroundColor” for “ProgressBar” is “greenAccessible”). There was some confusion in the meeting the other day about what stuff was in what files, I BELIEVE those two files full specify the appearance of components, but someone though that there might be other JSON files that are required, so if so I’d appreciate it if someone could point me at them.

It seems like a fundamental conflict we need to resolve in where things are specified in those various files is this:

Evandro’s suggestion of having a separate way of specifying the theme for mobile solves the second problem, but doesn’t really help with the first, if you do want to change the color a component uses, you would need to, as he said, open 3 PRs, one for the theme file on UDS, and one each for each of the iOS and Android mobile component libraries. Our original proposal of loading all data from the real palette/theme JSON files solves the first but not the second.

As an alternative, could we manage a hybrid approach that could maybe address both points, by separating, for the native mobile libraries, the notion of purely visual elements of the theme from the elements that affect layout, and treat those differently?

Setting aside for the moment exactly where the theme data is coming from (parse the original theme files vs. load some other data vs. specified in code in the native components libraries vs specified in code somewhere else), rather than specifying a single theme when initializing the native components, we could actually specify two things

This seems like an approach that could have a lot of advantages, it’s it something that seems reasonable to both the UDS team and the MyTelus team, such that we should consider it in more detail and start thinking about some of the specifics of how to actually implement something like that?

If not, does anyone else have any notion of how to resolve the conflict I mentioned above, or other thoughts on that dilemma?

[edited to restore the original formatting - nigel]

eslimaf commented 2 years ago

@nbrooke I agree with you. The approach you described is very similar to what Material and Spotify's design systems use. The separation of theming (colours) and layout (shapes, sizes) in my opinion is paramount to enable a MULTI platform design system. If the theme from UDS can provide that separation I don't see any problem using those definitions in the mobile platforms but reading the UDS theme.json that doesn't look like the case or at least not in a straight forward way.

"ProgressBar": {
      "appearances": {
        "inactive": "{appearances.ProgressBar.inactive}",
        "negative": {
          "type": "variant",
          "values": [true]
        }
      },
      "rules": [
        {
          "if": {
            "inactive": true
          },
          "tokens": {
            "backgroundColor": "{palette.color.greyCloud}",
            "gradient": "{system.gradient.none}",
            "outlineColor": "{palette.color.greyThunder}"
          }
        },
        {
          "if": {
            "negative": true
          },
          "tokens": {
            "backgroundColor": "{palette.color.redDark}",
            "gradient": "{system.gradient.none}",
            "outlineColor": "{palette.color.redDark}"
          }
        }
      ],
      "tokens": {
        "backgroundColor": "{palette.color.greenAccessible}",
        "borderRadius": "{palette.radius.radius12}",
        "gradient": "{palette.gradient.green}",
        "outlineColor": "{palette.color.greenAccessible}",
        "outlineWidth": "{palette.border.border1}"
      }
    }

To utilize the above definition a parser needs to be created to extract the rules, the default tokens and resolve links to palette.json and dependencies such as system in order to make those consumable for the mobile platforms. As UDS is not a multi platform but a cross platform the theme structure is extremely opinionated to work only with web paradigms. I think that a structural change would be necessary to achieve the benefits of having a theme that could be used across technologies without so much friction and complexity. Maybe the theme structure can be simplified as the one below making the variants specialization of a default definition, removing the if blocks and delegating the layout specific decisions (borderRadius, outlineWidth) to the platforms as you suggested.

Theme {
  // Enables easier branding and theme variation without the need to update the colours on EVERY single component
  SemanticColours {
    primary = palette.color.greenAccessible
    primaryVariant 
    secondary
    ...
  }
  // Component specific theming
  ProgressBar {
    DefaultTokens {
      backgroundColor = SemanticColours.primary
      outlineColor = SemanticColours.primary
    }
    // A variant can reach to Colours palette the side effect is to have to manually be adjusted when changing brands
    Inactive {
      backgroundColor = palette.color.greyCloud
    }

    Error {
      backgroundColor = SemanticColours.Error
      outlineColor = SemanticColours.Error
    }
  }
}
nbrooke commented 2 years ago

Yeah, I definitely agree that it would be very nice if the definitions were more semantic in nature rather than using more specific values everywhere. I think the sort of thing you are talking about there IS possible to represent in the design token files within the feature set available there, thought the current files definitely don't do that. I'd personally be very supportive of some refactoring of those files to try and represent some of those concepts better, but I'm not the person that could make the call on something like that.

To utilize the above definition a parser needs to be created to extract the rules, the default tokens and resolve links to palette.json and dependencies such as system in order to make those consumable for the mobile platforms.

The parser for that definitely isn't trivial, but I'm not sure it's terrible either, the layout is relatively straightforward, and the rules and lookups are done in simple / logical ways. I don't think parsing that file is some huge engineering challenge, it's arguably not worth it for one team that is only using one of those themes, but the whole point of this effort is to support ALL the themes all, so there are some potentially large advantages to doing that work int eh context of this effort.

Also, the simpler format you propose also has some of the same issues, in particular, you still need some matching of rules (we need to somehow represent that we are using the "inverted" variant and find the right definition to use for that. There are also things that the current theme definition can represent (that just aren't used in the specific example of the progress bar) that your proposed system might have a hard time representing. In particular, you totally could have a inactive AND inverted definition that set some different colours when BOTH variants are active:

"rules": [
        //...
        {
          "if": {
            "inactive": true,
            "negative": true,
          },
          "tokens": {
            "backgroundColor": "{palette.color.greyCloud}",
            "gradient": "{system.gradient.none}",
            "outlineColor": "{palette.color.redLight}"
          }
        },

delegating the layout specific decisions (borderRadius, outlineWidth) to the platforms as you suggested.

If we have a good way of representing different platforms in the themes, these COULD also be in those design token JSON files, again, it more a matter of them not being set up like that now than it being impossible to represent that.

nbrooke commented 2 years ago

We have an initial approach for this in development, so closing this bug, since this isn't really the spot to discuss this any more.