Skip to content

Latest commit

 

History

History
1246 lines (981 loc) · 34.7 KB

HANDBOOK.org

File metadata and controls

1246 lines (981 loc) · 34.7 KB

QuarkSuite Core Handbook (v2.1.0)

Table of Contents

Summary

This document is a practical guide to QuarkSuite Core. It’s intended to introduce you, the reader, to the library and its purpose from a user perspective. For a technical overview, refer to the API.

This document intentionally skips setup and environment details (see the README) to focus instead on a working example.

We’ll be covering three workflows in order of complexity:

  • Basic
  • Advanced
  • Modular

The basic workflow will show you simple token generation and collection assembly. From there, you’ll get into the advanced usage of defining rules and processes to scale collections. The modular workflow will show you how to distribute your rules and token structures to share with other projects.

By the end of this document, I hope you learn a little something about how you can use QuarkSuite to create consistent, accessible baselines for your web projects.

If any part of the handbook is hard to understand, please open an issue and let me know.

Basic Workflow

QuarkSuite allows a lot of flexibility in how you can work, but for our purposes, we’ll begin by creating a single file named tokens.js and build our baseline up from there.

Modules

For our project, we’ll need specific functionality from the color.js and content.js modules.

import {
  adjust,
  convert,
  harmony,
  palette,
  a11y
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/color.js";
import {
  grid,
  scale,
  text,
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/content.js";

Color

The first set of data we’ll create is our color palette.

1. Set a base color

Let’s begin by setting a base color and slightly adjusting it for our use.

const swatch = convert("rgb", "#7ea");
const base = adjust({ chroma: -5, hue: 60 }, swatch);

2. Generate a base scheme

Next, we want to generate an analogous color harmony as the basis of our palette generation.

const scheme = harmony({ configuration: "analogous" }, base);

3. Generate palettes

The next step is to generate palettes for our desired contexts. In this case, we’ll create a main and accent palette from the first and second colors in our scheme.

Assuming our web project is an app, we’ll want to generate a material configuration.

We’ll also limit our palette to the most accessible colors for our contexts. In our example, that’s AA large adherence for the ui and a AA adherence for the body text.

const main = palette({
  configuration: "material",
  contrast: 95,
  accents: true,
  states: true
}, scheme[0]);

const accent = palette({
  configuration: "material",
  contrast: 95,
  accents: true,
}, scheme[1]);

const ui = a11y({ mode: "standard", rating: "AA", large: true }, main);
const body = a11y({ mode: "standard", rating: "AA" }, accent);

Content

The main concern for our content at this point is to set a global ratio and values that the content modular scales can use to generate their data later.

You see that we want our ratio to be 1.32 and for our scales to output 4 values.

const ratio = 1.32;
const values = 4;

Tokens

Now we’re ready to actually create our tokens. For that, we’ll create a token object to export. Let’s wrap the tokens in a namespace of hb (for handbook) before slotting and transforming our data sets.

export default {
  hb: {
    color: {
      ui,
      text: body,
      splash: scheme[2],
    },
    text: {
      body: text({ system: "sans", weights: ["regular", "bold"] }, "Work Sans"),
      heading: text(
        { system: "serif", weights: ["light", "black"] },
        "Work Sans",
      ),
      size: scale(
        {
          configuration: "bidirectional",
          inversion: "em",
          ratio,
          values,
        },
        "1rem",
      ),
      leading: scale(
        { configuration: "ranged", floor: 1.2, ratio, values },
        1.5,
      ),
      measure: scale(
        {
          configuration: "ranged",
          floor: "48ch",
          trunc: true,
          ratio,
          values,
        },
        "75ch",
      ),
    },
    spacing: scale({ configuration: "bidirectional", ratio, values }, "1ex"),
    grid: grid({ ratio, rows: 3 }, values),
    lengths: {
      width: scale(
        {
          configuration: "ranged",
          floor: "10vw",
          ratio,
          values,
        },
        "100vw",
      ),
      height: scale(
        {
          configuration: "ranged",
          floor: "10vh",
          ratio,
          values,
        },
        "100vh",
      ),
      smallest: scale(
        {
          configuration: "ranged",
          floor: "10vmin",
          ratio,
          values,
        },
        "100vmin",
      ),
      largest: scale(
        {
          configuration: "ranged",
          floor: "10vmax",
          ratio,
          values,
        },
        "100vmax",
      ),
    },
  },
};

tokens.js Output

Our tokens.js file should now look something like the following code.

import {
  adjust,
  convert,
  harmony,
  palette,
  a11y
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/color.js";
import {
  grid,
  scale,
  text,
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/content.js";

const swatch = convert("rgb", "#7ea");
const base = adjust({ chroma: -5, hue: 60 }, swatch);

const scheme = harmony({ configuration: "analogous" }, base);

const main = palette({
  configuration: "material",
  contrast: 95,
  accents: true,
  states: true
}, scheme[0]);

const accent = palette({
  configuration: "material",
  contrast: 95,
  accents: true,
}, scheme[1]);

const ui = a11y({ mode: "standard", rating: "AA", large: true }, main);
const body = a11y({ mode: "standard", rating: "AA" }, accent);

const ratio = 1.32;
const values = 4;

export default {
  hb: {
    color: {
      ui,
      text: body,
      splash: scheme[2],
    },
    text: {
      body: text({ system: "sans", weights: ["regular", "bold"] }, "Work Sans"),
      heading: text(
        { system: "serif", weights: ["light", "black"] },
        "Work Sans",
      ),
      size: scale(
        {
          configuration: "bidirectional",
          inversion: "em",
          ratio,
          values,
        },
        "1rem",
      ),
      leading: scale(
        { configuration: "ranged", floor: 1.2, ratio, values },
        1.5,
      ),
      measure: scale(
        {
          configuration: "ranged",
          floor: "48ch",
          trunc: true,
          ratio,
          values,
        },
        "75ch",
      ),
    },
    spacing: scale({ configuration: "bidirectional", ratio, values }, "1ex"),
    grid: grid({ ratio, rows: 3 }, values),
    lengths: {
      width: scale(
        {
          configuration: "ranged",
          floor: "10vw",
          ratio,
          values,
        },
        "100vw",
      ),
      height: scale(
        {
          configuration: "ranged",
          floor: "10vh",
          ratio,
          values,
        },
        "100vh",
      ),
      smallest: scale(
        {
          configuration: "ranged",
          floor: "10vmin",
          ratio,
          values,
        },
        "100vmin",
      ),
      largest: scale(
        {
          configuration: "ranged",
          floor: "10vmax",
          ratio,
          values,
        },
        "100vmax",
      ),
    },
  },
};

Advanced Workflow

The basic workflow is great for small projects that need a singular data set.

The cracks in this approach starts to show the second you want to work with multiple data sets.

The library provides a workflow.js module to handle these advanced use cases. Its only purpose is altering the way library functions work to unlock design patterns that will be valuable for the developer who needs to scale.

If the basic workflow is a bottom-up procedure where we assemble data from a known value, then advanced usage dictates a top-down set of rules for unknown values.

Modules

The first thing to do is import workflow.js, so let’s do that now.

import {
  adjust,
  convert,
  harmony,
  palette,
  a11y
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/color.js";
import {
  grid,
  scale,
  text,
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/content.js";
import {
  preset,
  process,
  pipeline,
  delegate
} from "https://cdn.jsdelvr.net/gh/quarksuite/[email protected]/workflow.js";

Color

Now, what set of rules directs our color token generation? Think about it for a second before we go on.

Rules

1. Color

1a. Color should be converted to RGB
const toRgb = preset(convert, "rgb");
1b. Color chroma should be reduced by 5% and hue adjusted clockwise 60 degrees
const reduceChroma5 = preset(adjust, { chroma: -5 });
const shiftHueRight60 = preset(adjust, { hue: 60 });
1c. Color should be scaled to an analogous harmony
const scaleToAnalogous = preset(harmony, { configuration: "analogous" });

2. Palette

2a. Generate material palettes for UI and text (with accents and state)
const generateUiPalette = preset(palette, {
  configuration: "material",
  accents: true,
  states: true
});

const generateTextPalette = preset(palette, {
  configuration: "material",
  accents: true
});
2b. Palettes should be filtered to meet desired WCAG accessibility standards
const filterUiContext = preset(a11y, {
  mode: "standard",
  rating: "AA",
  large: true
});

const filterTextContext = preset(a11y, {
  mode: "standard",
  rating: "AA"
});

Result

All of the above makes our actual color generation code read like an order.

“Convert #7ea to RGB. Reduce chroma by 5 and shift hue 60 degrees right. Next, scale the result to an analogous harmony. Then delegate the UI and text color token processes as main and accent. Leave splash alone.”

The output is identical to the basic procedure but expressed in a more declarative way.

const scheme = pipeline(
  "#7ea",
  toRgb,
  reduceChroma5,
  shiftHueRight60,
  scaleToAnalogous
);

const [main, accent, splash] = delegate(
  scheme,
  process(generateUiPalette, filterUiContext),
  process(generateTextPalette, filterTextContext),
  undefined
);

Content

Content modular scales are so simple compared to color that applying a top-down approach to them usually isn’t necessary. Let’s say we do it anyway, though. How would that look?

We’re going to reuse our global settings as well.

const ratio = 1.32;
const values = 4;

Rules

1. Text

1a. Body uses regular and bold weights with a sans-serif system fallback
const bodyTokens = preset(text, { system: "sans", weights: ["regular", "bold"]});
1b. Headings use light and black weights with a serif system fallback
const headingTokens = preset(text, { system: "serif", weights: ["light", "black"]});
1c. Size is bidirectional in rem units with an em inversion
const sizeTokens = preset(scale, { configuration: "bidirectional", inversion: "em", ratio, values });
1d. Leading is ranged with a root of 1.5 and a floor of 1.2
const leadingTokens = preset(scale, { configuration: "ranged", floor: 1.2, ratio, values });
1e. Measure is ranged with a root of 75ch and a floor of 48ch
const measureTokens = preset(scale, {
  configuration: "ranged",
  floor: "48ch",
  trunc: true,
  ratio,
  values
});

2. Spacing

2a. Spacing is bidirectional in ex units
const spacingTokens = preset(scale, { configuration: "bidirectional", ratio, values });

3. Grid

3a. Grid is 4 columns/rows and fractionals use global ratio
const gridTokens = preset(grid, { ratio });

4. Lengths

4a. Lengths are viewport relative and ranged with a root value of 100 and a floor of 10.
const lengthOpts = { configuration: "ranged", floor: 10, ratio, values };
const lengthTokens = [
  preset(scale, lengthOpts),
  preset(scale, lengthOpts),
  preset(scale, lengthOpts),
  preset(scale, lengthOpts),
];

Result

Now we’ll delegate our content scale rules to generated scales. The text category has five subcategories.

const [body, heading] = delegate(
  ["Work Sans", "Work Sans"],
  bodyTokens,
  headingTokens
);

const [
  size,
  leading,
  measure
] = delegate(
  ["1rem", 1.5, "75ch"],
  sizeTokens,
  leadingTokens,
  measureTokens
);

Spacing is its own category.

const spacing = spacingTokens("1ex");

Then we want to generate our grid category.

const gridOut = gridTokens(values);

Finally, we apply the length rules to four subcategories.

const [width, height, smallest, largest] = delegate(
  ["100vw", "100vh", "100vmin", "100vmax"],
  ...lengthTokens
);

Tokens

Since we’ve used a top-down approach, our token dictionary is going to look a little different. We’ve now effectively separated token generation behavior from token collection structure. This means it’s easier to restructure our collection as needed.

export default {
  hb: {
    color: { ui: main, text: accent, splash },
    text: { body, heading, size, leading, measure },
    spacing,
    grid: gridOut,
    lengths: { width, height, smallest, largest }
  }
}

tokens.js Output

We’re done, so let’s take a final look at what we’ve done.

import {
  adjust,
  convert,
  harmony,
  palette,
  a11y,
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/color.js";
import {
  grid,
  scale,
  text,
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/content.js";
import {
  preset,
  process,
  pipeline
  delegate
} from "https://cdn.jsdelvr.net/gh/quarksuite/[email protected]/workflow.js";

const toRgb = preset(convert, "rgb");

const reduceChroma5 = preset(adjust, { chroma: -5 });
const shiftHueRight60 = preset(adjust, { hue: 60 });

const scaleToAnalogous = preset(harmony, { configuration: "analogous" });

const generateUiPalette = preset(palette, {
  configuration: "material",
  accents: true,
  states: true
});

const generateTextPalette = preset(palette, {
  configuration: "material",
  accents: true
});

const filterUiContext = preset(a11y, {
  mode: "standard",
  rating: "AA",
  large: true
});

const filterTextContext = preset(a11y, {
  mode: "standard",
  rating: "AA"
});

const scheme = pipeline(
  "#7ea",
  toRgb,
  reduceChroma5,
  shiftHueRight60,
  scaleToAnalogous
);

const [main, accent, splash] = delegate(
  scheme,
  process(generateUiPalette, filterUiContext),
  process(generateTextPalette, filterTextContext),
  undefined
);

const ratio = 1.32;
const values = 4;

const bodyTokens = preset(text, { system: "sans", weights: ["regular", "bold"]});

const headingTokens = preset(text, { system: "serif", weights: ["light", "black"]});

const sizeTokens = preset(scale, { configuration: "bidirectional", inversion: "em", ratio, values });

const leadingTokens = preset(scale, { configuration: "ranged", floor: 1.2, ratio, values });

const measureTokens = preset(scale, {
  configuration: "ranged",
  floor: "48ch",
  trunc: true,
  ratio,
  values
});

const spacingTokens = preset(scale, { configuration: "bidirectional", ratio, values });

const gridTokens = preset(grid, { ratio });

const lengthOpts = { configuration: "ranged", floor: 10, ratio, values };
const lengthTokens = [
  preset(scale, lengthOpts),
  preset(scale, lengthOpts),
  preset(scale, lengthOpts),
  preset(scale, lengthOpts),
];

const [body, heading] = delegate(
  ["Work Sans", "Work Sans"],
  bodyTokens,
  headingTokens
);

const [
  size,
  leading,
  measure
] = delegate(
  ["1rem", 1.5, "75ch"],
  sizeTokens,
  leadingTokens,
  measureTokens
);

const spacing = spacingTokens("1ex");

const gridOut = gridTokens(values);

const [width, height, smallest, largest] = delegate(
  ["100vw", "100vh", "100vmin", "100vmax"],
  ...lengthTokens
);

export default {
  hb: {
    color: { ui: main, text: accent, splash },
    text: { body, heading, size, leading, measure },
    spacing,
    grid: gridOut,
    lengths: { width, height, smallest, largest }
  }
}

Modular Workflow

The more complex our token generation needs become, the more we’ll start identifying habits in our process. These habits will replicate over projects and it will become tedious to set up the boilerplate. The solution here is to automate our habits.

Wrapping them in functions is the simplest approach. That’s the one we’ll use.

A modular workflow involves shifting your rules and processes from active to passive behavior. Think about what remains constant and what changes, and then expose only those knobs.

At the modular level, it’ll also be a good idea to break away from the data-last architecture we’ve been using up until now. This will save us from defining defaults every time we invoke our custom functions.

You may have also noticed that tokens.js is growing with each rule and process we define. Time to break things up.

Color

First, we’ll create a new file named color-recipe.js.

The key to refactoring our color token generation is to identify the variables and turn them into knobs without changing the meaning of our rules. How can we do that?

Modules

import {
  convert,
  harmony,
  palette,
  a11y
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/color.js";
import {
  preset,
  process,
  pipeline,
  delegate
} from "https://cdn.jsdelvr.net/gh/quarksuite/[email protected]/workflow.js";

Constants

We look at our constants:

  • The palettes will always output with material configurations
  • The palettes will always output with accents and interface states
  • UI and text contexts will always be delegated to the first two indexes
  • UI context will always be calibrated for AA large accessibility
  • Text context will always be calibrated for AA accessibility
  • Any remaining indexes are left untouched
const generateUiPalette = preset(palette, {
  configuration: "material",
  accents: true,
  states: true
});

const generateTextPalette = preset(palette, {
  configuration: "material",
  accents: true
});

const filterUiContext = preset(a11y, {
  mode: "standard",
  rating: "AA",
  large: true
});

const filterTextContext = preset(a11y, {
  mode: "standard",
  rating: "AA"
});

Recipe

And derive a recipe from our variables:

  • The color won’t always have its properties adjusted
  • The color won’t always scale to an analogous harmony
  • The output won’t always be in RGB format
export default function(color, scheme = "analogous", format = "rgb") {
  const setFormat = preset(convert, format);
  const setScheme = preset(harmony, { configuration: scheme });

  const base = pipeline(color, setFormat, setScheme);

  return delegate(
    Array.isArray(base)
      ? base
      : [base, base],
    process(generateUiPalette, filterUiContext),
    process(generateTextPalette, filterTextContext),
    undefined,
    undefined
  );
}

Content

First, create a new file named content-recipes.js.

Now we’re going to modularize one category at a time.

Modules

import {
  grid,
  scale,
  text
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/content.js";
import {
  preset,
  delegate
} from "https://cdn.jsdelvr.net/gh/quarksuite/[email protected]/workflow.js";

Text

Constants

  • Body family always outputs with regular and bold weights
  • Heading family always outputs with light and black weights
  • Size is always bidirectional in rems with em inversion
  • Leading is always a unitless range
  • Measure is always ranged in ch

Recipe

  • Body system fallback is not always sans-serif
  • Heading system fallback is not always serif
  • The default minimum and maximum leading is not always a good fit
  • The default minimum and maximum measure is not always a good fit
export function Font(font, bodyFallback = "sans", headingFallback = "serif") {
  const bodyTokens = preset(text, { system: bodyFallback, weights: ["regular", "bold"] });
  const headingTokens = preset(text, { system: headingFallback, weights: ["light", "black"] });

  return delegate([font, font], bodyTokens, headingTokens);
}

const ratio = 1.32;
const values = 4;

export function Content([size, leading, measure], measureFloor = 48, leadingFloor = 1.2) {
  const sizeTokens = preset(scale, {
    configuration: "bidirectional",
    inversion: "em",
    ratio,
    values
  });

  const leadingTokens = preset(scale, {
    configuration: "ranged",
    floor: leadingFloor,
    ratio,
    values
  });

  const [measureMin, measureMax] = measure;
  const measureTokens = preset(scale, {
    configuration: "ranged",
    floor: measureFloor,
    trunc: true,
    ratio,
    values

  });

  return delegate([size, leading, measure], sizeTokens, leadingTokens, measureTokens);
}

Spacing

Recipe

  • The spacing will not always be in ex
export function Spacing(root) {
  return scale({ configuration: "bidirectional" }, root);
}

Grid

Recipe

  • The grid will not always be 4 columns/rows
export function Grid(columns = 4, rows = columns) {
  return grid({ ratio, rows }, columns);
}

Lengths

Constants

  • The output will always be viewport relative corresponding with dimensions

Recipe

  • The output will not always need every dimension
  • The default minimum and maximum length is not always a good fit
export function Dimensions(
  root,
  dimensions = ["width", "height", "min", "max"],
  floor = 10
) {
  const lengthOpts = { configuration: "ranged", floor, ratio, values };

  const targets = [
    ["width", "vw"],
    ["height", "vh"],
    ["min", "vmin"],
    ["max", "vmax"]
  ];

  return targets
    .filter(([context], index) => context === dimensions[index])
    .map(([, length]) => scale(lengthOpts, String(root).concat(length)));
}

Tokens

Finally, let’s reassemble our tokens using the recipes we just made.

import { adjust } from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/color.js";
import Palette from "./color-recipe.js";
import { Font, Content, Spacing, Grid, Dimensions } from "./content-recipes.js";

const [main, accent, splash] = Palette(adjust({
    chroma: -5,
    hue: 60
}, "#7ea"));

const [body, heading] = Font("Work Sans");

const [size, leading, measure] = Content(["1rem", 1.5, "75ch"]);

const spacing = Spacing("1ex");

const grid = Grid();

const [width, height, smallest, largest] = Dimensions(100);

export default {
  hb: {
    color: { ui: main, text: accent, splash },
    text: { body, heading, size, leading, measure },
    spacing,
    grid,
    lengths: { width, height, smallest, largest }
  }
}

Exporting Tokens

At this point, it’s important to note that it’s a good idea to keep your token generating code apart from your exporting code. This will allow you to tailor your exporting process to a given JavaScript engine. And this means you can safely adapt the exporting logic for different engines.

Example:

  • build.web.js: when using the native web
  • build.node.js: when using Node.js
  • build.deno.js: when using Deno
  • build.qjs.js: when using QuickJS

Generally speaking, you will not need to export your tokens more than a few times during development, but I’m sure you can see the usefulness of this structure.

If your web project uses JavaScript itself to style your interface (such as a CSS-in-JS library): congratulations. You’re done already. Go forth and create.

For the rest of us, we’ll need to export our tokens to use in our target environments.

Since we’re still here, we’ll now create a build.js file for the exporting process.

1. Import exporter.js module

First, we have to pull in the exporters themselves before we can do anything.

import {
  stylesheet,
  data,
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/exporter.js";

2. Import the generated tokens

Next, we import the tokens we created in tokens.js.

import tokens from "./tokens.js";

3. Define project

This step is crucial. Unless we wrap the tokens in an object that contains a project property, the exporters will throw an error. This is by design; it prevents us from accidentally invoking an exporter on arbitrary token collections.

In this sense, project works like a tag that tells an exporter “this is a complete dictionary. You may proceed”. Otherwise, it’s “stop what you’re doing. Right now.”

We’ll store the token dictionary as dict for later.

const dict = {
  project: {
    name: "Handbook Example Tokens",
    author: "Chatman R. Jr",
    license: "Unlicense",
    version: "0.1.0"
  },
  ...tokens
};

4. Set domain targets

At this point, you should know that the exporter functions do not write to your filesystem. This is for security.

Instead, they format the token dictionary to a file-ready state which you can then write to a file yourself using your environment’s native API or a library.

Here’s the fun part. We’ll format our dictionary based on the domain targets.

In this case, we want to export our tokens as CSS custom properties and JSON. And let’s also store the results in targets.

As a bonus, exporters transform token collections in a dictionary recursively. This means the structure of your token collection is your choice.

const targets = {
  css: stylesheet("css", dict),
  json: data("json", dict)
};

5. Write to filesystem

Hint: if you’re using QuarkSuite server side and you’re exporting a single format, you can print the output of the exporter to the console and copy/paste or pipe the result to a new file.

Time to actually write the file to our OS. Let’s assume we’ve been building our tokens in Deno (v1.20.5) so far.

import { ensureDir } from "https://deno.land/[email protected]/fs/mod.ts";

const out = "./dist";

// This will create the output directory if it does not exist
await ensureDir(out);

Object.entries(targets).forEach(async ([ext, output]) => {
  await Deno.writeTextFile(out.concat(`/tokens.${ext}`), output);
});

6. Run build

Finally, we run build.js to create our export files.

deno run --allow-read --allow-write build.js

This will output ./dist with our exported tokens.

dist
├── tokens.css
└── tokens.json

build.js Output

Our build file is now complete and we won’t need to touch it again for a good while.

import {
  stylesheet,
  data,
} from "https://cdn.jsdelivr.net/gh/quarksuite/[email protected]/exporter.js";

import tokens from "./tokens.js";

const dict = {
  project: {
    name: "Handbook Example Tokens",
    author: "Chatman R. Jr",
    license: "Unlicense",
    version: "0.1.0"
  },
  ...tokens
};

const targets = {
  css: stylesheet("css", dict),
  json: data("json", dict)
};

import { ensureDir } from "https://deno.land/[email protected]/fs/mod.ts";

const out = "./dist";

// This will create the output directory if it does not exist
await ensureDir(out);

Object.entries(targets).forEach(async ([ext, output]) => {
  await Deno.writeTextFile(out.concat(`/tokens.${ext}`), output);
});

Next Steps

With that, we’ve reached the end of the handbook. Hopefully, you were able to follow along. You now know the core approaches to using and customizing QuarkSuite for your design token workflow. I didn’t cover everything, but I want to think I’ve provided a good foundation for the experimental reader to build on.

If I missed the mark, submit an issue as always.

If you’ve read the handbook but you haven’t read the API, I recommend doing that because it expands on the technical details not clarified here.

To those who have already read both documents by now: thank you for your time. I hope you got something out of it even if you don’t use the library.