Skip to content

Commit

Permalink
Add support for exporting colors from variables (#236)
Browse files Browse the repository at this point in the history
* Add VariablesEndpoint

Add Variables models

* Add new VariablesColors params

* Add raw ColorsVariablesLoader

Update ExportColors

Update params name

Update params name

Small refactoring

Small refactoring

Add doc

Refactoring

Refactoring

* Update doc

* Filter support

* Simplified handleColorMode method

* Small refactoring

* Update after review
  • Loading branch information
alexey1312 authored Apr 8, 2024
1 parent 90ef6b7 commit d667c99
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 28 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:
# [required] Identifier of the file containing variables
tokensFileId: shPilWnVdJfo10YF12345
# [required] Variables collection name
tokensCollectionName: Base collection
# [required] Name of the column containing light color variables in the tokens table
lightModeName: Light
# [optional] Name of the column containing dark color variables in the tokens table
darkModeName: Dark
# [optional] Name of the column containing light high contrast color variables in the tokens table
lightHCModeName: Contast Light
# [optional] Name of the column containing dark high contrast color variables in the tokens table
darkHCModeName: Contast Dark
# [optional] Name of the column containing color variables in the primitive table. If a value is not specified, the default values ​​will be taken
primitivesModeName: Collection_1
# [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 [figma-export.yaml](CONFIG.md) use either `colors` or `variablesColors`.**

With the introduction of color variables in Figma, you can use it instead of color styles. Color variables can be used in figma-export, for this in [figma-export.yaml](CONFIG.md) you need to use the `variablesColors` option 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`. Figma-export can work with any depth of variable nesting. You can specify the `primitivesModeName` parameter to indicate the mode for the final table with your primitives, if the parameter is not specified, the default value will be used.

Example:

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

1. tokensCollectionName - the name of the variable collection
2. lightModeName - the name of the color variable column for the light theme
3. darkModeName - the name of the color variable column for the dark theme
4. lightHCModeName - the name of the color variable column for the light theme with high contrast
5. darkHCModeName - the name of the color variable column for the dark theme with high contrast
6. A variable that has a local value
7. A variable that refers to another variable in a different file

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

1. primitivesModeName - the name of the variable column, if the value in [figma-export.yaml](CONFIG.md) is not specified, the default value will be used
2. A variable that has a local value

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
28 changes: 28 additions & 0 deletions Sources/FigmaAPI/Endpoint/VariablesEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation
#if os(Linux)
import FoundationNetworking
#endif

public struct VariablesEndpoint: BaseEndpoint {
public typealias Content = VariablesMeta

private let fileId: String

public init(fileId: String) {
self.fileId = fileId
}

func content(from root: VariablesResponse) -> Content {
return root.meta
}

public func makeRequest(baseURL: URL) -> URLRequest {
let url = baseURL
.appendingPathComponent("files")
.appendingPathComponent(fileId)
.appendingPathComponent("variables")
.appendingPathComponent("local")
return URLRequest(url: url)
}

}
69 changes: 69 additions & 0 deletions Sources/FigmaAPI/Model/Variables.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
public struct Mode: Decodable {
public var modeId: String
public var name: String
}

public struct VariableCollectionValue: Decodable {
public var defaultModeId: String
public var id: String
public var name: String
public var modes: [Mode]
public var variableIds: [String]
}

public struct VariableAlias: Codable {
public var id: String
public var type: String
}

public enum ValuesByMode: Decodable {
case variableAlias(VariableAlias)
case color(PaintColor)
case string(String)
case number(Double)
case boolean(Bool)

public enum CodingKeys: CodingKey {
case variableAlias
case color
case string
case number
case boolean
}

public init(from decoder: Decoder) throws {
if let variableAlias = try? VariableAlias(from: decoder) {
self = .variableAlias(variableAlias)
} else if let color = try? PaintColor(from: decoder) {
self = .color(color)
} else if let string = try? String(from: decoder) {
self = .string(string)
} else if let number = try? Double(from: decoder) {
self = .number(number)
} else if let boolean = try? Bool(from: decoder) {
self = .boolean(boolean)
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Data didn't match any expected type."))
}
}
}

public struct VariableValue: Decodable {
public var id: String
public var name: String
public var variableCollectionId: String
public var valuesByMode: [String: ValuesByMode]
public var description: String
}

public typealias VariableId = String
public typealias VariableCollectionId = String

public struct VariablesMeta: Decodable {
public var variableCollections: [VariableCollectionId: VariableCollectionValue]
public var variables: [VariableId: VariableValue]
}

public struct VariablesResponse: Decodable {
public let meta: VariablesMeta
}
17 changes: 17 additions & 0 deletions Sources/FigmaExport/Input/Params.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct Params: Decodable {
}

struct Common: Decodable {

struct Colors: Decodable {
let nameValidateRegexp: String?
let nameReplaceRegexp: String?
Expand All @@ -23,6 +24,21 @@ struct Params: Decodable {
let darkHCModeSuffix: String?
}

struct VariablesColors: Decodable {
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?
}

struct Icons: Decodable {
let nameValidateRegexp: String?
let figmaFrameName: String?
Expand All @@ -45,6 +61,7 @@ struct Params: Decodable {
}

let colors: Colors?
let variablesColors: VariablesColors?
let icons: Icons?
let images: Images?
let typography: Typography?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
import FigmaAPI
import FigmaExportCore

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

/// Loads colors from Figma
final class ColorsLoader {

private let client: Client
private let figmaParams: Params.Figma
private let colorParams: Params.Common.Colors?
private let filter: String?

init(client: Client, figmaParams: Params.Figma, colorParams: Params.Common.Colors?) {
init(
client: Client,
figmaParams: Params.Figma,
colorParams: Params.Common.Colors?,
filter: String?
) {
self.client = client
self.figmaParams = figmaParams
self.colorParams = colorParams
self.filter = filter
}

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

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

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

let darkSuffix = colorParams?.darkModeSuffix ?? "_dark"
let lightHCSuffix = colorParams?.lightHCModeSuffix ?? "_lightHC"
Expand Down Expand Up @@ -65,7 +68,7 @@ final class ColorsLoader {
return filteredColors
}

private func loadColors(fileId: String, filter: String?) throws -> [Color] {
private func loadColors(fileId: String) throws -> [Color] {
var styles = try loadStyles(fileId: fileId)

if let filter {
Expand Down
Loading

0 comments on commit d667c99

Please sign in to comment.