Skip to content

Mixed generate discrepancies: Figma mixed is emitted inconsistently across properties and runtimes #280

Description

@nathanacurtis

The reader has three different behaviours for a text property whose value varies across character ranges, and they disagree with each other. The intended contract — if characters produce mixed styling, derive the value from the first character and record that — is implemented in exactly one place, and even there the derived value is discarded.

Not currently biting: the design systems modelled so far do not mix values within a text node. Filing because the inconsistency is real and would surface the moment one does.

The three emitters

1. FontStyle.value() — REST path. The rule is implemented, then discarded.

Component/Typography/FontStyle.ts

// First character determines the value
const value = resolve(overrides[0]);

// Check for mixed across characters
for (const k of overrides) {
  if (resolve(k) !== value) return "Figma mixed";
}
return value;

It computes the first-character value correctly and then returns the marker anyway whenever any character differs. Affects fontFamily and fontStyle.

2. LetterSpacingStyle.value() — REST path. The rule is present but unreachable.

Component/Typography/LetterSpacingStyle.ts

The mixed check returns early near the top:

if (overrideValue !== undefined && overrideValue !== baseValue) {
  return "Figma mixed";
}

Further down, the normalization block does exactly what the contract describes — reads characterStyleOverrides[0], looks it up in styleOverrideTable, uses that value — but it can never run in the mixed case it was written for, because the check above already returned.

This is the path that produced the current fixture result:

testTypography3  variants[0].elements.text.styles.typography.letterSpacing
  baseline (CLI/REST):  "Figma mixed"
  round-trip:            0

3. MixedStyle.value() — Plugin API path. No derivation exists.

Component/Styles/Primitives/MixedStyle.ts

if (value === (figma.mixed as unknown as typeof value)) {
  return "Figma mixed";
}

No first-character logic, and it is generic over any style key, not only typography — Style.ts:237 routes every mixableString property through it, and FontStyle delegates to it whenever the runtime is the Plugin API. So the blast radius is wider than text properties.

The capability gap

getStyledTextSegments, getRangeFontSize, getRangeLetterSpacing — none appear anywhere in the reader.

The REST runtime can derive a first-character value, because characterStyleOverrides[0] is right there in the payload. The Plugin runtime has no equivalent in the current code; deriving one means new reader capability, not deleting a branch.

That is a CLI-vs-plugin parity concern in its own right: for any property where REST could derive a first-character value and the Plugin API cannot, the two runtimes would produce different specs for the same component. Today they agree only because both emit the marker.

What needs deciding

  • Is "first character wins" the contract for all mixable properties, or only fonts? MixedStyle is generic, so a blanket rule reaches well beyond typography.
  • What should the Plugin API path do? Matching REST requires reading getStyledTextSegments / getRangeXxx. Leaving it as-is guarantees CLI/plugin divergence once REST starts deriving.
  • Should "Figma mixed" remain in the schema at all? It is a declared member of the reader's Typography types (string | number | FigmaVariableReference | "Figma mixed" | null). If the contract is first-character-wins, the marker should stop being produced rather than being handled downstream.

Writer side

figma-from-specs has no handling of the marker at all. The numeric keys (fontSize, letterSpacing, paragraphIndent, paragraphSpacing, listSpacing) happen to be safe — they guard on typeof value === "number", so the marker is skipped and the node keeps Figma's default, which is where the round-tripped 0 comes from.

The string-typed keys are not safe:

case "textCase":       if (typeof value === "string") node.textCase = value as ...
case "textDecoration": if (typeof value === "string") node.textDecoration = value as ...
case "leadingTrim":    if (typeof value === "string") node.leadingTrim = value as ...

"Figma mixed" is a string, so it would be assigned directly into an enum-typed Figma property. No fixture exercises this today — testTypography3 only mixes letterSpacing. A fixture mixing textDecoration would hit it.

If the reader stops emitting the marker, this becomes a cheap safety net for specs already carrying it rather than a live defect.

Current status

The testTypography3 letterSpacing result is being ignored deliberately — see #277, closed in favour of this issue. It is a known discrepancy, not a render regression, and should not be counted as fidelity loss in round-trip reporting.

Done when

  • One documented rule for mixed values, applied consistently across FontStyle, LetterSpacingStyle, and MixedStyle.
  • REST and Plugin runtimes produce the same spec for the same mixed-value component.
  • The writer cannot assign a sentinel into an enum-typed Figma property.

Metadata

Metadata

Assignees

Labels

generatorspecs-from-figma processing engine

Type

No type

Projects

Status
Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions