Skip to content

Commit

Permalink
Add raw ColorsVariablesLoader
Browse files Browse the repository at this point in the history
Update ExportColors

Update params name

Update params name

Small refactoring

Small refactoring

Add doc

Refactoring

Refactoring
  • Loading branch information
alexey1312 committed Apr 3, 2024
1 parent bb5d949 commit 45960ad
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 32 deletions.
22 changes: 21 additions & 1 deletion CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Specification of `figma-export.yaml` file with all the available options:
```yaml
---
figma:
# Identifier of the file containing light color palette, icons and light images. To obtain a file id, open the file in the browser. The file id will be present in the URL after the word file and before the file name.
# [required] Identifier of the file containing light color palette, icons and light images. To obtain a file id, open the file in the browser. The file id will be present in the URL after the word file and before the file name.
lightFileId: shPilWnVdJfo10YF12345
# [optional] Identifier of the file containing dark color palette and dark images.
darkFileId: KfF6DnJTWHGZzC912345
Expand Down Expand Up @@ -39,6 +39,26 @@ common:
# [optional] If useSingleFile is true, customize the suffix to denote a dark high contrast color. Defaults to '_darkHC'
darkHCModeSuffix: '_darkHC'
# [optional]
variablesColors:
# [optional] Identifier of the file containing primitives color components
primitivesFileId: KfF6DnJTWHGZzC912345
# [optional] Name of the column containing color variables in the primitive table. If a value is not specified, the default values ​​will be taken
primitivesCollectionName: Collection_1
# [required] Identifier of the file containing tokens color components
tokensFileId: shPilWnVdJfo10YF12345
# [required] Name of the column containing light color variables in the tokens table
tokensLightCollectionName: Light
# [optional] Name of the column containing dark color variables in the tokens table
tokensDarkCollectionName: Dark
# [optional] Name of the column containing light high contrast color variables in the tokens table
tokensLightHCCollectionName: Contast Light
# [optional] Name of the column containing dark high contrast color variables in the tokens table
tokensDarkHCCollectionName: Contast Dark
# [optional] RegExp pattern for color name validation before exporting. If a name contains "/" symbol it will be replaced by "_" before executing the RegExp
nameValidateRegexp: '^([a-zA-Z_]+)$'
# [optional] RegExp pattern for replacing. Supports only $n
nameReplaceRegexp: 'color_$1'
# [optional]
icons:
# [optional] Name of the Figma's frame where icons components are located
figmaFrameName: Icons
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Table of Contents:
- [Configuration](#configuration)
- [Exporting Typography](#exporting-typography)
- [Design requirements](#design-requirements)
- [Colors](#for-colors)
- [Icons](#for-icons)
- [Images](#for-images)
- [Typography](#for-typography)
- [Example project](#example-project)
- [Contributing](#contributing)
- [License](#license)
Expand Down Expand Up @@ -473,6 +477,8 @@ If an icon supports RTL, it should contains "rtl" word in the description field

**Styles and Components must be published to a Team Library.**

### For colors

For `figma-export colors`

By default, if you support dark mode your Figma project must contains two files. One should contains a dark color palette, and the another light color palette. If you would like to specify light and dark colors in the same file, you can do so with the `useSingleFile` configuration option. You can then denote dark mode colors by adding a suffix like `_dark`. The suffix is also configurable. See [CONFIG.md](CONFIG.md) for more information in the colors section.
Expand All @@ -490,11 +496,46 @@ Example
| <img src="images/dark.png" width="352" /> | <img src="images/dark_c.png" width="200" /> |
| <img src="images/light.png" width="352" /> | <img src="images/light_c.png" width="200" /> |

### For variables

For `figma-export colors`

**Important, the [API](https://www.figma.com/developers/api#variables) for working with color variables in Figma is still in `Beta` stage, so something may break at any time.**

**Important, in [CONFIG.md](CONFIG.md) use either colors or variablesColors.**

With the introduction of color variables in Figma, you can use color variables instead of color styles. To do this in Figma, you need to create color variables and use them in color styles. Color variables can be used in figma-export, for this in [CONFIG.md](CONFIG.md) you need to use the `variablesColors` variable instead of `colors`.

The value of variables can be either the final color value or another variable. For example, the `Primary` variable can contain the value `#FFFFFF`, and the `Secondary` variable can contain the value `Pand/90`.

A collection of variables can contain color variables located in one file, as well as color variables located in different files through links. This is done to limit visibility and show only color tokens when designers are working together.

Example:

<img src="images/figma_colors_tokens.png" width="1024" />

1. tokensLightCollectionName - the name of the color variable collection for the light theme
2. tokensDarkCollectionName - the name of the color variable collection for the dark theme
3. tokensLightHCCollectionName - the name of the color variable collection for the light theme with high contrast
4. tokensDarkHCCollectionName - the name of the color variable collection for the dark theme with high contrast
5. A variable that refers to another variable in a different file
6. A variable that has a local value

<img src="images/figma_colors_primitives.png" width="352" />

