Aguarde...

25 de julho de 2020

Sincronizando estilos Figma com CSS em JS

Sincronizando estilos Figma com CSS em JS

Enquanto crio novos sistemas de design com CSS em bibliotecas JS como Styled Components ou Emotion, me deparei repetidamente tendo que traduzir tokens de design do Figma em um tema baseado em JSON para código. Vou montar um sistema de design inteiro dentro do Figma e depois passar horas copiando os tokens de design em um objeto de tema estruturado. Então, se eu atualizar os tokens de design no código do tema, tenho que sincronizar as alterações com o documento Figma – tudo manualmente.

Desde que o Figma abriu sua API para desenvolvedores de plugins, achei que seria uma boa oportunidade para aprender a API do Figma e criar uma ferramenta para acelerar a conversão de tokens de design entre o Figma e o CSS nas bibliotecas JS.

Criei um plug-in do Figma chamado Styled Theme Generator que converte os estilos de cores e texto do Figma em um CSS no tema JS, ou vice-versa, importando um tema e gerando os estilos do Figma. Analisarei o processo e o que descobri ao criar este plugin.

Confira o código fonte aqui ou continue lendo para saber mais!

Usando o plugin

Antes de nos aprofundarmos no porquê ou como por trás do plugin, vamos ver como usá-lo.

  1. Faça o download ou clone o repositório git do plug-in
  2. Vá para as configurações de Figma Plugins e adicione um novo plugin usando o manifest.json
  3. Crie um novo ou abra um documento Figma existente com estilos
  4. Execute o plug-in (Plugins> Desenvolvimento> styled-theme-generator)
  5. A janela do plugin será exibida. Há duas seções nas quais você pode converter os estilos Figma do documento em JSON ou importar um tema JSON para gerar estilos Figma.

Tente importar este tema JSON de amostra para preencher o documento com estilos de cores:

{
    "colors": {
        "white": "#FFFFFF",
        "black": "#000000"
    }
}

Em seguida, edite uma das cores e converta os estilos novamente em JSON.

Muito melhor do que o manual, certo? 🏎💨

Como funciona?

O plug-in usa a API do Figma para ler, criar e editar estilos. A API é fornecida aos plugins pela Figma por meio de uma figmavariável, na qual você pode executar métodos como getLocalTextStylesobter todos os estilos de texto do documento.

Eu giro ou analiso um objeto JS, restringindo ou analisando um objeto para JSON. O JSON deve seguir uma determinada especificação de tema (veja abaixo), facilitando o processo de importação / análise.

📦 Configuração do plugin Figma

Eu usei o projeto inicial da interface do usuário no tutorial de plug-in do Figma , gerado no menu “Novo plug-in”. Estilei o aplicativo usando o figma-plugin-ds , uma biblioteca de Thomas Lowry que replica a aparência da interface do Figma (e fornece algumas classes úteis de utilitário).

🏗 Estrutura do tema

A especificação do tema é System UI , usada por bibliotecas como Styled System ou xStyled e bibliotecas de componentes como Chakra UI ou Theme UI. Também estou usando a API variante do Styled System para criar estilos de texto agrupados.

Aqui está um exemplo da estrutura do tema com a qual trabalharemos:

export const theme = {
  colors: {
    text: "#111212",
    background: "#fff",
    primary: "#005CDD",
    secondary: "#6D59F0",
    muted: "#f6f6f9",
    gray: "#D3D7DA",
    highlight: "hsla(205, 100%, 40%, 0.125)",
    white: "#FFF",
    black: "#111212",
  },

  gradients: {
    subtle: `linear-gradient(180deg, ${colors.primary} 0%, ${colors.secondary} 100%)`,
    purple: `linear-gradient(180deg, ${colors.primary} 0%, #A000C4 100%)`,
    blue: `linear-gradient(180deg, #00D2FF 0%, ${colors.secondary} 100%)`,
  },

  // Typography
  fonts: {
    body: "Roboto, Helvetiva Neue, Helvetica, Aria, sans-serif",
    heading: "Archivo, Helvetiva Neue, Helvetica, Aria, sans-serif",
    monospace: "Menlo, monospace",
  },
  fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 96],
  fontWeights: {
    body: 400,
    heading: 500,
    bold: 700,
  },
  lineHeights: {
    body: 1.5,
    heading: 1.25,
  },

  // Text Variants
  text: {
    h1: {
      fontFamily: fonts.heading,
      lineHeight: "1.25",
      fontSize: [6, 6, 6, 6],
      marginBottom: 3,
    },

};

