@toghen/figma 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/cli/bin.mjs +19 -0
- package/cli/bridge.ts +243 -0
- package/cli/check.ts +62 -0
- package/cli/figcraft.ts +764 -0
- package/cli/figcraftApply.ts +1067 -0
- package/cli/figcraftFile.ts +476 -0
- package/cli/figcraftLibrary.ts +147 -0
- package/cli/figcraftRecord.ts +121 -0
- package/cli/figcraftScene.ts +661 -0
- package/cli/main.ts +218 -0
- package/cli/members.ts +65 -0
- package/cli/run.ts +756 -0
- package/component-files.md +665 -0
- package/dist/code.js +24161 -0
- package/dist/manifest.json +23 -0
- package/dist/ui.html +6918 -0
- package/guide.md +985 -0
- package/main/code.ts +50 -0
- package/main/components/apply.ts +704 -0
- package/main/components/endOfRun.ts +168 -0
- package/main/components/errors.ts +17 -0
- package/main/components/fieldWriter.ts +497 -0
- package/main/components/file.ts +263 -0
- package/main/components/receipt.ts +228 -0
- package/main/components/resolve.ts +1193 -0
- package/main/components/run.ts +273 -0
- package/main/components/snapshot.ts +341 -0
- package/main/components/stamp.ts +312 -0
- package/main/components/update.ts +465 -0
- package/main/dispatcher.ts +33 -0
- package/main/engines/batchRunner.ts +200 -0
- package/main/engines/ioAdapter.ts +132 -0
- package/main/engines/operationExecutor.ts +512 -0
- package/main/engines/propertyMapper.ts +221 -0
- package/main/engines/queryEngine.ts +45 -0
- package/main/engines/styleSyncEngine.ts +444 -0
- package/main/engines/variableSyncEngine.ts +561 -0
- package/main/handlers/auditSelection.ts +60 -0
- package/main/handlers/boundLayers.ts +51 -0
- package/main/handlers/cancelOperation.ts +18 -0
- package/main/handlers/executeApplyBindings.ts +69 -0
- package/main/handlers/executeScaffold.ts +59 -0
- package/main/handlers/executeStyleSync.ts +121 -0
- package/main/handlers/executeVariableSync.ts +82 -0
- package/main/handlers/exportDocument.ts +114 -0
- package/main/handlers/getSelection.ts +43 -0
- package/main/handlers/index.ts +30 -0
- package/main/handlers/inspectCorrespondence.ts +172 -0
- package/main/handlers/loadConventions.ts +86 -0
- package/main/handlers/loadDocument.ts +253 -0
- package/main/handlers/lock.ts +270 -0
- package/main/handlers/planApply.ts +157 -0
- package/main/handlers/planApplyBindings.ts +79 -0
- package/main/handlers/planScaffold.ts +84 -0
- package/main/handlers/planStyleSync.ts +49 -0
- package/main/handlers/planVariableSync.ts +49 -0
- package/main/handlers/readSelectionBindings.ts +160 -0
- package/main/handlers/resizeUi.ts +105 -0
- package/main/handlers/runComponent.ts +65 -0
- package/main/handlers/templates.ts +462 -0
- package/main/handlers/tokenGraph.ts +122 -0
- package/main/runtimeState.ts +66 -0
- package/main/utils/abortController.ts +11 -0
- package/main/utils/boundVariables.ts +84 -0
- package/main/utils/buildPayloads.ts +85 -0
- package/main/utils/correspondence.ts +658 -0
- package/main/utils/designSource.ts +122 -0
- package/main/utils/figmaCollectionIndex.ts +120 -0
- package/main/utils/figmaStyleAdapter.ts +268 -0
- package/main/utils/figmaTokenAdapter.ts +255 -0
- package/main/utils/flattenDesignSystem.ts +38 -0
- package/main/utils/folds.ts +203 -0
- package/main/utils/fontStack.ts +57 -0
- package/main/utils/lists.ts +4 -0
- package/main/utils/lockStore.ts +72 -0
- package/main/utils/moves.ts +67 -0
- package/main/utils/pageRoots.ts +58 -0
- package/main/utils/planSymbols.ts +96 -0
- package/main/utils/purpose.ts +351 -0
- package/main/utils/recentStore.ts +166 -0
- package/main/utils/resolverModes.ts +271 -0
- package/main/utils/rootRecord.ts +137 -0
- package/main/utils/shardedData.ts +142 -0
- package/main/utils/standing.ts +106 -0
- package/main/utils/styleTarget.ts +51 -0
- package/main/utils/sum.ts +612 -0
- package/main/utils/symbolicIdRegistry.ts +26 -0
- package/main/utils/syncState.ts +133 -0
- package/main/utils/templateStore.ts +73 -0
- package/main/utils/tokenPath.ts +28 -0
- package/main/utils/tokenStamps.ts +91 -0
- package/manifest.json +15 -0
- package/package.json +64 -4
- package/reference.md +1609 -0
- package/shared/components/artboard.ts +126 -0
- package/shared/components/axes.ts +420 -0
- package/shared/components/bindings.ts +64 -0
- package/shared/components/blueprint.ts +248 -0
- package/shared/components/check.ts +705 -0
- package/shared/components/diff.ts +668 -0
- package/shared/components/fields.ts +404 -0
- package/shared/components/groups.ts +306 -0
- package/shared/components/index.ts +140 -0
- package/shared/components/members.ts +280 -0
- package/shared/components/model.ts +273 -0
- package/shared/components/order.ts +53 -0
- package/shared/components/read.ts +480 -0
- package/shared/components/stack.ts +124 -0
- package/shared/components/steps.ts +186 -0
- package/shared/components/validate.ts +565 -0
- package/shared/conventions.ts +28 -0
- package/shared/inputs.ts +40 -0
- package/shared/operations.ts +233 -0
- package/shared/pendingNames.ts +39 -0
- package/shared/protocol.ts +588 -0
- package/shared/tokenTypeMap.ts +21 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
# Component files — the format
|
|
2
|
+
|
|
3
|
+
> What a `.component.kdl` file may say, node by node, for whoever writes one.
|
|
4
|
+
> How to write one, with a worked example, is in [guide.md](./guide.md#write-a-component-file); why the format is shaped this way is in [specs.md](./specs.md#components-from-files); the reader's API, the engine and the command line are in [reference.md](./reference.md#component-files).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What a component file is
|
|
9
|
+
|
|
10
|
+
A design system describes each Figma component in a text file, `<component>.component.kdl`, kept in its own repository beside the code the component mirrors. The file says what the component is: its properties, its layers, and what makes one variant differ from another. Toghen reads it, expands it into one member per combination of variant values, checks every member, and makes Figma agree with it in place: every node it creates carries a stamp in plugin data naming the part of the file it came from, so running the file again finds that work and changes only the difference, never rebuilding the set.
|
|
11
|
+
|
|
12
|
+
The file is a [kdlet](https://codeberg.org/egildo/kdlet) file: [KDL](https://kdl.dev/) with a small template language on top. kdlet owns the language, `guard`, `when`, `else`, `define` and `+call`; its [manual](https://codeberg.org/egildo/kdlet/src/branch/main/docs/manual.md) is the reference for those. Toghen owns five top-level words and the vocabulary of layers, fields and values, which is Figma's, spelled as the Plugin API spells it. This page is the reference for Toghen's part, and names kdlet's part where a component file meets it.
|
|
13
|
+
|
|
14
|
+
Three rules hold the whole format together:
|
|
15
|
+
|
|
16
|
+
- **A variant axis is an input.** The file is evaluated once per combination of axis values, and each evaluation is one member's tree. Nothing else is an input.
|
|
17
|
+
- **A `when` stands inside the layer it changes.** There is no rule that reaches a layer from elsewhere, and no way to move one. A region that sits in a different parent per variant is a `define` called under opposite guards.
|
|
18
|
+
- **A value is a token or a literal that says so.** Every field Figma can bind to a variable takes a token reference, `"{space.md}"`, or a literal marked `(literal)`. A bare number is refused, so that every number in a file is either followed from the tokens or explained in a comment beside it.
|
|
19
|
+
|
|
20
|
+
## The skeleton
|
|
21
|
+
|
|
22
|
+
```kdl
|
|
23
|
+
component Toggle
|
|
24
|
+
description "A switch with an optional label."
|
|
25
|
+
|
|
26
|
+
properties {
|
|
27
|
+
VARIANT State off on
|
|
28
|
+
BOOLEAN "Show Label" default=#true
|
|
29
|
+
TEXT Label default="Label"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
guard on { is (input)State on }
|
|
33
|
+
|
|
34
|
+
FRAME Toggle layoutMode=HORIZONTAL layoutSizingHorizontal=HUG layoutSizingVertical=HUG counterAxisAlignItems=CENTER itemSpacing="{space.sm}" {
|
|
35
|
+
// The track and thumb sizes are the design's, not a token's.
|
|
36
|
+
FRAME Track layoutMode=HORIZONTAL primaryAxisAlignItems=MIN width=(literal)40 height=(literal)24 cornerRadius="{radius.full}" {
|
|
37
|
+
fills "{surface.muted}"
|
|
38
|
+
when on primaryAxisAlignItems=MAX { fills "{surface.accent}" }
|
|
39
|
+
FRAME Thumb layoutMode=NONE width=(literal)20 height=(literal)20 cornerRadius="{radius.full}" {
|
|
40
|
+
fills "{surface.raised}"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
TEXT Caption visible=(property)"Show Label" characters=(property)Label textStyle="{text.label}"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The top-level nodes may come in any order. A comment is KDL's, `// …`, and is the right place for the reason a literal is a literal.
|
|
48
|
+
|
|
49
|
+
## Top-level nodes
|
|
50
|
+
|
|
51
|
+
| Node | Count | What it is |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `component <Name> [tokens="{group}"] [layout=parts] [across="<axis>, …"]` | exactly one | The component's name. It has no body and takes one name. `tokens` names the component's token group, which binds the root layer and brings in an axis for each part axis the group's subgroups use (see [From the tokens](#from-the-tokens)). `layout=parts` keeps those axes in the path, one variable per part (see [How parts become variables](#how-parts-become-variables)). `across` names the `VARIANT` axes laid out across the set's board, separated by commas; the others go down, and each direction nests in the order the axes are declared. Without it, axes go across from the last until the columns are at least the rows |
|
|
54
|
+
| `description "<text>"` | at most one | The component's description, as Figma shows it |
|
|
55
|
+
| `properties { … }` | at most one | The component properties, one child node each |
|
|
56
|
+
| `exclude <Axis>=<value>…` | any number | A combination that is never built |
|
|
57
|
+
| `include <Axis>=<value>…` | any number | When any is present, only the combinations an `include` matches are built |
|
|
58
|
+
| `guard <name> { … }` | any number | A named condition on the variant axes (kdlet's) |
|
|
59
|
+
| `define <name> <parameter>… { … }` | any number | A layer subtree written once, called with `+<name>` (kdlet's) |
|
|
60
|
+
| `FRAME <address> …` | exactly one | The root layer. It becomes the component itself in each member |
|
|
61
|
+
|
|
62
|
+
`component`, `description`, `properties`, `exclude` and `include` are Toghen's: kdlet hands them back unread. `guard`, `define` and `+call` are kdlet's. Everything else is a layer, and Figma's.
|
|
63
|
+
|
|
64
|
+
Refused: a second `component` or `description`; a `component` with a body, with no name, or with more than one argument; a `tokens` that is not a token reference in quotes (`tokens="{button}"`); a `layout` other than `parts`, as in `component: layout takes "parts" (one variable per part), not "folded"; leave it out for the folded layout`; `layout=parts` without `tokens`, as in `component: layout=parts is the layout of the parts of a token group, so it needs tokens="{group}"`; an `across` that names an axis twice (`component: across="a, a" names "a" twice`), has an empty name, or names something that is not a `VARIANT` axis once the axes are known (`component: across names "x", which is not a VARIANT axis`); a `description` that is not text; a top-level node that is none of the above.
|
|
65
|
+
|
|
66
|
+
## Properties
|
|
67
|
+
|
|
68
|
+
Each child of `properties` is named for the Figma property type it declares. All five types share one namespace: two properties may not have one name, whatever their types, because Figma renames one of two that collide, silently. A property name may not contain `#`, which Figma uses to tell properties apart.
|
|
69
|
+
|
|
70
|
+
| Node | Arguments and properties |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `VARIANT <Axis> <value>…` | A variant axis and its values. The first value is the default. Values are strings: quote a value that is not a bare word, and quote one that reads as another KDL value (`"true"`, since `#true` is a boolean) |
|
|
73
|
+
| `VARIANT <Axis> (modifier)"<name>"` | For a modifier of the design system, such as `theme`. The values are its contexts, the default first, and each member pins `modes <name>=<value>` on its root. A part axis never uses it. `(modifier)` is accepted only here, alone, in quotes: not beside a list of values, and not twice |
|
|
74
|
+
| `BOOLEAN <Name> default=#true` | `default` is `#true` or `#false`, or a token reference to a `boolean` token, whose variable is bound to it; required |
|
|
75
|
+
| `TEXT <Name> default="…"` | `default` is text, or a token reference to a `string` token, whose variable is bound to it; required |
|
|
76
|
+
| `INSTANCE_SWAP <Name> default=<Component>` | `default` is a component's name, or `(key)"<published key>"`; never a token reference. May have `preferred` children |
|
|
77
|
+
| `SLOT <Name> …` | `description` (text), `minChildren` and `maxChildren` (whole numbers of at least 0, and the minimum not above the maximum), `allowPreferredValuesOnly`, `stretchChildOnInsert`, `displayEmptyByDefault` (`#true` or `#false`), all optional. May have `preferred` children |
|
|
78
|
+
|
|
79
|
+
`preferred COMPONENT` and `preferred COMPONENT_SET` are the only children an `INSTANCE_SWAP` or a `SLOT` takes. Each names its component by exactly one of `key="<published key>"` or `name="<component name>"`; both, or neither, is refused with its line.
|
|
80
|
+
|
|
81
|
+
- **A name** is found as an instance swap's default is: the overrides map, then a declared library, then the file. A `COMPONENT` named after a set offers the set's default variant; a `COMPONENT_SET` must name a set. A name found nowhere, or found twice, stops the run before any write, with its line.
|
|
82
|
+
- **Where each is written.** FigCraft has no published keys, so it takes preferred values by name only, written as the components' ids; one by key is refused there. Figma takes them by key only: one by name is refused by name, before any write, until the desktop app shows that Figma takes a resolved component's key (decisions.md, *Measurements only the desktop app can give*). A file meant for both targets leaves preferred values out.
|
|
83
|
+
|
|
84
|
+
**The rules for an axis.** An axis name may not contain `,` or `=`, which the members' names use, nor `.`, which a guard cannot read: an axis is an input, and kdlet's reference `(input)<Axis>` names a field with no dot. An axis needs at least one value, may not repeat one, and a value may not be empty or contain `,` or `=`. An axis with a space in its name is written in quotes everywhere: `VARIANT "Header in" page content`, `(input)"Header in"`, `exclude "Header in"=content`.
|
|
85
|
+
|
|
86
|
+
**An axis from a token group.** `VARIANT <Axis> from="{group}"` takes its values from the names of the group's children, in the document's order, the first being the default, as `(modifier)` takes a modifier's contexts. A new child in the tokens is a new variant on the next run, with no edit to the file; `exclude` prunes a child that is not a value. `from` stands alone: not beside a list of values or `(modifier)`, and not with an `(input)` segment inside it. Refused, with the line: a group that names nothing or is a token, a document that cannot say, a child name that cannot be a value (a `,` or `=`, empty). Children are ordered as the document's paths first meet them, which is JS object-key order, so purely numeric child names come first, numerically.
|
|
87
|
+
|
|
88
|
+
**Only the axes are inputs.** `BOOLEAN`, `TEXT`, `INSTANCE_SWAP` and `SLOT` properties are wired in Figma by `(property)` and no guard reads them.
|
|
89
|
+
|
|
90
|
+
```kdl
|
|
91
|
+
properties {
|
|
92
|
+
VARIANT Variant solid outline
|
|
93
|
+
VARIANT Theme (modifier)"theme"
|
|
94
|
+
TEXT Label default="Label"
|
|
95
|
+
BOOLEAN "Show Icon" default=#true
|
|
96
|
+
INSTANCE_SWAP Icon default=Glyph {
|
|
97
|
+
preferred COMPONENT_SET key="7f3a…"
|
|
98
|
+
}
|
|
99
|
+
SLOT Content description="What the card holds." minChildren=1 allowPreferredValuesOnly=#false {
|
|
100
|
+
preferred COMPONENT key="b91c…"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## A segment of a reference from an axis
|
|
106
|
+
|
|
107
|
+
A reference in a layer field, or in a `tokens=` binding value, may take part of its path from an axis:
|
|
108
|
+
|
|
109
|
+
```kdl
|
|
110
|
+
properties { VARIANT hue from="{hue}" }
|
|
111
|
+
FRAME Avatar {
|
|
112
|
+
when soft { fills "{hue.(input)hue.soft.background}" }
|
|
113
|
+
when solid { fills "{hue.(input)hue.solid.background}" }
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Inside the braces, `(input)<Axis>` is replaced, in each member, by the axis's value; `(input)"Header in"` names an axis with a space. The segment ends at the next `.` or the closing brace, and is a whole segment (`blue-(input)hue` is refused). Each member then holds one ordinary reference, which every check on a reference applies to. A value with no token is an error naming the member, the axis value and the line, and so is an axis the file does not have. A value that holds `.`, `{` or `}` cannot be a segment, since it would change the path's shape. `(input)` outside braces is plain text. The `component` line's own `tokens=` is read once for the file and takes no segment.
|
|
118
|
+
|
|
119
|
+
## Layers
|
|
120
|
+
|
|
121
|
+
A layer is a node named `FRAME`, `SLOT`, `TEXT`, `INSTANCE` or `LINE`. Its first argument is its **address**: its layer name, which the stamp records and later runs find it by, so addresses are unique within a member. A `SLOT` layer's address is its property's name, and every `SLOT` property has exactly one `SLOT` layer in every member. `TEXT`, `INSTANCE` and `LINE` layers have no child layers. A `LINE`'s length is its `width`; it has no height, so `height`, `minHeight` and `maxHeight` are refused on it. The root layer is a `FRAME`, and it is the member's component itself: its fields are set on the component, it takes the member's name, and no property is wired to it.
|
|
122
|
+
|
|
123
|
+
A **scalar field** is a KDL property on the layer, named as the Plugin API names it: `layoutMode=HORIZONTAL`, `cornerRadius="{radius.md}"`. A **structured field** is a child node of the layer, because its value is a list or a structure:
|
|
124
|
+
|
|
125
|
+
| Node | Value |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `fills <value>…`, `strokes <value>…` | Paints, as arguments. `fills` with no arguments is no paint |
|
|
128
|
+
| `gridRowSizes { … }`, `gridColumnSizes { … }` | Tracks, one child node each: `FLEX [n]`, `FIXED n`, `HUG`. `FLEX` takes a number above 0 and defaults to 1; `FIXED` a number of at least 0; `HUG` none. At least one track |
|
|
129
|
+
| `grid row=<n> column=<n> [rowSpan=<n>] [columnSpan=<n>]` | The cell a child of a `GRID` layer sits in. `row` and `column` are required, whole numbers of at least 0; a span is a whole number of at least 1 |
|
|
130
|
+
| `dashPattern <value>…` | Dash and gap lengths, as arguments: one token reference to a `strokeStyle` token (its `dashArray` is written, in pixels, at the defaults), or `(literal)` numbers of at least 0. `dashPattern` with no arguments is a solid stroke |
|
|
131
|
+
| `constraints horizontal=<c> vertical=<c>` | How the layer follows its parent's resizing, both axes required, each `MIN` `CENTER` `MAX` `STRETCH` `SCALE` |
|
|
132
|
+
| `modes <Collection>=<mode>…` | Pins the layer to a mode of a variable collection, at least one pair: a local collection, or one a library the run declares with `--use` holds (found through the lock, as the collection its variables are named under). A library's collection is pinned in FigCraft, which checks the mode's name; Figma refuses it until pinning one is measured there. A name found both here and in a library, or in two libraries, is an error. With variable sync's collection per modifier, a pin names a modifier and a context: `modes density=compact`. A member pins its folded axes on its root itself, so the reader refuses a `modes` line on the root that names one of them |
|
|
133
|
+
| `effects <value>…` | Effects, as arguments: shadow tokens, dimension tokens (a layer blur's radius), `(literal)` radii. See [Gradients, effects and layout grids](#gradients-effects-and-layout-grids) |
|
|
134
|
+
| `layoutGrids { … }` | Layout grids, one child node each: `COLUMNS`, `ROWS` or `GRID`, its fields as properties. At least one grid. See [Gradients, effects and layout grids](#gradients-effects-and-layout-grids) |
|
|
135
|
+
|
|
136
|
+
The reader refuses a structured field written as a KDL property, and a scalar field written as a child node.
|
|
137
|
+
|
|
138
|
+
### Fields by layer kind
|
|
139
|
+
|
|
140
|
+
| Layers | Field | Value |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| all | `visible` | boolean, a token reference to a `boolean` token (bound), or `(property)` to a `BOOLEAN` property |
|
|
143
|
+
| all | `opacity` | an opacity token, or `(literal)` from 0 to 1. The token must be an opacity (named `opacity`, declared `["OPACITY"]`, or referred to only by opacities); any other token is refused. Its variable holds a percentage, the literal does not: see the guide, *An opacity is a percentage in Figma* |
|
|
144
|
+
| all | `rotation` | an angle: a number in degrees, counter-clockwise as Figma turns, bare or `(literal)`. No token holds an angle, so a bare number is accepted here |
|
|
145
|
+
| all | `modes` | structured |
|
|
146
|
+
| all | `grid` | structured |
|
|
147
|
+
| all | `layoutSizingHorizontal`, `layoutSizingVertical` | `FIXED` `HUG` `FILL` |
|
|
148
|
+
| all | `width`, `height` | size: a dimension |
|
|
149
|
+
| all | `minWidth`, `maxWidth`, `minHeight`, `maxHeight` | size: a dimension, or `#null` |
|
|
150
|
+
| `FRAME` `SLOT` `TEXT` | `tokens` | a token reference naming a group (see [Bindings from a group](#bindings-from-a-group)) |
|
|
151
|
+
| `FRAME` `SLOT` | `layoutMode` | `NONE` `HORIZONTAL` `VERTICAL` `GRID`; a `SLOT` is never `GRID` |
|
|
152
|
+
| `FRAME` `SLOT` | `primaryAxisAlignItems` | `MIN` `MAX` `CENTER` `SPACE_BETWEEN` `SPACE_EVENLY` `SPACE_AROUND` |
|
|
153
|
+
| `FRAME` `SLOT` | `counterAxisAlignItems` | `MIN` `MAX` `CENTER` `BASELINE` |
|
|
154
|
+
| `FRAME` `SLOT` | `itemSpacing`, `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`, `gridRowGap`, `gridColumnGap`, `strokeWeight`, `strokeTopWeight`, `strokeRightWeight`, `strokeBottomWeight`, `strokeLeftWeight`, `cornerRadius`, `topLeftRadius`, `topRightRadius`, `bottomRightRadius`, `bottomLeftRadius` | dimension. `cornerRadius` is set first, and a corner field overrides its corner |
|
|
155
|
+
| `FRAME` `SLOT` | `effectStyle` | a token reference to an effect style, made by style sync from a `shadow` token; a token that is only a reference to such a shadow binds its target's style, and a text style named through a typography reference does the same |
|
|
156
|
+
| `FRAME` `SLOT` | `gridRowSizes`, `gridColumnSizes` | structured |
|
|
157
|
+
| `FRAME` `SLOT` | `clipsContent` | boolean |
|
|
158
|
+
| `FRAME` `SLOT` `TEXT` | `fills` | structured |
|
|
159
|
+
| `FRAME` `SLOT` `LINE` | `strokes` | structured |
|
|
160
|
+
| `LINE` | `strokeWeight` | dimension |
|
|
161
|
+
| `TEXT` | `characters` | text, a token reference to a `string` token (bound), or `(property)` to a `TEXT` property |
|
|
162
|
+
| `TEXT` | `textStyle` | a token reference to a text style |
|
|
163
|
+
| `TEXT` | `textAutoResize` | `NONE` `HEIGHT` `WIDTH_AND_HEIGHT` |
|
|
164
|
+
| `TEXT` | `fontFamily` | a token reference only, to a `fontFamily` or `string` token: its variable is bound. A font stack's variable holds the first family, and the report says so |
|
|
165
|
+
| `TEXT` | `fontStyle` | a token reference only, to a `string` token holding a style name as Figma spells it (`Semi Bold`) |
|
|
166
|
+
| `TEXT` | `fontWeight` | a token reference only, to a `fontWeight` or `number` token: Figma cannot assign a weight, only bind one, and picks the style |
|
|
167
|
+
| `TEXT` | `fontSize`, `paragraphSpacing`, `paragraphIndent` | dimension |
|
|
168
|
+
| `TEXT` | `lineHeight` | dimension, or a `number` token: in pixels it is bound; a unitless number (a multiplier) is written as its value, as a percentage. A `(literal)` is pixels |
|
|
169
|
+
| `TEXT` | `letterSpacing` | dimension, or a `letterSpacing` token: in pixels it is bound; in `em` it is written as its value, as a percentage. A `(literal)` is pixels |
|
|
170
|
+
| `TEXT` | `listSpacing` | dimension, written as its value: Figma cannot bind it |
|
|
171
|
+
| `TEXT` | `maxLines` | a `number` token or a `(literal)` whole number of at least 1, written as its value; it holds only with `textTruncation=ENDING` |
|
|
172
|
+
| `TEXT` | `textAlignHorizontal` | `LEFT` `CENTER` `RIGHT` `JUSTIFIED` |
|
|
173
|
+
| `TEXT` | `textAlignVertical` | `TOP` `CENTER` `BOTTOM` |
|
|
174
|
+
| `TEXT` | `textCase` | `ORIGINAL` `UPPER` `LOWER` `TITLE` `SMALL_CAPS` `SMALL_CAPS_FORCED` |
|
|
175
|
+
| `TEXT` | `textDecoration` | `NONE` `UNDERLINE` `STRIKETHROUGH` |
|
|
176
|
+
| `TEXT` | `textTruncation` | `DISABLED` `ENDING` |
|
|
177
|
+
| `TEXT` | `leadingTrim` | `CAP_HEIGHT` `NONE` |
|
|
178
|
+
| `INSTANCE` | `component` | a component's name, `(key)"…"`, or `(property)` to an `INSTANCE_SWAP` property; required |
|
|
179
|
+
| `INSTANCE` | `set` | structured: the instance's properties by display name, see [below](#an-instances-properties) |
|
|
180
|
+
| `INSTANCE` | `isExposedInstance` | `#true` or `#false`: its properties listed on the component's instances, see [below](#an-instances-properties) |
|
|
181
|
+
| all | `effects` | structured; not beside `effectStyle` |
|
|
182
|
+
| `FRAME` `SLOT` `INSTANCE` | `layoutGrids` | structured |
|
|
183
|
+
| `FRAME` `SLOT` | `layoutWrap` | `NO_WRAP` `WRAP`. Only on a `HORIZONTAL` or `VERTICAL` layer; a `VERTICAL` one wraps into columns |
|
|
184
|
+
| `FRAME` `SLOT` | `counterAxisAlignContent` | `AUTO` `SPACE_BETWEEN`. Only on a `HORIZONTAL` or `VERTICAL` layer |
|
|
185
|
+
| `FRAME` `SLOT` | `counterAxisSpacing` | dimension, bound: the gap between the rows (or columns) of a wrapping flow. Only on a `HORIZONTAL` or `VERTICAL` layer |
|
|
186
|
+
| `FRAME` `SLOT` | `cornerSmoothing` | a number token or `(literal)` from 0 to 1, written as a value: Figma cannot bind it |
|
|
187
|
+
| all | `gridChildHorizontalAlign`, `gridChildVerticalAlign` | `MIN` `CENTER` `MAX` `AUTO`: how a child of a `GRID` layer sits in its cell. Only on a child of a `GRID` |
|
|
188
|
+
| all | `layoutPositioning` | `AUTO` `ABSOLUTE`. Only on a child of a layer with a layout |
|
|
189
|
+
| all | `x`, `y` | a dimension, written as a value: Figma cannot bind it. Not on the root, which Toghen places; on a child of a layer with a layout only with `layoutPositioning=ABSOLUTE` |
|
|
190
|
+
| all | `constraints` | structured |
|
|
191
|
+
| all | `blendMode` | Figma's `BlendMode`: `PASS_THROUGH` `NORMAL` `DARKEN` `MULTIPLY` `LINEAR_BURN` `COLOR_BURN` `LIGHTEN` `SCREEN` `LINEAR_DODGE` `COLOR_DODGE` `OVERLAY` `SOFT_LIGHT` `HARD_LIGHT` `DIFFERENCE` `EXCLUSION` `HUE` `SATURATION` `COLOR` `LUMINOSITY` |
|
|
192
|
+
| `FRAME` `SLOT` `LINE` | `strokeAlign` | `INSIDE` `OUTSIDE` `CENTER` |
|
|
193
|
+
| `FRAME` `SLOT` `LINE` | `strokeJoin` | `MITER` `BEVEL` `ROUND` |
|
|
194
|
+
| `FRAME` `SLOT` `LINE` | `dashPattern` | structured |
|
|
195
|
+
| `LINE` | `strokeCap` | `NONE` `ROUND` `SQUARE` `ARROW_LINES` `ARROW_EQUILATERAL` `DIAMOND_FILLED` `TRIANGLE_FILLED` `CIRCLE_FILLED` |
|
|
196
|
+
|
|
197
|
+
The format narrows Figma in a few places, on purpose. The wrap fields and `cornerSmoothing` are on `FRAME` and `SLOT`, as the other auto-layout and corner fields are. The stroke fields are on the kinds that take `strokes`. `strokeCap` is on a `LINE` only: a frame's outline has no open end to cap. `x` and `y` are refused where Figma would ignore them, so a run never writes a position the next run reads back as moved.
|
|
198
|
+
|
|
199
|
+
An unknown field, a field on a layer kind that does not take it, and a value of the wrong kind are each refused, naming the layer.
|
|
200
|
+
|
|
201
|
+
**A text's fields, or its style.** A text style sets the font, size, line height, letter spacing, paragraph spacing and indent, list spacing, case, decoration and leading trim. One of those beside `textStyle` would be a second source for one value, so it is refused, naming both: write the style, or the fields. Alignment, truncation and `maxLines` are the text's own, and go beside a style. A text field is bound on the whole text: a text whose ranges differ is not a component file's to describe.
|
|
202
|
+
|
|
203
|
+
### What an omitted field means
|
|
204
|
+
|
|
205
|
+
The engine sets only the fields the file declares, with these exceptions, so that a new node does not carry what Figma starts it with:
|
|
206
|
+
|
|
207
|
+
- **Fills.** A frame, slot or root the file gives no `fills` has its fills cleared, since a new frame starts white.
|
|
208
|
+
- **Strokes.** A layer that gives only a side's weight (`strokeBottomWeight`) gets `strokeWeight` 0 first, so the other sides are none. A layer that gives a side's weight is given `strokeAlign` `INSIDE` too, unless it declares its own `strokeAlign`, which then wins.
|
|
209
|
+
- **A slot clips** its contents unless the layer says `clipsContent=#false`.
|
|
210
|
+
- **A wired field starts at its property's default**: a `visible=(property)"Show Nav"` layer starts visible or not as the boolean's `default` says, a wired `characters` as the text's.
|
|
211
|
+
- **A text's sizing** is set after its style, characters and text fields, so an empty text does not freeze at zero width. The font's halves come first among the text fields, since every later write needs the font they make loaded; `maxLines` comes after `textTruncation`, since Figma drops it silently while truncation is off.
|
|
212
|
+
|
|
213
|
+
A field the file stops declaring keeps the value it has in Figma; the run reports it once.
|
|
214
|
+
|
|
215
|
+
### Gradients, effects and layout grids
|
|
216
|
+
|
|
217
|
+
These take tokens whose parts Figma binds one by one. A part that follows a token with a variable is bound to it; any other part is written as its value at the design system's defaults, and the run's report says so once (see *A token where Figma cannot bind it*, under [Values](#values)). Each reads its tokens from the loaded resolver document, so a file that uses them needs one loaded.
|
|
218
|
+
|
|
219
|
+
**A gradient on a paint.** `fills` and `strokes` take a `gradient` token wherever they take a colour: `fills "{gradient.dawn}"`. The design system says which a token is. Each stop whose colour is a reference to a colour token with a variable is bound to it; a stop whose colour is written out is written. The stops' positions are written. A W3C gradient is a list of stops with no angle, and runs left to right (Figma's `gradientTransform` `[[1, 0, 0], [0, 1, 0]]`); a gradient in the object form, `{ type, angle, stops }`, has its angle turned about the layer's centre, as style sync turns one. There is no field for a fill style.
|
|
220
|
+
|
|
221
|
+
**Effects.** `effects` lists the layer's effects in order, each argument one of:
|
|
222
|
+
|
|
223
|
+
- a `shadow` token: a drop shadow, or an inner shadow when `inset` is true; a token that holds a list of shadows gives one effect each. Its `color`, `offsetX`, `offsetY`, `blur` and `spread` each bind (the colour `EFFECT_COLOR`, the rest `EFFECT_FLOAT`) when it refers to a token with a variable; a member written out is written. Binding the colour gives the shadow its variable's alpha.
|
|
224
|
+
- a `dimension` token: a layer blur, its radius bound to the token's variable.
|
|
225
|
+
- `(literal)` a number: a layer blur of that radius in pixels.
|
|
226
|
+
|
|
227
|
+
```kdl
|
|
228
|
+
FRAME Card {
|
|
229
|
+
effects "{shadow.raised}" "{blur.soft}"
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
A bare number is refused, as on a dimension. `effects` and `effectStyle` on one layer are refused together, naming both: the style sets the effects the field sets. Figma draws a shadow's spread only on a frame with a visible fill that clips its content (measured §7). A background blur, noise, texture and glass are not written.
|
|
234
|
+
|
|
235
|
+
**Layout grids.** `layoutGrids` holds one child node per grid, named by its pattern, its fields as properties:
|
|
236
|
+
|
|
237
|
+
```kdl
|
|
238
|
+
FRAME Page {
|
|
239
|
+
layoutGrids {
|
|
240
|
+
COLUMNS alignment=STRETCH count="{grid.columns}" gutterSize="{space.md}" offset="{space.lg}"
|
|
241
|
+
ROWS alignment=CENTER count=4 gutterSize=(literal)8 sectionSize="{space.xl}"
|
|
242
|
+
GRID sectionSize="{space.sm}" color=(literal)"#ff00001a"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| Pattern | `alignment` | Fields it takes |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `COLUMNS`, `ROWS` | `STRETCH` | `count`, `gutterSize`, `offset` |
|
|
250
|
+
| `COLUMNS`, `ROWS` | `CENTER` | `count`, `gutterSize`, `sectionSize` |
|
|
251
|
+
| `COLUMNS`, `ROWS` | `MIN`, `MAX` | `count`, `gutterSize`, `offset`, `sectionSize` |
|
|
252
|
+
| `GRID` | none | `sectionSize` |
|
|
253
|
+
|
|
254
|
+
Each field it takes is required but `offset`, which is 0 when left out. `count` is a `number` token or a whole number of at least 1; `gutterSize`, `offset` and `sectionSize` are dimensions, a token or a `(literal)` number. A token is bound. A field the grid's alignment does not take is refused with its line, since Figma refuses to bind it there (`STRETCH` refuses `sectionSize`, `CENTER` `offset`, a square grid all but `sectionSize`; measured §14). `color` is a colour token, written as its value since Figma cannot bind it, or a `(literal)` colour; a grid that names none is red at 10%.
|
|
255
|
+
|
|
256
|
+
## Values
|
|
257
|
+
|
|
258
|
+
| Form | Meaning |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `"{space.md}"` | A token reference: a string that is exactly one reference, in the core's [reference grammar](https://codeberg.org/Toghen/lib/src/branch/main/reference.md#reference-grammar). Resolved by name in Figma to the variable or style that variable sync and style sync wrote for the token |
|
|
261
|
+
| `(literal)8`, `(literal)"#ffffff80"` | A literal on purpose: a number for a dimension, `#rrggbb` or `#rrggbbaa` for a colour. Why it is a literal belongs in a comment beside it. On `characters` or `visible` in a `when`, it marks a value that replaces the property's wiring on purpose (see *Conditions*) |
|
|
262
|
+
| `(property)"Show Nav"` | Wires the field to a component property of the matching type |
|
|
263
|
+
| `(key)"…"` | A published component key. Accepted only where a component is named |
|
|
264
|
+
| `(input)<Axis>` | kdlet's reference: the axis's value as text, which a `define`'s parameter or a layer's name may take |
|
|
265
|
+
| `#null` | Clears a size limit: accepted on `minWidth`, `maxWidth`, `minHeight` and `maxHeight` only. On `width` or `height` it is refused, `"width=#null": a width cannot be cleared; to let the layer size itself, write layoutSizingHorizontal=HUG (or FILL)` |
|
|
266
|
+
| `"{{size.sm}}"` | A doubled brace is a literal brace, as in a token's value ([reference grammar](https://codeberg.org/Toghen/lib/src/branch/main/reference.md#reference-grammar)): the text `{size.sm}`, never the token. Written the same in KDL and in the JSON of a data file |
|
|
267
|
+
| a bare word, number, `#true`, `#false` | Plain: an enum, a boolean, text |
|
|
268
|
+
|
|
269
|
+
**A token where Figma cannot bind it.** A token on a field, or on part of a field, that Figma cannot bind is written as its value at the design system's defaults (the default context of every modifier), and the run's report says so once: the layer will not follow the token until the next run. The value comes from the loaded resolver document, so such a token needs one: with none loaded, the run stops at its line, and a token of a type the field does not take stops it too. Such fields are `x` and `y` (a dimension), `cornerSmoothing` (a number) and `dashPattern` (a `strokeStyle`, as its dash array). Such a part is a colour's alpha in `fills` or `strokes`: the paint's colour is bound to the token's variable, and its opacity is written as the alpha. A colour with no alpha, or an alpha of 1, is bound and needs nothing more. On a text, `listSpacing` and `maxLines` are always written as their value, and a line height or letter spacing is whenever it is not in pixels: Figma binds those two in pixels only. Which a line height or letter spacing is, only the token's unit says, so a token on either needs the resolver document loaded, in pixels too.
|
|
270
|
+
|
|
271
|
+
`literal`, `property`, `key` and `modifier` are the tags the vocabulary gives kdlet. A **dimension** is a token or a `(literal)` number; the reader refuses a bare number, with a message that says what to write instead. A **paint** is a token or a `(literal)` colour; it refuses a bare colour string the same way. Any other tag, or a tag where it is not accepted, is an error.
|
|
272
|
+
|
|
273
|
+
## Conditions, as a component file uses them
|
|
274
|
+
|
|
275
|
+
The language is kdlet's, and the [manual](https://codeberg.org/egildo/kdlet/src/branch/main/docs/manual.md) is the reference for it. A component file uses it this way.
|
|
276
|
+
|
|
277
|
+
**A guard names a condition once.** `is (input)state hover pressed` is true when the axis has either value; `not <guard>`, `all { … }` and `any { … }` build larger conditions. A guard reads only variant axes and lists only values the axis has; a guard with no body, which in kdlet marks one the host supplies, is refused since a component file has no host. A guard is visible in the body it is written in and inward, so one written at the top of the file is visible everywhere except inside a `define`, which is closed.
|
|
278
|
+
|
|
279
|
+
```kdl
|
|
280
|
+
guard on { is (input)State on }
|
|
281
|
+
guard hovered { is (input)state hover pressed }
|
|
282
|
+
guard primary { is (input)variant primary }
|
|
283
|
+
guard header-in-page { not header-in-content }
|
|
284
|
+
guard upcoming { all { not current; not done } }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**`when` stands inside the layer it changes** and names one or more guards; all must hold. Its properties are fields of that layer, and its children are fields written as child nodes, or child layers.
|
|
288
|
+
|
|
289
|
+
```kdl
|
|
290
|
+
FRAME Track layoutMode=HORIZONTAL primaryAxisAlignItems=MIN {
|
|
291
|
+
fills "{surface.muted}"
|
|
292
|
+
when on primaryAxisAlignItems=MAX { fills "{surface.accent}" }
|
|
293
|
+
when outline { fills; strokes "{border.strong}"; }
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- The layer's own line is applied first, then the `when`'s properties and children, in file order. **Every field replaces**: a later write replaces an earlier one, a list replaces whole, and `#null` sets a size limit to null, which is not the same as never setting it.
|
|
298
|
+
- A `when` that replaces a wired field, `visible=#false` over `visible=(property)"Show Nav"`, unwires it in that member; the run warns that the property is wired to nothing there. When that is meant, write the value as `(literal)`: `when ready characters=(literal)"Ready"`. The member is then unwired with no warning. A text cannot follow a variant while staying wired: in Figma a text property has one default for the whole set, and every wired variant shows it.
|
|
299
|
+
- `else` after a `when` applies when the `when` did not: `else pressed opacity=(literal)0.8` is tried only if the guards before failed, and a bare `else …` is the last case.
|
|
300
|
+
- The reader refuses a `when` outside a layer: there is no layer for it to change. A `when` may add a child layer: `when on { FRAME Badge … }`.
|
|
301
|
+
|
|
302
|
+
**A region in a different parent per variant is a `define`** called with `+name` under opposite guards:
|
|
303
|
+
|
|
304
|
+
```kdl
|
|
305
|
+
define header-region {
|
|
306
|
+
FRAME "Header region" layoutMode=VERTICAL { … }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
FRAME Shell … {
|
|
310
|
+
when header-in-page { +header-region }
|
|
311
|
+
FRAME "Main lane" … {
|
|
312
|
+
when header-in-content { +header-region }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
A `define` is closed: its body sees its parameters, written `(input)<parameter>`, and the guards written inside it, and nothing else; a parameter with a value is optional, a bare one required. A call gives parameters by name, `+step id=First at=(input)Progress here=first`, and a call's children land where the definition writes `block`. A layer the call produces has the address the definition gave it, so the diff finds the same layer under another parent. A call to a definition the file does not define is refused.
|
|
318
|
+
|
|
319
|
+
**Not available in a component file:** `each`, since nothing gives a component file a list to repeat over. kdlet reads it, and the reader refuses the file with a message that says so.
|
|
320
|
+
|
|
321
|
+
### An instance's properties
|
|
322
|
+
|
|
323
|
+
A composite is built from other components, and an instance of one reads its default until told otherwise. `set <Name>=<value>…` inside an `INSTANCE` gives it its properties, by the display names its component declares, never with Figma's `#id` suffix:
|
|
324
|
+
|
|
325
|
+
```kdl
|
|
326
|
+
component ContactRow
|
|
327
|
+
properties {
|
|
328
|
+
VARIANT density comfortable compact
|
|
329
|
+
}
|
|
330
|
+
guard compact { is (input)density compact }
|
|
331
|
+
FRAME ContactRow layoutMode=HORIZONTAL {
|
|
332
|
+
INSTANCE Mark component=Badge {
|
|
333
|
+
set Variant=tag Label="Organisation"
|
|
334
|
+
when compact { set Size=small }
|
|
335
|
+
}
|
|
336
|
+
INSTANCE Plate component=Person {
|
|
337
|
+
set Density=(input)density
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- A variant property takes one of its values, a boolean `#true` or `#false`, a text a string, a swap a component's name or `(key)"…"`. An axis of the file reaches a value through `(input)`, so each member sets its own; `when` adds to an earlier `set`, and the same name twice takes the later.
|
|
343
|
+
- A boolean or a text property also takes a token reference, `set Title="{copy.title}"`: a `boolean` or `string` token's variable is bound to it, and the instance follows the variable. A variant or a swap takes no token.
|
|
344
|
+
- What is set is set again every run, and a change in Figma since is drift, kept and said, written over with `--overwrite-drift`; a property the file never mentions is left as the component has it. A property the file stops naming keeps its last value, and the run says so.
|
|
345
|
+
- Refused, with the line and the component: a name the component does not have, two of its properties with one display name, a value its variant does not offer, a value of the wrong kind, a slot written as a property, a swap to a component not in the file. A line that every member carries is said once.
|
|
346
|
+
- **Not a wire.** A value is fixed or an axis's; `(property)` is refused: the Plugin API refuses to wire a nested instance's property, or any layer inside one, to the composite's property (measured), and Figma's own UI has no such link: a nested instance's properties are exposed, never passed through.
|
|
347
|
+
- **`isExposedInstance=#true`** is what the API has instead: the instance's own properties are listed on every instance of the component, under its name, and set there. The row's `Plate` exposed shows the person's `Name` on the row's instances. It makes no property of the component's; an artboard's `set` reaches the component's own properties only.
|
|
348
|
+
|
|
349
|
+
## Exclusions and inclusions
|
|
350
|
+
|
|
351
|
+
```kdl
|
|
352
|
+
exclude Variant=outline State=hover
|
|
353
|
+
exclude Variant=outline State=pressed
|
|
354
|
+
include State=rest Size=medium
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`exclude <Axis>=<value>…` names a combination that is never built, one value for each axis it names; an axis it does not name matches any value. Several combinations take several lines; a list of values in one value, such as `"a|b"`, is not read. A value that is not a bare word is quoted. The default member, the first value of every axis, may not be excluded.
|
|
358
|
+
|
|
359
|
+
`include <Axis>=<value>…` turns it around: when any `include` is present, only the combinations an `include` matches are built, and `exclude` then removes from those. The default member must be included.
|
|
360
|
+
|
|
361
|
+
Refused: a key that is not a `VARIANT` axis, a value the axis does not have, a line with no `Axis=value`.
|
|
362
|
+
|
|
363
|
+
On a component whose axes come from its parts, the key is a part under its axis: `exclude state=pressed` builds no member in the pressed state. A part key the axis does not have is refused, as in `exclude variant: "quiet" is not one of its values (rest, primary)`. So is an exclusion of the default member, such as `exclude variant=rest`, with `exclude removes the default member (variant=rest, state=rest, selection=rest); the first value of every axis is the default and is always built`.
|
|
364
|
+
|
|
365
|
+
## From the tokens
|
|
366
|
+
|
|
367
|
+
A component's parts, and its bindings, are already in the token document, and the file can take them from there instead of writing them. Both need a design system: the command line reads it from the resolver document (`components <file> --resolver <resolver.json>`, or `sync`), and a file that takes anything from the tokens stops without one.
|
|
368
|
+
|
|
369
|
+
A part is a subgroup whose key is a value of an axis in the [parts vocabulary](https://codeberg.org/Toghen/lib/src/branch/main/model.md#the-vocabulary) at its parent group. The [model](https://codeberg.org/Toghen/lib/src/branch/main/model.md#components-and-parts) explains parts, and its [glossary](https://codeberg.org/Toghen/lib/src/branch/main/model.md#glossary) defines the terms.
|
|
370
|
+
|
|
371
|
+
A file written before parts moves in [the guide](./guide.md#move-a-component-file-off-state-modifiers).
|
|
372
|
+
|
|
373
|
+
### Axes from the parts
|
|
374
|
+
|
|
375
|
+
`component Button tokens="{button}"` brings in one axis for each axis of the vocabulary that the group's subgroups use. Each takes the vocabulary's name, with `rest` as its first value and then the part keys the subgroups use, in the vocabulary's order. The [guide](./guide.md#take-a-components-axes-from-its-parts) shows the task.
|
|
376
|
+
|
|
377
|
+
- A part nested in another, such as `selected.hover`, is one value on each of two axes.
|
|
378
|
+
- Inferred axes come after the declared ones, in the vocabulary's order.
|
|
379
|
+
- **A declared axis of the same name wins.** A file that declares `VARIANT variant soft solid` while its parts use the `variant` axis keeps the declared axis. A declared value that is a part key reads that part (`variant=solid` reads `avatar.solid.*`), and one that is not reads the rest. A part key the declared axis leaves out is a warning on the component line: `VARIANT "variant" is declared and also comes from the parts of "avatar"; the declared values win, and "outline" is left out.` When the parts would fold that axis into modes, the file is refused, since a mode cannot hold a declared value with no part: `VARIANT "variant" is declared and also comes from the parts of "avatar", which fold it into modes, and a mode cannot hold the declared values that have no part. Drop the declared axis, or lay the component out with layout=parts`.
|
|
380
|
+
- A modifier of the design system, such as `theme` or `density`, is never an axis by itself: the frame an instance sits in sets it, as a stylesheet takes it from an ancestor. A file may still declare one by hand with `(modifier)`, which is then the author's choice.
|
|
381
|
+
- Adding a context to a modifier the file declares adds a member on the next run, with no edit to the file.
|
|
382
|
+
- **Only the contexts Figma gets.** A modifier that lists `figma.include` gives an axis declared with `(modifier)` only those contexts. A file that names a left-out context as an axis value, in an `exclude`, `include` or guard, or in `modes <modifier>=<context>`, is an error with the line, saying the context is not one Figma gets.
|
|
383
|
+
|
|
384
|
+
Refused, with the line: a part axis whose name holds `.`, `,` or `=`, or a part key that is `rest` or holds `,` or `=`:
|
|
385
|
+
|
|
386
|
+
`component tokens="{<group>}" has parts on the axis "<axis>", which cannot be a variant: an axis name cannot contain ",", "=" or ".", and a part key cannot be "rest" or contain "," or "="`
|
|
387
|
+
|
|
388
|
+
A key that is a value of two axes, or two parts of one axis nested in one path, stops the read with the design system's message, which names the key and the group. A group that names nothing stops it too: `component tokens="{nothing}" names no token and no group of the design system`.
|
|
389
|
+
|
|
390
|
+
### How parts become variables
|
|
391
|
+
|
|
392
|
+
`layout=parts` on the component line keeps every axis of the component in the path: no member pins a collection, and each part is a variable of its own. Without it, each axis folds into a collection of modes where the tokens allow, and the dry run says which axes stay and why. The members are the same in both layouts; only the pins and the variables they bind differ. The [guide](./guide.md#choose-how-parts-become-variables) says which to choose.
|
|
393
|
+
|
|
394
|
+
Axes fold in the vocabulary's order, so an earlier axis folds first. Put variant axes before state axes in the vocabulary, and nest a variant's own state under it, as in `button.primary.hover`.
|
|
395
|
+
|
|
396
|
+
**A folded part has no variable of its own.** Folded, `chip.solid.hover.ink` is the `hover` mode of the variable `chip.solid.ink`, so a file that names the part names nothing Figma will hold. `check`, and the plugin's check before a sync, refuse it with the name to use:
|
|
397
|
+
|
|
398
|
+
`token {chip.solid.hover.ink} is the "hover" part of "chip.solid.ink", which is one variable with a mode per "interaction": name {chip.solid.ink}; the member pins the mode (a tokens= axis, or modes interaction=hover)`
|
|
399
|
+
|
|
400
|
+
A part that a modifier varies keeps its own variable, and its name passes. The check reads every file's `layout=parts` before it checks any name, so a file with `layout=parts` keeps its parts whatever order the files come in.
|
|
401
|
+
|
|
402
|
+
The command line checks the files before it plans the variables, so the sync learns the layout from the check. A sync that did not check the file folds the component, and a build of a `layout=parts` file then stops, as a reference to a missing variable does. The fold is tested against the Figma mock, and ran in a real file on 13 October 2026 (guide, *Run in a real file*).
|
|
403
|
+
|
|
404
|
+
### Bindings from a group
|
|
405
|
+
|
|
406
|
+
`tokens="{group}"` is a field on a `FRAME`, `SLOT` or `TEXT`; the `component` line's `tokens=` applies to the root, and a root's own `tokens=` replaces it there. For each token **one level down** in the group, keyed by the CSS property it fills, the field below is written as if the file had said it, so every check on a token reference applies. A field the member's evaluated layer already has, from the layer or a `when`, is kept. Subgroups are not read; a group of subgroups binds nothing. Nothing is matched by name: a layer binds only the group it names, so renaming a layer never changes what it binds.
|
|
407
|
+
|
|
408
|
+
| Key | Field | On |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| `backgroundColor` | `fills` | `FRAME`, `SLOT` |
|
|
411
|
+
| `background` | `fills`, when the token is one `color`; a gradient or an image stays skipped with the usual note. `backgroundColor` wins when both are in the group | `FRAME`, `SLOT` |
|
|
412
|
+
| `color` | `fills` | `TEXT` |
|
|
413
|
+
| `borderColor` | `strokes` | `FRAME`, `SLOT` |
|
|
414
|
+
| `borderWidth` | `strokeWeight` | `FRAME`, `SLOT` |
|
|
415
|
+
| `borderTopWidth` … `borderLeftWidth` | `strokeTopWeight` … `strokeLeftWeight` | `FRAME`, `SLOT` |
|
|
416
|
+
| `border` (composite) | `strokes` from `color`, `strokeWeight` from `width` | `FRAME`, `SLOT` |
|
|
417
|
+
| `borderRadius` | `cornerRadius` | `FRAME`, `SLOT` |
|
|
418
|
+
| `borderTopLeftRadius` … `borderBottomLeftRadius` | `topLeftRadius` … `bottomLeftRadius` | `FRAME`, `SLOT` |
|
|
419
|
+
| `padding` | the four paddings | `FRAME`, `SLOT` |
|
|
420
|
+
| `paddingBlock` | `paddingTop`, `paddingBottom` | `FRAME`, `SLOT` |
|
|
421
|
+
| `paddingInline` | `paddingLeft`, `paddingRight` | `FRAME`, `SLOT` |
|
|
422
|
+
| `paddingTop` … `paddingLeft` | the same | `FRAME`, `SLOT` |
|
|
423
|
+
| `gap` | `itemSpacing` under `HORIZONTAL` or `VERTICAL`; `gridRowGap` and `gridColumnGap` under `GRID` | `FRAME`, `SLOT` |
|
|
424
|
+
| `rowGap`, `columnGap` | `gridRowGap`, `gridColumnGap`, under `GRID` only | `FRAME`, `SLOT` |
|
|
425
|
+
| `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` | the same | all three |
|
|
426
|
+
| `opacity` | `opacity` | all three |
|
|
427
|
+
| `font` (typography) | `textStyle` | `TEXT` |
|
|
428
|
+
|
|
429
|
+
- **More specific wins**, whatever the document's order: `paddingTop` over `paddingBlock` over `padding`; `borderColor` and `borderWidth` over `border`; `borderTopWidth` over `borderWidth`; `rowGap` and `columnGap` over `gap`.
|
|
430
|
+
- **A composite `border`** binds its members written as references to the referenced tokens' variables. A literal member has no variable; the reader warns and does not write it. A `style` other than `solid`, in any context, is an error when the file is read.
|
|
431
|
+
- **Skipped into the run report's notes**, once per key and layer: a key the table lacks for the layer kind (`transition`, `cursor`, `backgroundColor` on a text), a gap key on a layout where it means nothing.
|
|
432
|
+
- **Refused:** `tokens` on an `INSTANCE`; a value that is not a whole reference; a reference that names a token, or nothing; no design system given.
|
|
433
|
+
- **A member follows its parts.** A folded axis pins its collection on the member's root, and the bound variable takes that mode's value. An axis left in the path binds the part group the member selects: `button.selected.backgroundColor` for `selection=selected`, and the rest's token where that group does not give the key. Of two part groups that give one key, the one with more parts wins, so `selected.hover` outranks `selected`. The part groups of a folded axis are never read, since the rest's variable carries them.
|
|
434
|
+
|
|
435
|
+
```kdl
|
|
436
|
+
component Button tokens="{button}"
|
|
437
|
+
|
|
438
|
+
properties {
|
|
439
|
+
TEXT Label default="Save"
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
FRAME Button layoutMode=HORIZONTAL layoutSizingHorizontal=HUG layoutSizingVertical=HUG {
|
|
443
|
+
TEXT Caption tokens="{button.label}" characters=(property)Label
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Expansion and names
|
|
448
|
+
|
|
449
|
+
The members are the cartesian product of the `VARIANT` axes in declared order, the last axis varying fastest, minus every combination an `exclude` matches (and, with any `include`, only those an `include` matches). With no `VARIANT` axis there is one member, named for the component. A member is named `Axis=value, Axis=value` in declared axis order, which is how Figma names the members of a set; the default member, the first value of every axis and `rest` on every part axis, is placed top-left, which is how Figma picks a set's default variant.
|
|
450
|
+
|
|
451
|
+
Each member is kdlet's evaluation of the document with that combination as its inputs, read as a tree of layers. The members' trees may differ in more than fields: a `when` may add a child layer, and a call under a guard may put a layer in another parent. The engine's diff reads a layer by address and takes care of both.
|
|
452
|
+
|
|
453
|
+
A token reference resolves in Figma to the variable `tokenPathToVariableName(path)` names, the path without the collection segment with `/` between groups (`space/md`), looked for across every local collection; a `textStyle` to the text style of the same name. A name that matches nothing stops the run, and so does one found in two collections.
|
|
454
|
+
|
|
455
|
+
## What is checked
|
|
456
|
+
|
|
457
|
+
The checks run in stages that share one report, and a stage that cannot run says nothing, so a file is told about a duplicate address only once the spelling of its fields is fixed. Every problem carries a line and a column; a problem found in a member after a `when` wrote the field points at the `when`. A problem every member has is said once; one some members have is said once per member, with the member's name.
|
|
458
|
+
|
|
459
|
+
1. **Reading.** kdlet reads the file, and Toghen reads its five words. A reserved word with the wrong shape, a guard out of scope, a property written twice on a node, a `when` outside a layer, a `define` that is not at the top: kdlet's. The rules under [Top-level nodes](#top-level-nodes), [Properties](#properties) and [Exclusions](#exclusions-and-inclusions): Toghen's.
|
|
460
|
+
2. **Axes from the tokens**, when the file names a token group: its parts, laid out as the file asks, against the design system.
|
|
461
|
+
3. **The document against the axes.** Every guard reads only variant axes and lists only values they have; every `exclude` and `include` names an axis with one of its values; the default member is built; the binding plan of each group the file names is worked out.
|
|
462
|
+
4. **Each member**, after kdlet evaluates it. A problem kdlet finds (a missing value, an unknown tag, an unknown parameter) stops that member's checks. Then the tree: an unknown field for the layer's kind, a value of the wrong kind, an unknown enum value, a bare number or colour where a `(literal)` is wanted, a structured field written as a property; a duplicate address; `GRID` layout needs `gridRowSizes` and `gridColumnSizes`, forbids `primaryAxisAlignItems`, `counterAxisAlignItems` and `itemSpacing`, and every child has `grid`; a `FLEX` track on an axis the grid hugs; any other layout forbids the four grid fields and a child's `grid`; a `SLOT` is never `GRID`; `layoutWrap`, `counterAxisAlignContent` and `counterAxisSpacing` need a `HORIZONTAL` or `VERTICAL` layer, a cell alignment a `GRID` parent, `layoutPositioning` a parent with a layout, and `x` or `y` a layer that is not the root and is not placed by its parent's layout; an `INSTANCE` names its `component`; the root is one `FRAME`, does not fill a parent it does not have and carries no `grid`; wiring, where `(property)` on `visible` names a `BOOLEAN`, on `characters` a `TEXT`, on `component` an `INSTANCE_SWAP`, never on the root; a `SLOT` layer and a `SLOT` property match one to one; no field a text style also sets stands beside `textStyle`, and a literal `maxLines` is a whole number of at least 1; `#null` on a `width` or `height`; a `width` or `height` bound to a token, written or from `tokens=`, beside `HUG` or `FILL` on its axis, or beside a text's `textAutoResize` that sizes that axis (`HEIGHT` or `WIDTH_AND_HEIGHT` for a height, `WIDTH_AND_HEIGHT` for a width), since a bound size is a fixed size and the target would settle on one of the two: `FRAME "Chip": "height" is bound to {chip.height} while layoutSizingVertical=HUG: a bound height is a fixed size; write layoutSizingVertical=HUG and drop the height token, or keep the token and drop HUG`; and a `HORIZONTAL` or `VERTICAL` frame that hugs an axis every child in its flow fills, since nothing is left to size it and the target sets one of the two back: `FRAME "Face" fills the width of FRAME "Card", which hugs it: a frame cannot hug children that only fill it; give "Card" a fixed width, or a child a width of its own`.
|
|
463
|
+
5. **All members together.** Every `BOOLEAN`, `TEXT` and `INSTANCE_SWAP` property is wired by some layer of some member.
|
|
464
|
+
6. **Names against the design system**, by `toghen-figma check` and by the plugin's check before a sync: every token and style a file names is in the design system, and is not a part the fold took into its rest's variable ([How parts become variables](#how-parts-become-variables)). What only Figma holds, such as a variable that is ambiguous between collections, is the dry run's.
|
|
465
|
+
|
|
466
|
+
A **warning**, found per member: a declared `BOOLEAN`, `TEXT` or `INSTANCE_SWAP` property wired to nothing in that member. It is sometimes what a `when` means, and it is also how a property goes dead by accident. A warning does not stop anything; an error builds nothing.
|
|
467
|
+
|
|
468
|
+
A problem with one paint in `fills` or `strokes` points at the node and not at the paint, since kdlet carries one position for a list of arguments.
|
|
469
|
+
|
|
470
|
+
## Worked examples
|
|
471
|
+
|
|
472
|
+
**The running example's button, with axes from its parts.** Its subgroups use three axes of the vocabulary. `variant` and `state` fold, so each member pins them on its root. `selection` stays in the path, so its members bind the `selected` part group. The file is the running example's `button.component.kdl`, and the axes and members below are what the test mock gives for it.
|
|
473
|
+
|
|
474
|
+
```kdl
|
|
475
|
+
component Button tokens="{button}"
|
|
476
|
+
|
|
477
|
+
properties {
|
|
478
|
+
TEXT Label default="Save"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
FRAME Button layoutMode=HORIZONTAL layoutSizingHorizontal=HUG layoutSizingVertical=HUG counterAxisAlignItems=CENTER {
|
|
482
|
+
TEXT Caption tokens="{button}" characters=(property)Label textAutoResize=WIDTH_AND_HEIGHT
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The axes and the sixteen members:
|
|
487
|
+
|
|
488
|
+
```text
|
|
489
|
+
properties (declared first, then the axes taken from the parts)
|
|
490
|
+
Label TEXT default "Save"
|
|
491
|
+
variant VARIANT rest, primary pins the collection variant
|
|
492
|
+
state VARIANT rest, hover, pressed, disabled pins the collection state
|
|
493
|
+
selection VARIANT rest, selected no pin: laid out in the path
|
|
494
|
+
|
|
495
|
+
16 members
|
|
496
|
+
variant=rest, state=rest, selection=rest | pins variant=rest state=rest | fill button.backgroundColor
|
|
497
|
+
variant=rest, state=rest, selection=selected | pins variant=rest state=rest | fill button.selected.backgroundColor
|
|
498
|
+
variant=rest, state=hover, selection=rest | pins variant=rest state=hover | fill button.backgroundColor
|
|
499
|
+
variant=rest, state=hover, selection=selected | pins variant=rest state=hover | fill button.selected.backgroundColor
|
|
500
|
+
variant=rest, state=pressed, selection=rest | pins variant=rest state=pressed | fill button.backgroundColor
|
|
501
|
+
variant=rest, state=pressed, selection=selected | pins variant=rest state=pressed | fill button.selected.backgroundColor
|
|
502
|
+
variant=rest, state=disabled, selection=rest | pins variant=rest state=disabled | fill button.backgroundColor
|
|
503
|
+
variant=rest, state=disabled, selection=selected | pins variant=rest state=disabled | fill button.selected.backgroundColor
|
|
504
|
+
variant=primary, state=rest, selection=rest | pins variant=primary state=rest | fill button.backgroundColor
|
|
505
|
+
variant=primary, state=rest, selection=selected | pins variant=primary state=rest | fill button.selected.backgroundColor
|
|
506
|
+
variant=primary, state=hover, selection=rest | pins variant=primary state=hover | fill button.backgroundColor
|
|
507
|
+
variant=primary, state=hover, selection=selected | pins variant=primary state=hover | fill button.selected.backgroundColor
|
|
508
|
+
variant=primary, state=pressed, selection=rest | pins variant=primary state=pressed | fill button.backgroundColor
|
|
509
|
+
variant=primary, state=pressed, selection=selected | pins variant=primary state=pressed | fill button.selected.backgroundColor
|
|
510
|
+
variant=primary, state=disabled, selection=rest | pins variant=primary state=disabled | fill button.backgroundColor
|
|
511
|
+
variant=primary, state=disabled, selection=selected | pins variant=primary state=disabled | fill button.selected.backgroundColor
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**A control with three axes, an excluded corner, a swap and a mode pin.** `exclude` leaves the outline variant with no hover or pressed; `Scale` is a collection a `when` pins; the icon is an instance swapped through a property.
|
|
515
|
+
|
|
516
|
+
```kdl
|
|
517
|
+
component Control
|
|
518
|
+
description "A pressable control with a label and an optional icon."
|
|
519
|
+
|
|
520
|
+
properties {
|
|
521
|
+
VARIANT Variant solid outline
|
|
522
|
+
VARIANT State rest hover pressed
|
|
523
|
+
VARIANT Size medium small large
|
|
524
|
+
TEXT Label default="Label"
|
|
525
|
+
BOOLEAN "Show Icon" default=#true
|
|
526
|
+
INSTANCE_SWAP Icon default=Glyph
|
|
527
|
+
}
|
|
528
|
+
exclude Variant=outline State=hover
|
|
529
|
+
exclude Variant=outline State=pressed
|
|
530
|
+
|
|
531
|
+
guard active { is (input)State hover pressed }
|
|
532
|
+
guard small { is (input)Size small }
|
|
533
|
+
guard outline { is (input)Variant outline }
|
|
534
|
+
|
|
535
|
+
FRAME Control layoutMode=HORIZONTAL layoutSizingHorizontal=HUG layoutSizingVertical=HUG counterAxisAlignItems=CENTER paddingLeft="{space.md}" paddingRight="{space.md}" paddingTop="{space.sm}" paddingBottom="{space.sm}" itemSpacing="{space.sm}" cornerRadius="{radius.md}" strokeWeight=(literal)1 minHeight=(literal)44 { // a touch target, no token names it
|
|
536
|
+
fills "{surface.control}"
|
|
537
|
+
when active { fills "{surface.control-active}"; }
|
|
538
|
+
// A translucent white highlight: only this control uses it.
|
|
539
|
+
strokes (literal)"#ffffff80"
|
|
540
|
+
when small paddingLeft="{space.sm}" paddingRight="{space.sm}" { modes Scale=small; }
|
|
541
|
+
when outline { fills; strokes "{border.strong}"; }
|
|
542
|
+
INSTANCE "Leading icon" visible=(property)"Show Icon" component=(property)Icon
|
|
543
|
+
TEXT Caption characters=(property)Label textStyle="{text.label}" textAutoResize=WIDTH_AND_HEIGHT {
|
|
544
|
+
when small textStyle="{text.label-small}"
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Twelve members: three sizes by three states by two variants, minus the two excluded.
|
|
550
|
+
|
|
551
|
+
**A repeated part through a `define` with parameters.** One step, written once, called three times with the axis value handed in; the definition's guards compare its parameters.
|
|
552
|
+
|
|
553
|
+
```kdl
|
|
554
|
+
component Stepper
|
|
555
|
+
|
|
556
|
+
properties {
|
|
557
|
+
VARIANT Progress first second third
|
|
558
|
+
TEXT "First label" default="Details"
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
define step id at here done-at=#null {
|
|
562
|
+
guard current { is (input)at (input)here }
|
|
563
|
+
guard done { is (input)at (input)done-at }
|
|
564
|
+
|
|
565
|
+
FRAME (input)id layoutMode=HORIZONTAL layoutSizingHorizontal=HUG layoutSizingVertical=HUG cornerRadius="{radius.full}" {
|
|
566
|
+
fills "{surface.muted}"
|
|
567
|
+
when current { fills "{surface.accent}" }
|
|
568
|
+
else done { fills "{surface.raised}" }
|
|
569
|
+
block
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
FRAME Stepper layoutMode=HORIZONTAL layoutSizingHorizontal=HUG layoutSizingVertical=HUG itemSpacing="{space.md}" {
|
|
574
|
+
+step id="Step 1" at=(input)Progress here=first done-at=second {
|
|
575
|
+
TEXT "Label 1" characters=(property)"First label" textStyle="{text.label}"
|
|
576
|
+
}
|
|
577
|
+
+step id="Step 2" at=(input)Progress here=second
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**A layout with slots, a grid and a region that changes parent** is the worked example in [specs.md](./specs.md#worked-example-a-layout-with-slots-a-grid-and-a-region-that-changes-parent): four `SLOT` properties with descriptions, a `GRID` frame whose tracks a `when` rewrites, `grid row= column=` on its children, a `#null` clearing a minimum width, and a `define` called under opposite guards in two parents.
|
|
582
|
+
|
|
583
|
+
**Axes and bindings from the tokens** are the button above; its layers bind the group's keys as the table under [Bindings from a group](#bindings-from-a-group) says.
|
|
584
|
+
|
|
585
|
+
## Artboard files
|
|
586
|
+
|
|
587
|
+
A page that documents a system is a frame of instances, not a component. An **artboard file**, `<name>.artboard.kdl`, builds one. It shares this format's layers, fields, values and `when`, `define` and `+call`, and differs in five ways. The reasons are in [specs.md](./specs.md#artboard-files).
|
|
588
|
+
|
|
589
|
+
```kdl
|
|
590
|
+
artboard Reference
|
|
591
|
+
FRAME Page layoutMode=VERTICAL itemSpacing=(literal)16 {
|
|
592
|
+
TEXT Title characters=(input)name
|
|
593
|
+
each p in=(input)properties key=name {
|
|
594
|
+
INSTANCE "Row" component=PropertyRow {
|
|
595
|
+
set Name=(p)name Default=(p)default
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
INSTANCE Stage component=DefinitionFrame {
|
|
599
|
+
slot Content {
|
|
600
|
+
INSTANCE Set component=(key)"…"
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
- **The header is `artboard <Name>`** in place of `component <Name>`, and it takes no `properties`, `exclude`, `include` or `description`; there are no axes and no members. The root `FRAME` is an ordinary frame on the run's page (`--page`, else the current page), found again by its stamp wherever a person moved it, and reconciled in place by address and key. A stamped layer the file stops producing is removed, a breaking step that stops a real run unless `--allow-breaking` is given; a layer with no stamp is never touched and is reported.
|
|
607
|
+
- **`set <Name>=<value>…` on an `INSTANCE`** gives the instance's properties, as in a component file ([above](#an-instances-properties)); here a value may also come from the inputs, `set Name=(p)name`.
|
|
608
|
+
- **An `INSTANCE` takes its size:** `width`, `height`, the four limits, `layoutSizingHorizontal` and `layoutSizingVertical`, as in a component file. Its properties are written first and its sizing after, and again whenever a run switches its variant, since a variant switch resizes an instance: `INSTANCE Stage component=Panel width=(literal)480 layoutSizingVertical=HUG { set Tone=calm }` stays 480 wide whichever tone it shows. Measured in FigCraft; in Figma it is written the same way and not measured.
|
|
609
|
+
- **`slot <Name> { … }` on an `INSTANCE`** fills the instance's slot with layers written inside it. A slot the component does not have is refused. `each` and `when` work inside it.
|
|
610
|
+
- **Inputs.** `--input <document>` is a resolver-like document, read by lib and resolved at the default contexts. A template reads its resolved tokens by path with `(input)`, and a token whose `$value` is a list of records is a list `each` repeats over. Records are flat, because kdlet reads no dotted path: write `(p)width`, not `(p)size.width`. A reference inside a record resolves like any other, so the data can point at the system's tokens. A reference to something the document lacks is an error with the line. A doubled brace in the data, `"{{size.sm}}"`, is the text `{size.sm}`; but a value that arrives as exactly one reference and lands on `characters` is then read as a token reference, a limit still open.
|
|
611
|
+
- **`each` needs a key** (`key=name`), and a repeated layer's address is its name plus the key, so a second run with an inserted row patches that row and leaves the others.
|
|
612
|
+
- **A hand edit.** What the file declares is set again every run, and the run says when it overwrote a different value; what the file never mentions is left alone. A hand edit is reported only when the file did not change that layer in the same run.
|
|
613
|
+
- **Refused in an artboard file:** `tokens=`, `(property)`, `isExposedInstance`, a `SLOT` layer, `description`, and a file named `.component.kdl` that holds an `artboard` (and the reverse). A component file still refuses `each` and `slot`.
|
|
614
|
+
|
|
615
|
+
## Blueprints
|
|
616
|
+
|
|
617
|
+
A page is often several components and the artboard that shows them. A **blueprint**, `<page>.blueprint.kdl`, folds their files into one document and says what the page needs. Component files and artboard files stay readable on their own; a blueprint is a way to keep a page together, not a requirement. The reasons are in [specs.md](./specs.md#blueprints).
|
|
618
|
+
|
|
619
|
+
```kdl
|
|
620
|
+
page Buttons
|
|
621
|
+
|
|
622
|
+
tokens "tokens/ds.resolver.json" overrides="themes/warm.overrides.json"
|
|
623
|
+
library Core
|
|
624
|
+
|
|
625
|
+
component Button tokens="{button}" {
|
|
626
|
+
properties {
|
|
627
|
+
VARIANT Tone calm loud
|
|
628
|
+
}
|
|
629
|
+
FRAME Button layoutMode=HORIZONTAL itemSpacing="{space.sm}" {
|
|
630
|
+
INSTANCE Icon component=Icon
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
artboard "Button reference" {
|
|
635
|
+
FRAME Page layoutMode=VERTICAL {
|
|
636
|
+
INSTANCE Set component=Button
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
| Node | How many | What it says |
|
|
642
|
+
|---|---|---|
|
|
643
|
+
| `page <Name>` | exactly one | The Figma page the blueprint builds into |
|
|
644
|
+
| `tokens <entry>… [name="…"] [overrides="…"]` | at most one | What the core's `load` is given, in its three forms: one resolver document, one token file, or several token files. `name` is the collection's name for token files; `overrides` the file that holds the overrides map. In the plugin it is held against what was loaded; it reads no file |
|
|
645
|
+
| `library <Name>` | any number | A library whose components, styles and variables the page names |
|
|
646
|
+
| `component <Name> … { … }` | any number | A component file: the header's attributes (`tokens=`, `layout=`, `across=`) stay on this line, and the braces hold the rest of the file |
|
|
647
|
+
| `artboard <Name> { … }` | any number | An artboard file, the same way |
|
|
648
|
+
| `guard`, `define` | any number | kdlet's, shared by every body, as its scope gives |
|
|
649
|
+
|
|
650
|
+
**A body is read as its file would be.** The reader hands each body, with the shared `guard` and `define` nodes, to the reader of a component file or of an artboard file, at the lines and columns it has in the blueprint. Every check under [What is checked](#what-is-checked) and [Artboard files](#artboard-files) applies, and every problem points at the blueprint's own line. A shared `guard` or `define` is read with every body, so it is checked by each, and a problem with it is said once. On the plugin's screen nothing supplies an artboard's inputs, so a body that reads `(input)` is refused there, as a missing input.
|
|
651
|
+
|
|
652
|
+
**Refused**, each with its line:
|
|
653
|
+
|
|
654
|
+
- no `page`, or a second one;
|
|
655
|
+
- a second `tokens`; a `tokens` with no location, or with a property other than `name` and `overrides`;
|
|
656
|
+
- a word that is none of the blueprint's, nor `guard` or `define`;
|
|
657
|
+
- a body with its own header line (a `component` or `artboard` node inside the braces);
|
|
658
|
+
- two `component` bodies of one name, or two `artboard` bodies of one name;
|
|
659
|
+
- inside a body, whatever its file refuses: `each` in a component body, as today.
|
|
660
|
+
|
|
661
|
+
**What a blueprint needs** is listed from what it reads: the `tokens` line, the `library` names, and every component its bodies name, once each, with where it is first named: `component=` on an `INSTANCE`, an `INSTANCE_SWAP` property's default, and a `(key)"…"` on either or in a `set`, listed as a key. A name a `component` body of the same blueprint defines is satisfied by it. A body that does not read names nothing. The shape is the [reference's](./reference.md#blueprintneedsblueprint-blueprintneeds).
|
|
662
|
+
|
|
663
|
+
## From the file to Figma
|
|
664
|
+
|
|
665
|
+
The plugin reads and checks the file, compares it with what Figma holds, and changes the difference. Two ways bring it the file: `toghen-figma sync` or `components` from the repository, and the Templates pane, which keeps a file in the Figma file. The drill is in the [guide](./guide.md#from-the-repository-to-figma), and the engine's contract (what it resolves, the steps, the stamps, what breaks instances) is in the [reference](./reference.md#rundesired-options-promisereport).
|