1. primitivesCollectionName - the name of the variable collection, if the value in [CONFIG.md](CONFIG.md) is not specified, the default value will be used

See [CONFIG.md](CONFIG.md) for more information in the variablesColors section.

### For icons

For `figma-export icons`

By default, your Figma file should contains a frame with `Icons` name which contains components for each icon. You may change a frame name in a [CONFIG.md](CONFIG.md) file by setting `common.icons.figmaFrameName` property.
If you support dark mode and want separate icons for dark mode, Figma project must contains two files. One should contains a dark icons, and another light icons. If you would like to have light and dark icons in the same file, you can do so with the `useSingleFile` configuration option. You can then denote dark mode icons by adding a suffix like `_dark`. The suffix is also configurable. See [CONFIG.md](CONFIG.md) for more information in the icons section.

### For images

For `figma-export images`

Your Figma file should contains a frame with `Illustrations` name which contains components for each illustration. You may change a frame name in a [CONFIG.md](CONFIG.md) file by setting `common.images.figmaFrameName` property.
Expand All @@ -505,6 +546,8 @@ If you want to specify image variants for different devices (iPhone, iPad, Mac e

<img src="images/ios_image_idiom_figma.png"/>

### For typography

For `figma-export typography`.

Your Figma file must contains Text Styles.
Expand Down
8 changes: 4 additions & 4 deletions Sources/FigmaAPI/Model/Variables.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ public struct Mode: Decodable {
}

public struct VariableCollectionId: Decodable {
public var defaultModeId: String
public var id: String
public var name: String
public var key: String
public var remote: Bool
public var defaultModeId: String
public var modes: [Mode]
public var id: String
public var variableIds: [String]
public var key: String
public var hiddenFromPublishing: Bool
public var variableIds: [String]
}

public enum ResolvedType: String, Decodable {
Expand Down
17 changes: 9 additions & 8 deletions Sources/FigmaExport/Input/Params.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ struct Params: Decodable {
}

struct VariablesColors: Decodable {
let colorPrimitivesFileId: String?
let colorPrimitivesColumnName: String?

let colorTokensFileId: String
let colorTokensLightColumnName: String
let colorTokensDarkColumnName: String?
let colorTokensLightHCColumnName: String?
let colorTokensDarkHCColumnName: String?
let tokensFileId: String
let tokensCollectionName: String

let lightModeName: String
let darkModeName: String?
let lightHCModeName: String?
let darkHCModeName: String?

let primitivesModeName: String?

let nameValidateRegexp: String?
let nameReplaceRegexp: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import FigmaAPI
import FigmaExportCore

/// Loads colors from Figma
final class ColorsLoader {
final class ColorsLoader: ColorsLoaderProtocol {

private let client: Client
private let figmaParams: Params.Figma
private let colorParams: Params.Common.Colors?
Expand All @@ -14,28 +14,22 @@ final class ColorsLoader {
self.colorParams = colorParams
}

func load(filter: String?) throws -> (light: [Color], dark: [Color]?, lightHC: [Color]?, darkHC: [Color]?) {
func load(filter: String?) throws -> ColorsLoaderOutput {
guard let useSingleFile = colorParams?.useSingleFile, useSingleFile else {
return try loadColorsFromLightAndDarkFile(filter: filter)
}
return try loadColorsFromSingleFile(filter: filter)
}

private func loadColorsFromLightAndDarkFile(filter: String?) throws -> (light: [Color],
dark: [Color]?,
lightHC: [Color]?,
darkHC: [Color]?) {
private func loadColorsFromLightAndDarkFile(filter: String?) throws -> ColorsLoaderOutput {
let lightColors = try loadColors(fileId: figmaParams.lightFileId, filter: filter)
let darkColors = try figmaParams.darkFileId.map { try loadColors(fileId: $0, filter: filter) }
let lightHighContrastColors = try figmaParams.lightHighContrastFileId.map { try loadColors(fileId: $0, filter: filter) }
let darkHighContrastColors = try figmaParams.darkHighContrastFileId.map { try loadColors(fileId: $0, filter: filter) }
return (lightColors, darkColors, lightHighContrastColors, darkHighContrastColors)
}

private func loadColorsFromSingleFile(filter: String?) throws -> (light: [Color],
dark: [Color]?,
lightHC: [Color]?,
darkHC: [Color]?) {
private func loadColorsFromSingleFile(filter: String?) throws -> ColorsLoaderOutput {
let colors = try loadColors(fileId: figmaParams.lightFileId, filter: filter)

let darkSuffix = colorParams?.darkModeSuffix ?? "_dark"
Expand Down
8 changes: 8 additions & 0 deletions Sources/FigmaExport/Loaders/Colors/ColorsLoaderProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import FigmaAPI
import FigmaExportCore

typealias ColorsLoaderOutput = (light: [Color], dark: [Color]?, lightHC: [Color]?, darkHC: [Color]?)

protocol ColorsLoaderProtocol {
func load(filter: String?) throws -> ColorsLoaderOutput
}
143 changes: 143 additions & 0 deletions Sources/FigmaExport/Loaders/Colors/ColorsVariablesLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import FigmaAPI
import FigmaExportCore

/// Loads variables colors from Figma
final class ColorsVariablesLoader: ColorsLoaderProtocol {
private let client: Client
private let variableParams: Params.Common.VariablesColors?

init(client: Client, figmaParams: Params.Figma, variableParams: Params.Common.VariablesColors?) {
self.client = client
self.variableParams = variableParams
}

func load(filter: String?) throws -> ColorsLoaderOutput {
guard
let tokensFileId = variableParams?.tokensFileId,
let tokensCollectionName = variableParams?.tokensCollectionName
else { throw FigmaExportError.custom(errorString: "tokensFileId or tokensLightCollectionName is nil") }

return try loadProcess(colorTokensFileId: tokensFileId, tokensCollectionName: tokensCollectionName)
}

private func loadProcess(colorTokensFileId: String, tokensCollectionName: String) throws -> ColorsLoaderOutput {
// Load variables
let meta = try loadVariables(fileId: colorTokensFileId)

guard let tokenCollection = meta.variableCollections.filter({ $0.value.name == tokensCollectionName }).first
else { throw FigmaExportError.custom(errorString: "tokensCollectionName is nil") }

let tokensId = tokenCollection.value.variableIds
let modeIds = extractModeIds(from: tokenCollection.value)
let primitivesModeName = variableParams?.primitivesModeName

let variables: [Variable] = tokensId.compactMap { tokenId in
guard let variableMeta = meta.variables[tokenId]
else { return nil }

let values = Values(
light: variableMeta.valuesByMode[modeIds.lightModeId],
dark: variableMeta.valuesByMode[modeIds.darkModeId],
lightHC: variableMeta.valuesByMode[modeIds.lightHCModeId],
darkHC: variableMeta.valuesByMode[modeIds.darkHCModeId]
)

return Variable(
name: variableMeta.name,
description: variableMeta.description,
valuesByMode: values
)
}

var colors = Colors()
func handleColorMode(variable: Variable, mode: ValuesByMode?, colorsArray: inout [Color]) {
if case let .variableAlias(variableAlias) = mode {
guard
let variableMeta = meta.variables[variableAlias.id],
let variableCollectionId = meta.variableCollections[variableMeta.variableCollectionId]
else { return }
let modeId = variableCollectionId.modes
.filter { $0.name == primitivesModeName }
.first?.modeId ?? variableCollectionId.defaultModeId
if case let .color(color) = variableMeta.valuesByMode[modeId] {
colorsArray.append(createColor(from: variable, color: color))
} else {
handleColorMode(variable: variable, mode: mode, colorsArray: &colorsArray)
}
} else if case let .color(color) = mode {
colorsArray.append(createColor(from: variable, color: color))
}
}
variables.forEach { value in
handleColorMode(variable: value, mode: value.valuesByMode.light, colorsArray: &colors.lightColors)
handleColorMode(variable: value, mode: value.valuesByMode.dark, colorsArray: &colors.darkColors)
handleColorMode(variable: value, mode: value.valuesByMode.lightHC, colorsArray: &colors.lightHCColors)
handleColorMode(variable: value, mode: value.valuesByMode.darkHC, colorsArray: &colors.darkHCColors)
}
return (colors.lightColors, colors.darkColors, colors.lightHCColors, colors.darkHCColors)
}

private func loadVariables(fileId: String) throws -> VariablesEndpoint.Content {
let endpoint = VariablesEndpoint(fileId: fileId)
return try client.request(endpoint)
}

private func extractModeIds(from collections: Dictionary<String, VariableCollectionId>.Values.Element) -> ModeIds {
var modeIds = ModeIds()
collections.modes.forEach {
switch $0.name {
case variableParams?.lightModeName:
modeIds.lightModeId = $0.modeId
case variableParams?.darkModeName:
modeIds.darkModeId = $0.modeId
case variableParams?.lightHCModeName:
modeIds.lightHCModeId = $0.modeId
case variableParams?.darkHCModeName:
modeIds.darkHCModeId = $0.modeId
default:
modeIds.lightModeId = $0.modeId
}
}
return modeIds
}

private func createColor(from variable: Variable, color: PaintColor) -> Color {
return Color(
name: variable.name,
platform: Platform(rawValue: variable.description),
red: color.r,
green: color.g,
blue: color.b,
alpha: color.a
)
}
}

private extension ColorsVariablesLoader {
struct ModeIds {
var lightModeId = String()
var darkModeId = String()
var lightHCModeId = String()
var darkHCModeId = String()
}

struct Colors {
var lightColors: [Color] = []
var darkColors: [Color] = []
var lightHCColors: [Color] = []
var darkHCColors: [Color] = []
}

struct Values {
let light: ValuesByMode?
let dark: ValuesByMode?
let lightHC: ValuesByMode?
let darkHC: ValuesByMode?
}

struct Variable {
let name: String
let description: String
let valuesByMode: Values
}
}
Loading

0 comments on commit 45960ad

Please sign in to comment.