amzn / style-dictionary

A build system for creating cross-platform styles.
https://styledictionary.com
Apache License 2.0
3.87k stars 543 forks source link

Design Tokens (json) → XAML (for .NET MAUI) #977

Open alexfi1in opened 1 year ago

alexfi1in commented 1 year ago

I develop applications on the .NET MAUI platform. Is there any way to convert from design tokens (json) to XAML format?

dbanksdesign commented 1 year ago

@alexfi1in do you have an example output of XAML you are trying to achieve? It would help to see an example and we can work backwards from there.

AntonKosenkoDX commented 1 year ago

For the following json file

{
    "brown": {
    "50": {
      "value": "#EFEBE9",
      "type": "color"
    },
    "100": {
      "value": "#D7CCC8",
      "type": "color"
    },
    "200": {
      "value": "#BCAAA4",
      "type": "color"
    },
    "300": {
      "value": "#A1887F",
      "type": "color"
    },
    "400": {
      "value": "#8D6E63",
      "type": "color"
    },
    "500": {
      "value": "#795548",
      "type": "color"
    },
    "600": {
      "value": "#6D4C41",
      "type": "color"
    },
    "700": {
      "value": "#5D4037",
      "type": "color"
    },
    "800": {
      "value": "#4E342E",
      "type": "color"
    },
    "900": {
      "value": "#3E2723",
      "type": "color"
    }
  }
}

Converter should generate something like this:

<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">

    <Color x:Key="Brown50">#EFEBE9</Color>
    <Color x:Key="Brown100">#D7CCC8</Color>
    <Color x:Key="Brown200">#BCAAA4</Color>
    <Color x:Key="Brown300">#A1887F</Color>
    <Color x:Key="Brown400">#8D6E63</Color>
    <Color x:Key="Brown500">#795548</Color>
    <Color x:Key="Brown600">#6D4C41</Color>
    <Color x:Key="Brown700">#5D4037</Color>
    <Color x:Key="Brown800">#4E342E</Color>
    <Color x:Key="Brown900">#3E2723</Color>

    <SolidColorBrush x:Key="Brown50Brush" Color="{StaticResource Brown50}"/>
    <SolidColorBrush x:Key="Brown100Brush" Color="{StaticResource Brown100}"/>
    <SolidColorBrush x:Key="Brown200Brush" Color="{StaticResource Brown200}"/>
    <SolidColorBrush x:Key="Brown300Brush" Color="{StaticResource Brown300}"/>
    <SolidColorBrush x:Key="Brown400Brush" Color="{StaticResource Brown400}"/>
    <SolidColorBrush x:Key="Brown500Brush" Color="{StaticResource Brown500}"/>
    <SolidColorBrush x:Key="Brown600Brush" Color="{StaticResource Brown600}"/>
    <SolidColorBrush x:Key="Brown700Brush" Color="{StaticResource Brown700}"/>
    <SolidColorBrush x:Key="Brown800Brush" Color="{StaticResource Brown800}"/>
    <SolidColorBrush x:Key="Brown900Brush" Color="{StaticResource Brown900}"/>

</ResourceDictionary>

But this is very simple example, xaml styles also support a lot complex and nested properties. The documentation for xaml styles syntax may be found here: https://learn.microsoft.com/en-us/windows/apps/design/style/xaml-styles

chazzmoney commented 1 year ago

You could create a format and then use registerFormat. Something like this could get you started:

const XamlFormat = {
  name: 'xaml',
  formatter: function ({ dictionary, options }) {
    // Generate XAML content based on the design tokens in the dictionary
    let xamlContent = `<?xml version="1.0" encoding="UTF-8" ?>\n<?xaml-comp compile="true" ?>\n<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                               xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">`;

    // Generate the <Color> elements
    let colorElements = '';
    for (const tokenName in dictionary) {
      const tokenValue = dictionary[tokenName];

      // Create <Color> element based on the token name and value
      const colorElement = `<Color x:Key="${tokenName}">${tokenValue}</Color>`;
      colorElements += colorElement;
    }

    // Add the <Color> elements block to the XAML content
    xamlContent += colorElements;

    // Generate the <SolidColorBrush> elements
    let brushElements = '';
    for (const tokenName in dictionary) {
      // Create <SolidColorBrush> element based on the token name
      const brushElement = `<SolidColorBrush x:Key="${tokenName}Brush" Color="{StaticResource ${tokenName}}" />`;
      brushElements += brushElement;
    }

    // Add the <SolidColorBrush> elements block to the XAML content
    xamlContent += brushElements;

    // Close the ResourceDictionary tag
    xamlContent += `</ResourceDictionary>`;

    return xamlContent;
  }
};

