Carpenter Mordant

Typed design tokens and YAML code generation for Flutter.

Mordant turns declarative token files into strongly typed Dart APIs.

Installation

Add Mordant:

dependencies:
  carpenter_mordant: ^0.1.0

dev_dependencies:
  build_runner: any

Basic usage

Create a Mordant file under lib:

# lib/tokens/tokens.mordant.yaml

palette:
  brand:
    500: "#987654"
    600: "#825E4A"

space:
  sm: 0.5rem
  md: 1rem

button:
  primary:
    background: "${palette.brand[500]}"
    padding: "${space.md}"

Generate Dart code:

dart run build_runner build --delete-conflicting-outputs

Mordant generates:

lib/tokens/tokens.mordant.g.dart

The generated API can be used directly:

palette.brand[500
]
palette.brand[600
]

space.md

button.primary.background
button.primary.padding

Unit tokens integrate with carpenter_units and resolve through an explicit runtime context:


final padding = button.primary.padding.resolve(
  const LengthUnitContext(rootFontSize: 16),
);

Angle tokens are generated as typed Degrees, Radians, or Turns values:

motion:
  loadingAngle: 45deg
final radians = motion.loadingAngle.toRadians().value;

Includes

Large token sets can be split across multiple files.

lib/tokens/
├── tokens.mordant.yaml
├── palette.mordant.part.yaml
├── space.mordant.part.yaml
└── button.mordant.part.yaml

Root file:

$include:
  - palette.mordant.part.yaml
  - space.mordant.part.yaml
  - button.mordant.part.yaml

Includes are resolved relative to the file containing $include.

They may also be nested:

$include:
  - components/button.mordant.part.yaml

Included files use the .mordant.part.yaml suffix so they do not generate standalone Dart files.

Duplicate token definitions are rejected.

Circular includes are rejected.

Generated palettes

Mordant can generate a color scale from a seed:

palette:
  brand:
    $generate: palette
    seed:
      colorSpace: oklch
      components: [0.62, 0.18, 288]
      alpha: 1
    pivot: 500
    gamut: srgb

This produces:

palette.brand[0]
palette.brand[50]
palette.brand[100]
palette.brand[200]
palette.brand[300]
palette.brand[400]
palette.brand[500]
palette.brand[600]
palette.brand[700]
palette.brand[800]
palette.brand[900]
palette.brand[950]
palette.brand[1000]

The default scale contains stops 0, 50 through 950, and 1000. Mordant uses the OKLCH lightness and chroma curves documented by Carpenter and maps out-of-gamut colors by reducing chroma while preserving lightness and hue.

Hex seeds and the former start name remain supported for compatibility:

palette:
  brand:
    $generate: palette
    seed: "#987654"
    start: 500

Custom steps are supported:

palette:
  brand:
    $generate: palette
    seed: "#987654"
    start: 400

    steps:
      - 50
      - 100
      - 200
      - 300
      - 400
      - 500
      - 600
      - 700
      - 800
      - 900
      - 950

Palette lightness targets and chroma envelope can also be configured:

palette:
  brand:
    $generate: palette
    seed:
      colorSpace: oklch
      components: [0.62, 0.18, 288]
      alpha: 1
    pivot: 500
    gamma: 1.15
    lightness:
      50: 0.975
      950: 0.175

Token values

Mordant currently recognizes:

color: "#987654"

px: 12px
em: 1.25em
rem: 1rem
duration: 200ms

number: 42
decimal: 1.25

enabled: true

label: Hello

reference: "${space.md}"

Hex colors must be quoted because an unquoted # starts a YAML comment.

References

References use ${...} syntax:

space:
  md: 1rem

button:
  padding: "${space.md}"

Indexed groups may be referenced with bracket syntax:

palette:
  brand:
    500: "#987654"

button:
  background: "${palette.brand[500]}"

References are validated during generation.

Missing and circular references cause the build to fail.

Generated code

Generated files are automatically formatted.

Mordant generates typed Dart structures instead of runtime string lookups.

For example:

button:
  primary:
    filled:
      background: "${palette.brand[500]}"

becomes an API shaped like:

button.primary.filled.background

No runtime token-path parsing is involved.

License

Apache License 2.0.