export default theme;

A estrutura do tema possui uma colorspropriedade que é um mapa de hash de cada token de cor ( colors: { white: "#FFF" }). Isso nos permite fazer um loop sobre ele ao importar ou mapear um fora enquanto percorre as cores do documento.

Para estilos de texto, teríamos que usar algumas propriedades na estrutura do tema, de fontSizepara fontsaté text. A fontSizeseria um array de inteiros que representam toda a escala da fonte, facilmente gerada com uma matriz de-enganados. A fontspropriedade é um mapa de hash, semelhante às cores, onde cada opção de fonte é uma propriedade (geralmente denominada semanticamente como bodyou header).

Onde o trabalho realmente acontece é a textpropriedade. Aqui é onde podemos armazenar “variantes” de texto, ou grupos de propriedades CSS, que podem ser rapidamente aplicadas aos componentes por meio de um variantsuporte. Isso permite <Heading variant="h1">estilizar o componente com um h1estilo (que pode ser um tamanho de fonte maior, peso de fonte em negrito, etc.).

Ao percorrer os estilos de texto para criar a textpropriedade, importamos / geramos as propriedades fontSizee correspondentes fonts. Também podemos associar os dois graças aos getters de tema “mágicos” da Styled Systems dentro de certas propriedades do tema (como variantes). Isso permite que você escreva coisas como fontFamily: "body"e ele pega a bodypropriedade da theme.fontspropriedade. Graças a isso, podemos gerar o JSON muito mais fácil, sem ter que realmente usar um AST para associar as variáveis.

📖 Obtendo estilos de texto

Leitura de Figma

Os estilos de texto estão disponíveis executando o getLocalTextStylesmétodo na figmaclasse de plug – in. Isso retorna uma matriz de objetos TextStyle . Esses objetos contêm uma namepropriedade que é o nome que você define na interface do usuário.

Se você usar uma estrutura aninhada para estilos (como Header/H1Header/H2), os nomes ficarão assim – /separados. Você pode supor que, uma vez que os estilos se aninham, eles seriam representados por uma matriz de estilos semelhantes (como todos os Headerestilos em uma matriz “agrupada”, na matriz raiz retornada por getLocalTextStyles). Mas eles não têm, é uma matriz plana , então você precisa analisar o nome e garantir a verificação do separador.

// This shows the HTML page in "ui.html".
figma.showUI(__html__);

figma.ui.onmessage = (msg) => {
  if (msg.type === "generate-styles") {
    // Get text styles to generate text variants
    const textStyles = figma.getLocalTextStyles();

    const textVariants = textStyles.map(
      ({
        id,
        name,
        fontName,
        fontSize,
        letterSpacing,
        lineHeight,
        textCase,
        textDecoration,
      }) => ({
        name,
        fontFamily: fontName!.family,
        fontWeight: fontName.style
        fontSize,
        letterSpacing,
        lineHeight,
        textCase,
        textDecoration,
      })
    );

        console.log('text styles', textVariants)
  }

  figma.closePlugin();
};

Fornece um objeto como:

;[
  {
    name: 'Header',
    fontFamily: 'Roboto',
    fontWeight: 'Regular',
    fontSize: 24,
    letterSpacing: { unit: 'PERCENT', value: 0 },
    lineHeight: { unit: 'AUTO' },
    textCase: 'ORIGINAL',
    textDecoration: 'NONE',
  },
  {
    name: 'Paragraph',
    fontFamily: 'Roboto',
    fontWeight: 'Regular',
    fontSize: 12,
    letterSpacing: { unit: 'PERCENT', value: 0 },
    lineHeight: { unit: 'AUTO' },
    textCase: 'ORIGINAL',
    textDecoration: 'NONE',
  },
]

Extraindo tamanhos de fonte

Um arquivo de tema depende de uma escala de tamanho de fonte que geralmente é um valor de matriz de números inteiros ( const fontSizes = [8,16,24,32,40,48]). Para criar um arquivo de tema adequado, precisamos associar os valores de tamanho de fonte com base em pixel (da Figma) à matriz com base em número inteiro.