module.exports = XamlFormat;

The "hard" part is really going to be how to decide what XAML element to create for each token, and then making sure to filter only to the tokens you want.

To use this format, you need to register it using the registerFormat function:

const StyleDictionary = require('style-dictionary');
const XamlFormat = require('./path/to/xaml-format');

StyleDictionary.registerFormat(XamlFormat);

After registering the format, you can use it when running StyleDictionary's build command, specifying xaml as the format:

style-dictionary build --format xaml

You can find more info on building a custom format here: https://amzn.github.io/style-dictionary/#/formats?id=custom-formats

hansmbakker commented 8 months ago

For my project I created a converter for MAUI.

Our approach is this: based on the JSON exported from Figma, it can generate C# classes with static properties.

The class members can be used in XAML like

{x:Static styles:ColorTokens.TextOnDefaultBackgroundStrong}

Another option would be to create a markupextension to reference them.

Properties can also reference each other (so that e.g. only color primitives have values, color aliases and color tokens are references to other static members - to prevent redundant calls to Color.FromRgba(...)):

public class ColorPrimitives
{
    public static Color Red = Color.FromRgba("#ff0000ff");
}

public class ColorTokens
{
    public static Color Warning = ColorPrimitives.Red;
}

The tokens are grouped in classes based on their path. This allows to have separate classes for colors, spacings (sizes for paddings and margins), effects (shadows), typography (font size, font family, weight, line height, etc).

What I haven't figured out yet is how best to create combinations of typography values: in Figma one typography token (e.g. BodyRegularXl, Heading01, BodyBoldSm) contains multiple values (font size, font family, weight, line height, etc). <style> only allows one TargetType unfortunately 🤦🏼‍♂️.

So it might require to create multiple styles for the same typography token- for Label, Button, Entry, etc.

@jorenbroekema @chazzmoney if I would want to contribute a converter for MAUI, would somebody be available to give the required feedback and assistance to get a PR merged?

Would others from this topic like to contribute?

Note: I still have to discuss internally how much we can share, but already wanted to check here as well, and I could already share our approach above.

Feedback to my approach is also appreciated! 🙏

jorenbroekema commented 8 months ago

Hi, yes you can tag me and I'll review it. Thanks for taking the steps towards contributing this!

If it's okay with you, I would prefer if you could use the v4 branch of style-dictionary, the latest should also allow you to use the reference utils that are now exposed to make it easier to put references/aliases in the output as you've described.

import { usesReferences, getReferences } from 'style-dictionary/utils';

// pseudo code to get the token value or platform-specific alias/reference if outputReferences is enabled
function compileTokenValue(token) => {
  let value = token.value;
  const original = token.original.value;
  if (options.outputReferences && usesReferences(original)) {
    const refs = getReferences(dictionary, original);
    refs.forEach(ref => {
      // '{foo.bar.qux}' replaced by FooBarQux but since you're grouping primitives, your implementation will differ slightly
      value = token.value.replace(new RegExp(`{${ref.path.join('.')}(.value)?}`), 'g'), ref.name)
    });
  }
  return value;
}

My advice would be to start with some tests, then write the implementation and create a draft PR and I'll help you finalize it

hansmbakker commented 8 months ago

Great! I'm still in the process of getting approval, sorry.

Regarding the tests and structuring the code I probably could use some help later.

hansmbakker commented 5 months ago

@jorenbroekema I started work on this in #1162 - I started on the basics with exporting colors. I'm exporting to C# rather than XAML, so that they are also usable in C#, and so that you can generate compilable code. If you want to have a separate issue for this, please let me know.