carpenter_mordant 0.4.0 copy "carpenter_mordant: ^0.4.0" to clipboard
carpenter_mordant: ^0.4.0 copied to clipboard

Typed design tokens and YAML code generation for Flutter.

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.