Temos que percorrer os estilos de texto e criar uma matriz de tamanho de fonte, o que deve ser fácil durante a análise inicial. Em seguida, podemos analisar o arquivo de tema gerado e substituir qualquer valor de pixel pela referência à matriz de tamanho da fonte.

// Get text styles to generate text variants
const textStyles = figma.getLocalTextStyles()

// Parse font sizes
// Create array of font sizes and sort numerically by least to most
const fontSizesWithDupes = textStyles
  .map(({ fontSize }) => fontSize)
  .sort((a, b) => a - b)
// Remove dupes
const fontSizes = fontSizesWithDupes.filter(
  (item, index) => fontSizesWithDupes.indexOf(item) == index
)

Isso também pode ser feito para outros valores semelhantes, como famílias de fontes, pesos, etc. Exceto que estes sejam armazenados como um mapa de hash , não como uma matriz. Isso é feito usando o reducemétodo da matriz para mapeá-lo para um objeto vazio:

// Parse font families
// Create array of font sizes and sort numerically by least to most
const fontFamilies = textStyles
  .map(({ fontName }) => fontName!.family)
  .sort()
  .reduce((map, obj) => {
    map[obj.toLowerCase()] = obj
    return map
  }, {})

Observe aqui que apenas pegamos o nome da fonte e usamos como chave do objeto (ou propriedade). Eu debati sobre essa implementação, quer note ou tente gerar convenções de nomes semânticos (como cabeçalho vs fonte do corpo), mas optei por usar apenas o nome da fonte. Ele permite que os desenvolvedores entrem no tema e localizem e substituam mais facilmente a propriedade por um nome semântico. Isso parecia melhor do que alternativas, como criar uma matriz e acessar fontes usando fonts[2].

Variantes de manipulação

Para as variantes ou a textpropriedade do tema, tivemos que fazer um loop nos mesmos estilos de texto do Figma com os quais geramos fontes e dimensionamento de fontes. Aqui estão algumas notas principais:

  • As variantes não estão aninhadas no arquivo do tema. Isso significa que, apesar de os estilos de texto serem aninhados no Figma, eles não devem se aninhar no arquivo do tema (por exemplo, “Parágrafo / Corpo” não deve se tornar paragraph: { body : {} }). Se uma variante é baseada em outra variante (como a versão em negrito de um estilo de fonte que precisa alterar apenas a propriedade negrito), você deve “estender” a partir de outras variantes (em vez de aninhar).
  • O tamanho da fonte e as fontes devem estar relacionados às variáveis fontSizesfontsque geramos usando a sintaxe “mágica” (números inteiros para o tamanho da fonte ou a string do nome da propriedade da fonte).
  • Barras não são usadas para nomes de propriedades de objetos JS. Eles podem ser, mas não é uma prática comum. Ao analisar o nome do estilo de texto da Figma, se ele contém barras, temos que converter para um símbolo diferente (como um ponto ou barra).

Por exemplo , Paragraph/Bodytorna-se paragraph-body, assim parece <Text variant="paragraph-body">. Como essa é uma decisão muito opinativa, deve haver uma lista suspensa ou entrada que permita aos usuários selecionar diferentes opções.

Também é difícil fazer isso durante a importação, pois os arquivos de tema provavelmente não seguirão muito essa convenção; portanto, as importações provavelmente serão agrupadas pelo componente associado (o que ainda é superficial, pois as variantes podem ser usadas em vários componentes).

Agora que eu tinha uma idéia melhor de como lidar com a implementação, tentei algumas versões diferentes.

Versão separada por pontos

Eu criei isso primeiro como uma solução experimental para o problema de estilo aninhado.

const textVariants = textStyles
      .map(
        ({
          name,
          fontName,
          fontSize,
          letterSpacing,
          lineHeight,
          textCase,
          textDecoration,
        }) => ({
          name,
          fontFamily: `${fontName!.family}`,
          fontWeight: `${fontName.style}`,
          fontSize,
          letterSpacing,
          lineHeight,
          textCase,
          textDecoration,
        })
      )
      .reduce((map, obj) => {
        map[obj.name.replace("/", ".").toLowerCase()] = obj;
        return map;
      }, {});

Mas depois que me atualizei com a versão mais recente da API do Styled System, eles realmente mudaram a maneira como lidam com as variantes de um nível de tema para um componente. Isso significa que é mais recomendável ter tipos diferentes de variantes, em vez de compartilhá-las em todos os componentes.

Devido a essa alteração, parece que um objeto aninhado seria uma boa idéia, pois poderia permitir que os desenvolvedores pegassem estilos agrupados (como todos os outros Header/H1 Header/H2etc) e os adicionassem a um <Heading>componente. Mais modular do que ter todos os componentes com acesso a versões separadas por pontos / traços (por exemplo <Text variant="heading.h2">).

Versão do objeto aninhado

Com esta versão, precisamos fazer um loop sobre os estilos de texto, pegar o nome de cada estilo e usá-lo splitpara separar o nome pelo /. Em seguida, pegamos o conjunto de “pedaços de nome” e usamos o redutor para mapeá-lo em um objeto. Mas, para fazer isso, também usamos uma função recursiva para percorrer o objeto, chegar à propriedade mais profundamente aninhada e adicionar a próxima propriedade lá. Isso nos permite pegar algo parecido Paragraph/Bodye convertê-lo em textVariants: { paragraph: { body: {} } }.

Você pode ver um detalhamento da walkObjectfunção recursiva abaixo.

// Parse text variants
let textVariants = {};
textStyles.map(
  ({
    name,
    fontName,
    fontSize,
    letterSpacing,
    lineHeight,
    textCase,
    textDecoration,
  }) => {
    // Parse name from Figma slash `/` to object `.`
    let filteredName = name;
    if (flagLowercaseNames) filteredName = filteredName.toLowerCase();
    const nameArray = filteredName.split("/");

    const textNameReducer = (accumulator, currentValue, index) => {
      if (index == nameArray.length) {
        return walkObject(accumulator, "");
      }
      return walkObject(accumulator, currentValue, true);
    };
    let textObject: object = nameArray.reduce(textNameReducer, {});

    const textVariant = {
      fontFamily: `${fontName!.family}`,
      fontWeight: `${fontName.style}`,
      fontSize: getFontSize(fontSize),
      letterSpacing,
      lineHeight,
      textCase,
      textDecoration,
    };

    textObject = walkObject(textObject, textVariant);
    textVariants = merge(textVariants, textObject);
  }
);

Acabei ficando com a versão do objeto aninhado, mas, à medida que permaneço nela, quero sinceramente aprender mais sobre a sintaxe do ponto. É muito mais fácil e rápido no final da geração do código – e mais fácil e rápido no final do desenvolvedor (permitindo que eles copiem e continuem – em vez de analisar manualmente as variantes nos componentes apropriados). Especialmente porque são apenas estilos de texto, você basicamente tem de 1 a 3 componentes aos quais essas variantes se aplicam (caixa, texto, talvez um título?).

🎨 Obtendo estilos de cores

Com base nos tipos PaintStyle e Paint . Eles podem ser acessados ​​usando o getLocalPaintStylesmétodo, que retorna uma matriz de PaintStyleobjetos.

// Get colors
const colors = figma.getLocalPaintStyles()

colors.map(({ paints, type, remote, name }) =>
  console.log(JSON.stringify({ paints, type, remote, name }))
)

Dá-lhe algo como:

{
        paints: [
          {
            type: "SOLID",
            visible: true,
            opacity: 1,
            blendMode: "NORMAL",
            color: {
              r: 0.7686274647712708,
              g: 0.7686274647712708,
              b: 0.7686274647712708,
            },
          },
        ],
        type: "PAINT",
        remote: false,
        name: "Gray",
      }

Tipos de manipulação

Como você pode ver acima, os objetos na paintsmatriz têm uma typepropriedade Isso permite que você saiba se a cor é sólida ou gradiente. Podemos verificar isso usando uma função curta que usa o Typecript para comparar o tipo com uma enumeração associada (como um mapa de hash):

/**
 * Describes a Figma paint type retrieved from the Figma API.
 * @ignore
 */
const enum FigmaPaintType {
  Solid = 'SOLID',
  GradientLinear = 'GRADIENT_LINEAR',
}

const isFigmaLinearGradient = (paint: FigmaPaint): paint is GradientPaint => {
  return paint.type === FigmaPaintType.GradientLinear
}

const isFigmaSolid = (paint: FigmaPaint): paint is SolidPaint => {
  return paint.type === FigmaPaintType.Solid
}

if (isFigmaSolid(paint)) {
}
if (isFigmaLinearGradient(paint)) {
}

Originalmente, encontrei esse código na base de código da CLI do Diez , mas eles estavam usando suas próprias digitações Figma personalizadas (desde que ele foi desenvolvido antes da digitação oficial do Typma). Toquei no código e garanti que ele usasse os tipos oficiais de Figma.

Convertendo o formato Figma para CSS RGBA / HEX / HSL

As cores da Figma estão no formato RGB, mas são armazenadas como um valor de 0 a 1, em vez de 0 a 255 como o formato RGB padrão. Isso significa que o valor da cor Figma precisa ser convertido:

const { r, g, b } = paint.color
let newColor = `rgba (${Math.round(r * 255)}, ${Math.round(
  g * 255
)}, ${Math.round(b * 255)}, ${paint.opacity})`

Que é então facilmente usado com bibliotecas como TinyColor:

newColor = Color(newColor).toHexString()
// Gives HEX version of color

newColor = Color(newColor).toRgbString()
// Gives RGBA version of color

newColor = Color(newColor).toHslString()
// Gives HSL version of color

Eu criei uma instrução switch no método de análise de cores para lidar com essas diferentes conversões. Idealmente, posso adicionar uma entrada à interface do usuário que permita aos usuários selecionar qual formato de cor eles preferem. Por enquanto, porém, defino o hex como padrão.

Manipulando Separadores / Estilos Aninhados

Geralmente, os estilos e as cores são organizados em uma estrutura de objeto aninhada, como:

const theme = {
    textVariants: {
        h1: {
            fontFamily: "Roboto",
        }
    }
    colors: {
        brand: {
            primary: "blue",
            secondary: "purple"
        }
    }

É importante pegar os estilos Figma que apresentam separadores (como Header/H1) e dividi-los em objetos aninhados. Podemos fazer isso usando o split()método no Stringprotótipo:

const colorArray = name.split('/')

Agora, temos uma matriz de nomes “partes” que precisamos converter em um objeto aninhado, em que cada nome de propriedade é um segmento do nosso nome. Deve ficar assim:

const obj = {
  Header: {
    H1: '',
  },
}

Essa função precisa ser recursiva, o que significa que será executada repetidamente até que uma determinada condição seja atendida. Nesse caso, percorremos os parâmetros do objeto (usando Object.keys()) e verificamos se o parâmetro é um objeto. Nesse caso, executamos a função novamente.

Também precisamos definir o valor do objeto profundamente aninhado. Portanto, enquanto estamos percorrendo o objeto profundamente, precisamos verificar se é a última parte do nome do estilo. Se for o último, você define o parâmetro para um valor passado para a função. Dessa forma, ele percorre recursivamente até que não haja mais elementos de matriz (ou segmentos de nome). Verificamos o último elemento da matriz usando um redutor, em vez da função “andar de objeto” real, para separar a lógica (uma vez que requer conhecimento da matriz original, que a função “andar de objeto” não terá).

/**
 * Loops through a nested object to set the last objects param or value
 *
 * @param obj
 * @param newValue
 * @param isKey
 */
function walkObject(obj: object, newValue: string, isKey: boolean = false) {
  const keys = Object.keys(obj)

  // If it's the top level, create first param
  if (keys.length === 0) {
    obj[newValue] = {}
  }

  // Loop through objects parameters
  keys.forEach(function (key, i) {
    // Only do the first for perf reasons
    if (i === 0) {
      let value = obj[key]

      // If it's an object, recursively run again
      const nestedKeys = Object.keys(value)
      if (typeof value === 'object' && nestedKeys.length > 0) {
        walkObject(value, newValue, isKey)
      } else {
        // Set param or value of nested object
        if (isKey) {
          obj[key][newValue] = {}
        } else {
          obj[key] = newValue
        }
      }
    }
  })

  return obj
}

O redutor:

const colorNameReducer = (accumulator, currentValue, index) => {
  if (index == colorArray.length) {
    return walkObject(accumulator, '')
  }
  console.log('creating param', accumulator, currentValue)
  return walkObject(accumulator, currentValue, true)
}
let colorObject = colorArray.reduce(colorNameReducer, {})

O redutor da matriz aceita um objeto vazio, o que permite criar um novo objeto como base para a redução. Usamos esse objeto vazio como um accumulator, que executa a função “caminhar objeto”.

Isso retorna um objeto para cada cor que se parece com isso:

{
    "Black": "#000",
}

// Or for nested
{
    "Brand": {
        "Primary": "blue",
    }
}

Isso então pode ser combinado posteriormente:

let finalColors = {}

// loop here

finalColors = { ...finalColors, colorObject }

Fusão superficial vs profunda

O grande problema aqui é que os dados são ” superficialmente mesclados “. Isso significa que, se você tiver algum objeto aninhado, como o Brand.Primarydescrito acima, o perderá se mesclar outro objeto com uma estrutura semelhante (como Brand.Secondary).

O primeiro instinto é usar o finalColorsobjeto como base para o redutor, em vez de um objeto vazio. O problema aqui é que o redutor foi projetado para passar pelo objeto até a última propriedade e somente a primeira propriedade de cada função (por razões de desempenho).

Em vez disso, podemos usar uma função de mesclagem profunda para mesclar com segurança os vários objetos sem perda de dados.

Mas para habilitar isso, preciso alterar a configuração do meu plug-in Figma.Por usar um dos modelos básicos de plug-in do Figma, fornecidos na página de documentação / introdução, não consigo usar os módulos JS (local Arquivos .JS com exportou arquivos NPM).

Você recebe o seguinte erro, que levou um tempo para descobrir esse problema:

Error: Syntax error on line 1: Unexpected token
    at runPluginCodeInternal (figma_app.184c709c00d0e7d1014eeb96b8405934.min.js:900)
    at /file/rMHyOhwndcGNtvaJdZvodR/async https:/www.figma.com/figbuild/symlinks/figma_app.184c709c00d0e7d1014eeb96b8405934.min.js:900

Teve que fazer o downgrade de uma dependência para que o Webpack funcionasse conforme os documentos do Figma especificados. Veja o problema aqui no Github ."html-webpack-plugin": "3.2.0",

Também tive problemas com deepmerge e Typcript também. Não foi possível importar por algum motivo, o TS continuou dando erro sobre os tipos ausentes de módulos (apesar de ter tipos). Verificando os problemas, parece que o Webpack foi um GRANDE problema:

Então, em vez disso, copiei o módulo para o meu projeto em uma utilspasta e adaptei o módulo à sintaxe de importação / exportação. Isso funcionou muito melhor com o Webpack. E, felizmente, não estou muito preocupado em ter que atualizar o módulo, já que é uma daquelas bibliotecas do NPM que estão “prontas” e raramente atualizadas.

Eu também adicionei esModuleInteropcomo truepara tsconfig.json:

{
  "compilerOptions": {
    "target": "es6",
    "typeRoots": ["./node_modules/@types", "./node_modules/@figma"],
    "esModuleInterop": true
  }
}

⬇️ Importando temas

Sincronizando estilos Figma com CSS em JS

Depois que descobri o processo de leitura do texto e dos estilos de cores da Figma, mudei para o processo de importação. Adicionei uma <textarea>entrada ao código de interface do usuário do plug-in, onde o usuário pode colar um objeto JSON de seu tema.

<div class="section-title">Import</div>
  <div id="msg" class="hidden row mb-xsmall">
    <div class="icon icon--warning-large icon--red"></div>
    <div id="msg-text" class="type type--small ml-xsmall"></div>
  </div>
  <button id="generate" class="button button--secondary">
    Import Figma styles from theme JSON 👇
  </button>
  <div class="section-title">Theme JSON</div>
  <div class="input">
    <textarea id="theme" class="textarea" rows="2"></textarea>
  </div>

Isso é enviado para o código do plug-in “back-end”, que analisa o tema nos estilos Figma. Também fazemos uma verificação para ver se nenhum tema é fornecido e informamos o usuário com uma mensagem de erro.

document.getElementById('generate').onclick = () => {
  // Clear errors
  const errorBox = document.getElementById('msg')
  errorBox.classList.remove('flex')
  errorBox.classList.add('hidden')

  const textbox = document.getElementById('theme')

  // Check if theme is empty before sending
  if (textbox.innerHTML !== '') {
    parent.postMessage(
      { pluginMessage: { type: 'generate', theme: textbox.innerHTML } },
      '*'
    )
  } else {
    const errorBox = document.getElementById('msg')
    errorBox.classList.remove('hidden')
    errorBox.classList.add('flex')
    const errorText = document.getElementById('msg-text')
    errorText.innerHTML =
      'No theme found. Please copy your theme inside the text box.'
  }
}

No código de back-end, podemos acessar isso usando o themesuporte no msgobjeto Figma (um tipo de “resposta” da entrada / front-end).

figma.ui.onmessage = async (msg) => {
  if (msg.type === 'generate') {
    const theme = JSON.parse(msg.theme)
    console.log('the theme', theme)
  }
}

Como já conhecemos a estrutura do tema, podemos fazer um loop sobre as matrizes e os mapas de hash de acordo. Aqui está um exemplo do importador de estilos de texto:

// Get existing document styles to check later
const localTextStyles = figma.getLocalTextStyles()

// Loop through text styles
// Note that this is an async function
Object.keys(theme.text)?.map(async (name) => {
  const themeFont = theme.text[name]
  // See if name exists in document styles
  const localStyle = localTextStyles.find(
    ({ name: localName }) => localName === name
  )
  // Use existing style or create new one
  const textStyle = localStyle || figma.createTextStyle()

  // Prepare the fontName object
  const fontName = {
    family: theme.fonts[themeFont.fontFamily],
    style: themeFont.fontStyle ? themeFont.fontStyle : 'Regular',
  }

  // Update/mutate the text style's properties
  textStyle.name = name
  // Load font before you apply the property
  // You must await or Figma will not apply the font proper
  await figma.loadFontAsync(fontName)
  textStyle.fontName = fontName
  textStyle.fontSize = themeFont.fontSize
  textStyle.letterSpacing = themeFont.letterSpacing
  textStyle.lineHeight = themeFont.lineHeight
  textStyle.textCase = themeFont.textTransform
  textStyle.textDecoration = themeFont.textDecoration
  console.log('text style', textStyle)
})

Levei um minuto para descobrir como criar estilos Figma. A documentação oficial lista o método createTextStyle(), mas não informa o que fazer com ele. Tentei passar o objeto de fonte para ele e ele retrocedeu com alguns erros. Então tentei apenas executar a função por si só e um estilo em branco apareceu na barra lateral do Figma (sem nome, sem propriedades). Finalmente descobri que você cria esse estilo de texto e apenas modifica suas propriedades, muito semelhante à criação de um elemento com JS e ao usar a referência de elemento para alterar seus atributos / innerHTML.

Da mesma forma, tentei definir o nome da fonte e chamá-lo por dia (desde que eu estava usando o Roboto, uma fonte que eu sabia que tinha no meu computador). Figma retrocedeu com um erro muito informativo afirmando que eu precisava carregar a fonte primeiro. Pesquisando isso, descobri que tinha que usar awaitloadFontAsyncmétodo antes de aplicar a propriedade. Isso funcionou muito bem.

Se você está procurando uma representação funcional mais limpa desse código, consulte o plugin figma-markdown-parser ou figma-theme-ui .

Pensamentos finais

Tem sido interessante abordar os sistemas de design da perspectiva da geração de código. Sou forçado a pensar em todos os casos de uso, que começam a descrever as melhores práticas.

Ao trabalhar em vários sistemas de design, você pode entender melhor o escopo necessário de um sistema básico. Coisas que pareciam “semânticas” e “lógicas” em um sistema rapidamente se tornam “gargalos” e “limitações” em outro.

Também foi legal sonhar em como levar essa tecnologia ainda mais longe para criar melhores integrações.

Propriedades de estruturação

Por exemplo, estou usando a interface do usuário do sistema (também conhecida como Styled System) como base para a estrutura do tema. Para o peso da fonte , eles usam nomes semânticos (pesos do corpo versus cabeçalho). Em alguns sistemas, isso não faz sentido, pois uma fonte de cabeçalho pode compartilhar o mesmo peso que uma fonte de corpo.

Em vez disso, os pesos devem ser uma escala de fino a grosso, separando a propriedade dos detalhes da implementação: fontWeights = [ 300, 400, 700 ]

Ou, se você deseja manter as convenções de nomeação semântica, inclua-se na nomeação que descreve a propriedade – e não onde ela existe: fontWeights = [ thin: 300, regular: 400, bold: 700 ]

Manuseio de componentes

Eu pude escopo outras idéias enquanto eu cavava na API Figma, uma delas sendo componentes. Como você analisaria os componentes Figma ou React e geraria o outro?

Isso é realmente difícil em um design e no final do Figma. Design não é igual a código. Como designer, muitas vezes precisamos fazer coisas que podem não fazer sentido no escopo do DOM, porque estamos trabalhando dentro do paradigma do sistema de design. Como observei uma infinidade de kits de interface do usuário para sistemas de design na Figma, as estruturas dos componentes diferem drasticamente. Alguns componentes usam o layout automático, alguns contêm componentes extras de “dimensionamento” para lidar com o preenchimento, outros podem ser mais parecidos com Sketch e ter várias camadas para itens como background / state / etc.

O que eu tenho interesse em explorar são os componentes de layout automático e a extração deles em código de várias maneiras.

Em primeiro lugar, eu gostaria de pegar o preenchimento e a margem do layout automático e adicioná-los à spacingpropriedade do tema. Isso preencheria um tema com todos os valores de espaçamento padrão (idealmente seguindo algumas escalas como 4, 8, 16, 32).

Eu também estaria interessado em usar componentes mais simples que usam apenas uma camada de layout automático (geralmente o próprio quadro do componente) e tentar gerar um componente React com base nisso. Teríamos acesso ao preenchimento / margem do layout automático, bem como aos estilos de texto e cor. E tudo isso pode ser associado aos valores do tema quando o documento for analisado completamente (como relacionar estilos de texto a tokens de design acima usando a sintaxe “mágica” do Styled System). Eu conseguia imaginar componentes parecidos com:

<Box
  variant="heading.h1"
  sx={{
    color: 'primary',
    m: 2,
    px: 3,
  }}
>
  Component text
</Box>

Variações de componentes

Dando um passo adiante no último exemplo, assumindo que podemos obter as propriedades variantsxde cada componente Figma, poderemos gerar variações no nível do componente. Você pode analisar componentes, pegar cada /nome separado de barra ( ) e criar variações diferentes. Então, se você tivesse Button/Disabledvs Button/Hovered, combinaria os dois conjuntos de variantes ( disabledhovered) em um buttonobjeto. Isso pode ser usado dentro do componente para criar todas as várias variantes.

Torna-se um pouco mais complexo, pois os componentes no Figma são geralmente classificados em grupos profundamente aninhados. Você provavelmente verá um componente parecido com Button/Text/Large/DisabledButton/Icon/Small/Hovered. Aqui, nossa hierarquia de componentes se ramifica primeiro do ícone versus texto para uma escala de dimensionamento, depois para o estado do componente. Idealmente, gostaríamos de criar uma variante para cada uma. Os estados dos componentes seriam sua própria variante ( state), o dimensionamento seria outro ( size) e um para type. O único problema aqui é que, quando processarmos cada um desses componentes, teremos estilos duplicados de outras variantes, pois cada componente será uma mistura de várias variantes. Para descobrir quais valores são compartilhados em cada grupo, você pode armazenar cada componente em uma matriz de grupos (comoconst largeComponent = [ styles1, component2 ]) Em seguida, pegue essas matrizes e compare algumas (ou todas?) Com quais propriedades são duplicadas e, em seguida, armazene-as como a variante. Demora um pouco de processamento, especialmente se um documento possui muitos componentes, mas parece factível.

Obter conversão!

Espero que isso ajude você a acelerar seu fluxo de trabalho com CSS em temas JS e a eliminar mais traduções entre suas equipes de design e desenvolvedor! Se você tiver algum problema, sinta-se à vontade para abrir um no Github ou entrar em contato comigo no Twitter com qualquer dúvida. Ainda estou trabalhando em algumas distorções e recursos, por isso não sugeriria usá-lo ainda em produção.

Postado em Blog
Escreva um comentário