Repository: CrowdStrike/glide-core
Pull request #732: Add new Figma Variable syncing
Author: login: ynotdraw
Created: 2025-02-27T16:55:21Z | Merged: 2025-03-04T16:12:35Z

## 🚀 Description

This PR includes all of the changes to retrieve the Figma Variables from their API, convert them into the draft W3C [Design Token format](https://tr.designtokens.org/format), and then generate stylesheets from them.  This PR uses our new Glide variables ("Figma V3") versus what we have today ("Figma V2").

For the time being, this **does not** replace our existing stylesheets. This PR only writes to gitignored directories and allows for an easier review.  The work to fully replace `start:production:figma` will be done in a downstream PR.  It'll look something like this:

```diff
- "start:production:figma": "pnpm dt export-variables && pnpm dt build-tokens && pnpm dt build-styles",
+ "start:production:figma": "tsx ./src/figma/run.ts",
```

When the above change happens, our old stylesheets will be replaced by the new ones. I'll begin converting all components over to the new variables.  I also have a task for myself around some cleanup work, such as updating `.gitignore`, removing the design-tokens package we currently use, etc., I also plan on scripting the conversion of old tokens to new, so that's what I'll be working on next.

## 📋 Checklist

<!-- Please ensure you've gone through this checklist before adding reviewers. -->

- I have followed the [Contributing Guidelines](https://github.com/crowdstrike/glide-core/blob/main/CONTRIBUTING.md).
- I have added tests to cover new or updated functionality.
- I have added or updated Storybook stories.
- I have [localized](https://github.com/CrowdStrike/glide-core/blob/main/CONTRIBUTING.md#translations-and-static-strings) new strings.
- I have followed the [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/) or met with the Accessibility Team.
- I have included a [changeset](https://github.com/CrowdStrike/glide-core/blob/main/CONTRIBUTING.md#versioning-a-package).
- I have scheduled a design review.

## 🔬 How to Test

You can test this all locally by doing the following:

- Checkout this branch
- Verify you've followed our CONTRIBUTING.md file and have a `FIGMA_TOKEN` configured
- `pnpm i`
- Run `pnpm exec tsx ./src/figma/run.ts`
- Verify `stylesheets` and `tokens` directories are created when the script completes successfully

If you don't have access to the new Figma file, you will likely get an error when fetching the variables. If you want access, you'll need to request it through design.

## 📸 Images/Videos of Functionality

When running the script, you'll see something like this.

<img width="771" alt="Screenshot 2025-02-27 at 11 53 37 AM" src="https://github.com/user-attachments/assets/2cc93e0d-6b40-44c6-99c4-931a4ebcdcfe" />

After completed, two new directories will exist with some `.json` and `.css` files.

<img width="215" alt="Screenshot 2025-02-27 at 11 53 56 AM" src="https://github.com/user-attachments/assets/4d53963a-6304-4ab1-b617-501a6e4815f4" />

And opening up one of the CSS files should look something like this.

<img width="1230" alt="Screenshot 2025-02-27 at 11 54 14 AM" src="https://github.com/user-attachments/assets/7d0c29cb-c7f8-4430-bd98-dec07bf35443" />






Diff:
diff --git a/.gitignore b/.gitignore
index d3f416ef8..f2b9ba882 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ variables.json
 tokens.json
 dist/
 node_modules/
+tokens/
+stylesheets/
diff --git a/eslint.config.js b/eslint.config.js
index e1c76142b..45fcaef14 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -282,6 +282,12 @@ export default [
       globals: globals.node,
     },
   },
+  {
+    files: ['src/figma/*.ts'],
+    languageOptions: {
+      globals: globals.node,
+    },
+  },
   {
     files: [
       '**/*.js',
diff --git a/package.json b/package.json
index f15eec030..70cb55e80 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
     "!dist/*.test.*.js",
     "!dist/cem-analyzer-plugins",
     "!dist/eslint",
+    "!dist/figma",
     "!dist/icons/storybook.*",
     "!dist/storybook",
     "!dist/stylelint",
@@ -114,6 +115,7 @@
     "@custom-elements-manifest/analyzer": "^0.10.4",
     "@eslint/eslintrc": "^3.2.0",
     "@eslint/js": "^9.17.0",
+    "@figma/rest-api-spec": "^0.24.0",
     "@open-wc/testing": "^4.0.0",
     "@playwright/test": "^1.50.1",
     "@rollup/plugin-commonjs": "^28.0.2",
@@ -185,7 +187,8 @@
     "typescript": "^5.8.2",
     "typescript-eslint": "^8.26.0",
     "vite": "^6.1.1",
-    "vitest": "^3.0.6"
+    "vitest": "^3.0.6",
+    "yocto-spinner": "^0.2.0"
   },
   "engines": {
     "node": ">= 20",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0d7d7c4c0..3b4bad375 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
       '@eslint/js':
         specifier: ^9.17.0
         version: 9.17.0
+      '@figma/rest-api-spec':
+        specifier: ^0.24.0
+        version: 0.24.0
       '@open-wc/testing':
         specifier: ^4.0.0
         version: 4.0.0
@@ -257,6 +260,9 @@ importers:
       vitest:
         specifier: ^3.0.6
         version: 3.0.6(@types/node@22.10.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
+      yocto-spinner:
+        specifier: ^0.2.0
+        version: 0.2.0
 
 packages:
 
@@ -582,6 +588,9 @@ packages:
   '@esm-bundle/chai@4.3.4-fix.0':
     resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==}
 
+  '@figma/rest-api-spec@0.24.0':
+    resolution: {integrity: sha512-c/LHQNzfn8HSuo608TnfHJS8K3Ps61MvDbqTTL+qVx2FCIui7dI3RC2bG2/kSHmQXXKTbgbcAADyU6Rf8YkZbQ==}
+
   '@floating-ui/core@1.6.8':
     resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
 
@@ -4611,6 +4620,14 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
+  yocto-spinner@0.2.0:
+    resolution: {integrity: sha512-Qu6WAqNLGleB687CCGcmgHIo8l+J19MX/32UrSMfbf/4L8gLoxjpOYoiHT1asiWyqvjRZbgvOhLlvne6E5Tbdw==}
+    engines: {node: '>=18.19'}
+
+  yoctocolors@2.1.1:
+    resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
+    engines: {node: '>=18'}
+
   zod@3.23.8:
     resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
 
@@ -4978,6 +4995,8 @@ snapshots:
     dependencies:
       '@types/chai': 4.3.20
 
+  '@figma/rest-api-spec@0.24.0': {}
+
   '@floating-ui/core@1.6.8':
     dependencies:
       '@floating-ui/utils': 0.2.8
@@ -9449,4 +9468,10 @@ snapshots:
 
   yocto-queue@0.1.0: {}
 
+  yocto-spinner@0.2.0:
+    dependencies:
+      yoctocolors: 2.1.1
+
+  yoctocolors@2.1.1: {}
+
   zod@3.23.8: {}
diff --git a/src/figma/fetch-figma-variables.ts b/src/figma/fetch-figma-variables.ts
new file mode 100644
index 000000000..c7ed2b770
--- /dev/null
+++ b/src/figma/fetch-figma-variables.ts
@@ -0,0 +1,59 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables and variable collections from Figma's API¹
+ * and returns them.
+ *
+ * 1: https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { fileId: string; token: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. See [`CONTRIBUTING.md`](https://github.com/CrowdStrike/glide-core/blob/main/CONTRIBUTING.md#updating-style-variables) for more information.',
+    );
+  }
+
+  const spinner = yoctoSpinner({ text: 'Fetching Figma variables…' }).start();
+
+  try {
+    const response = await fetch(
+      `https://api.figma.com/v1/files/${fileId}/variables/local?scopes=file_variables:read&branch_data=true`,
+      {
+        method: 'GET',
+        headers: {
+          'X-FIGMA-TOKEN': token,
+        },
+      },
+    );
+
+    if (!response.ok) {
+      throw new Error(
+        `The Figma API failed with a ${response.status} error code.`,
+      );
+    }
+
+    const tokens = (await response.json()) as GetLocalVariablesResponse;
+
+    if (tokens.error) {
+      throw new Error("Figma's API returned an unknown error.");
+    }
+
+    if (Object.keys(tokens.meta.variables).length === 0) {
+      throw new Error('There were no variables returned in the response.');
+    }
+
+    if (Object.keys(tokens.meta.variableCollections).length === 0) {
+      throw new Error(
+        'There were no variable collections returned in the response.',
+      );
+    }
+
+    spinner.success('Figma variables collected.');
+
+    return tokens.meta;
+  } catch (error) {
+    spinner.error('An error occurred fetching the tokens from Figma.');
+    throw error;
+  }
+};
diff --git a/src/figma/get-css-variables.ts b/src/figma/get-css-variables.ts
new file mode 100644
index 000000000..08e657cd7
--- /dev/null
+++ b/src/figma/get-css-variables.ts
@@ -0,0 +1,188 @@
+import { readdir, readFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isDesignToken from './is-design-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step to
+ * generate CSS variables, grouped by collection and mode,
+ * from all of the Design Tokens. This function will return an
+ * object in the following pattern:
+ *
+ * {
+ *   "collection-1-mode-1": [
+ *     "variable-1: #000000",
+ *     "variable-2: #000001",
+ *   ],
+ *   "collection-1-mode-2": [
+ *     "variable-1: #000000",
+ *     "variable-2: #000001",
+ *   ],
+ *   "collection-2": [
+ *     "variable-1: #ffffff,
+ *     "variable-2: #000001,
+ *   ]
+ * }
+ *
+ * Each variable follows this naming convention to align with Design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async (directory: string) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to CSS variables…\n',
+  }).start();
+
+  let files: string[];
+
+  try {
+    files = await readdir(directory);
+  } catch (error) {
+    spinner.error(`Failed to read the tokens directory "${directory}".`);
+    throw error;
+  }
+
+  try {
+    const variablesByCollection: Record<string, string[]> = {};
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // It's safe to drop the extension completely at this point.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}".`,
+        );
+      }
+
+      const fileContent = await readFile(path.join(directory, file), 'utf8');
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const variables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      variablesByCollection[fileName] = variables;
+    }
+
+    spinner.success('Tokens successfully converted to CSS variables.');
+
+    return variablesByCollection;
+  } catch (error) {
+    spinner.error('An error occurred converting the tokens to CSS variables.');
+    throw error;
+  }
+};
+
+/**
+ * Loops over every Design Token, looks at its `$type`, and determines
+ * how to create a CSS property and value from it.
+ */
+function generateCSSVariablesFromTokens({
+  prefix,
+  tokens,
+}: {
+  prefix: string;
+  tokens: TokenGroup;
+}): string[] {
+  const cssVariables: string[] = [];
+
+  for (const [key, value] of Object.entries(tokens)) {
+    if (isDesignToken(value)) {
+      // Skip tokens with empty or undefined scopes.
+      // If the scope is empty, it's an extended style.
+      // Extended styles are local to a component and not
+      // meant for global use.
+      //
+      // These are similar in concept to our "private"-prefixed
+      // CSS variables in code, but on the Figma side.
+      if (!value.$extensions?.['com.figma']?.scopes?.length) {
+        continue;
+      }
+
+      let cssValue: string;
+
+      switch (value.$type) {
+        case 'color':
+        case 'string': {
+          cssValue = value.$value;
+          break;
+        }
+        case 'dimension': {
+          const dimension = value.$value;
+          cssValue = `${dimension.value}${dimension.unit}`;
+          break;
+        }
+        case 'number':
+        case 'fontWeight': {
+          cssValue = value.$value.toString();
+          break;
+        }
+        case 'fontFamily': {
+          cssValue = `"${value.$value}"`;
+          break;
+        }
+
+        default: {
+          // This shouldn't happen given all of the checks prior to this point.
+          //
+          // But why `JSON.stringify()`? It may not be needed, but if `value` happened
+          // to be an object at this point, without `JSON.stringify()` we'd get
+          // `[object Object]` logged. It's safer to always stringify.
+          throw new Error(`Unknown $type for "${JSON.stringify(value)}".`);
+        }
+      }
+
+      cssVariables.push(`${prefix}-${key.toLowerCase()}: ${cssValue};`);
+    } else if (typeof value === 'object') {
+      // Why wouldn't `value` be a Design Token?
+      //
+      // It's the start of our Token Group.
+      // Remember how we store tokens:
+      //
+      // {
+      //   "group": {
+      //     "token-1": {
+      //       "$type": "color",
+      //       "$value": "#000000"
+      //     }
+      //   }
+      // }
+      //
+      // At this stage in the process, we are at the `group` level.
+      // We need to go one layer deeper to get to our actual
+      // token, so we recursively call this function to
+      // get there.
+      cssVariables.push(
+        ...generateCSSVariablesFromTokens({
+          tokens: value,
+          prefix: `${prefix}-${key.toLowerCase()}`,
+        }),
+      );
+    }
+  }
+
+  return cssVariables;
+}
diff --git a/src/figma/get-design-tokens.ts b/src/figma/get-design-tokens.ts
new file mode 100644
index 000000000..4b919e75a
--- /dev/null
+++ b/src/figma/get-design-tokens.ts
@@ -0,0 +1,423 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type DesignToken, type TokenGroup, type TokensFile } from './types.js';
+import isDesignToken from './is-design-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` meta from Figma's API to
+ * generate Design Tokens¹ grouped by the Figma collection.
+ *
+ * 1: https://tr.designtokens.org/format/#design-token
+ */
+export default (meta: GetLocalVariablesResponse['meta']) => {
+  if (!meta) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(meta);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
+
+function isRGBA(object: unknown): object is RGBA {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'r' in object &&
+    'g' in object &&
+    'b' in object &&
+    'a' in object &&
+    typeof (object as RGBA).r === 'number' &&
+    typeof (object as RGBA).g === 'number' &&
+    typeof (object as RGBA).b === 'number' &&
+    typeof (object as RGBA).a === 'number'
+  );
+}
+
+function isVariableAlias(object: unknown): object is VariableAlias {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'type' in object &&
+    (object as VariableAlias).type === 'VARIABLE_ALIAS' &&
+    'id' in object &&
+    typeof (object as VariableAlias).id === 'string'
+  );
+}
+
+function getTokenValueFromVariable({
+  $type,
+  modeId,
+  scopes,
+  variable,
+  variables,
+}: {
+  $type: DesignToken['$type'];
+  modeId: string;
+  scopes: VariableScope[];
+  variable: LocalVariable;
+  variables: Record<string, LocalVariable>;
+}): string | number | { value: number; unit: 'rem' } {
+  const value = variable.valuesByMode[modeId];
+
+  // When a variable is an object, it's either a color
+  // or a `VariableAlias`.
+  if (typeof value === 'object') {
+    // A `VariableAlias` is a variable that references
+    // another variable.
+    if (isVariableAlias(value)) {
+      const aliasedVariable = variables[value.id];
+
+      if (!aliasedVariable) {
+        throw new Error(`Aliased variable not found for "${value.id}".`);
+      }
+
+      const [maybeRgba] = Object.values(aliasedVariable.valuesByMode);
+
+      // In some cases, a variable is only aliased one level deep
+      // and we can extract the value immediately.
+      // In other cases, a variable is aliased again (and again!),
+      // which means we recursively need to call this function
+      // until we get to a non-aliased variable value that we can parse.
+      return isRGBA(maybeRgba)
+        ? rgbToHex(maybeRgba)
+        : getTokenValueFromVariable({
+            $type,
+            modeId,
+            scopes,
+            variable: aliasedVariable,
+            variables,
+          });
+    }
+
+    if (isRGBA(value)) {
+      return rgbToHex(value);
+    }
+
+    throw new Error(
+      `The variable "${JSON.stringify(value)}" is an object, but is unable to be parsed to a hex value or variable alias.`,
+    );
+  }
+
+  if (typeof value === 'number') {
+    // Figma's API returns numbers as pixels, but we prefer using
+    // rem instead. Assuming a dimension¹ should use a rem unit
+    // may not be true in the future, so we may need to update accordingly
+    // as new use cases come in.
+    //
+    // 1: https://tr.designtokens.org/format/#dimension
+    if ($type === 'dimension') {
+      return { value: value / 16, unit: 'rem' };
+    }
+
+    return value;
+  }
+
+  return String(value);
+}
+
+function buildTokensFromVariables({
+  variables,
+  variableCollections,
+}: {
+  variables: GetLocalVariablesResponse['meta']['variables'];
+  variableCollections: GetLocalVariablesResponse['meta']['variableCollections'];
+}) {
+  const tokens: Record<string, TokensFile> = {};
+  const deletedButReferencedVariableNames: string[] = [];
+
+  for (const variable of Object.values(variables)) {
+    // A remote variable¹ is a type of variable that is defined and managed in a separate file
+    // and can be used across multiple files or projects.
+    // We should not use remote variables in our Design System as we instead rely on
+    // local variables from the provided Figma file.
+    //
+    // 1: https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    if (variable.remote) {
+      continue;
+    }
+
+    // `deletedButReferenced¹` indicates that the variable was deleted in the editor,
+    // but the document may still contain references to the variable.
+    // References to the variable may exist through bound values or variable aliases.
+    //
+    // We should not generate a token from these variables, but we can notify the
+    // design team of their existence, in hopes they can be properly removed from
+    // Figma.
+    //
+    // 1: https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    if (variable.deletedButReferenced) {
+      deletedButReferencedVariableNames.push(variable.name);
+      continue;
+    }
+
+    const collection = variableCollections[variable.variableCollectionId];
+
+    if (!collection) {
+      throw new Error(
+        `Could not find a collection with the id of "${variable.variableCollectionId}".`,
+      );
+    }
+
+    for (const mode of collection.modes) {
+      // "mode" is used to differentiate between "light" and "dark" mode variables;
+      // however, Figma also adds a default mode when one is not specified.
+      // Typically this is labeled as "Mode 1". We could rely on "Mode 1", or we
+      // could instead be explicit with our known, custom modes - "light" and "dark".
+      //
+      // If the time comes where we have additional modes to support,
+      // we'll need to modify this logic.
+      const modeName =
+        mode.name.toLowerCase() === 'light' ||
+        mode.name.toLowerCase() === 'dark'
+          ? mode.name.toLowerCase()
+          : null;
+
+      // Token file names use the `.tokens.json` extension to follow the
+      // file extension¹ guidance provided by the draft specification.
+      //
+      // Tokens are grouped based on the Figma collection they are a part of.
+      // Some collections also have a mode specified, as outlined above.
+      // Others do not, which we refer to as "modeless collections".
+      // These are used for variables that don't need different values based on
+      // the theme, for example. They contain a single set of values for each variable.
+      //
+      // The naming convention we use is `{collection}.tokens.json` for modeless collections.
+      // Collections with modes should use kebab casing, e.g., `{collection}-{mode}.tokens.json`.
+      //
+      // Adjusting this naming convention will have consequences in other parts of the
+      // export process, so take care in making changes here.
+      //
+      // Additionally, we use the collection and optional mode as the file name
+      // as it aligns closely with what users see in Figma's UI. This helps with
+      // troubleshooting.
+      //
+      // 1: https://tr.designtokens.org/format/#file-extensions
+      const tokenFileName = modeName
+        ? `${collection.name.toLowerCase()}-${modeName}.tokens.json`
+        : `${collection.name.toLowerCase()}.tokens.json`;
+
+      if (!tokens[tokenFileName]) {
+        tokens[tokenFileName] = {};
+      }
+
+      let tokenGroup: TokenGroup = tokens[tokenFileName];
+
+      // In Figma, variables can be organized by adding them to groups within a collection.
+      // Here's how that looks in Figma's UI:
+      //
+      // color-collection/
+      // ├─ parent-group/
+      // │  ├─ child-group-1/
+      // │  │  ├─ variable-1
+      // │  │  ├─ variable-2
+      // │  ├─ child-group-2/
+      // │  │  ├─ variable-3
+      //
+      // In our API response, a Figma variable's name includes the group(s) it is part of,
+      // separated by a `/`. `parent-group/child-group-1/variable-1` would be the full name
+      // for `variable-1` in the example above.
+      //
+      // To make it easier to do a diff between Figma's UI and our generated
+      // token JSON files, we put them in a similar format, where each group is a separate
+      // key.
+      //
+      // {
+      //   "parent-group": {
+      //     "child-group": {
+      //       "variable-1": #000000;
+      //     }
+      //   }
+      // }
+      //
+      // To do this, we break the full name into parts separated by `/`.
+      const parts = variable.name.split('/');
+
+      for (const [index, part] of parts.entries()) {
+        // When we get to the last part, we can stop
+        // building up our `tokenGroup` object keys
+        // as we're now at the full variable name.
+        // We'll write a Design Token to this last
+        // part in a moment.
+        if (index === parts.length - 1) {
+          break;
+        }
+
+        if (!(part in tokenGroup) || isDesignToken(tokenGroup[part])) {
+          tokenGroup[part] = {};
+        }
+
+        if (!tokenGroup[part]) {
+          throw new Error(
+            `Part "${part}" in "${JSON.stringify(tokenGroup)}" does not exist.`,
+          );
+        }
+
+        tokenGroup = tokenGroup[part];
+      }
+
+      const $type = getTokenTypeFromVariable({
+        scopes: variable.scopes,
+        variable,
+      });
+
+      const token = {
+        $type,
+        $value: getTokenValueFromVariable({
+          $type,
+          modeId: mode.modeId,
+          scopes: variable.scopes,
+          variable,
+          variables,
+        }),
+        $description: variable.description,
+        $extensions: {
+          'com.figma': {
+            hiddenFromPublishing: variable.hiddenFromPublishing,
+            scopes: variable.scopes,
+            codeSyntax: variable.codeSyntax,
+            mode: mode.name,
+          },
+        },
+      } as Extract<DesignToken, { $type: typeof $type }>;
+
+      // As mentioned above, the groups of a variable, separated by a `/`, are used as keys.
+      //
+      // Going back to the example above of the `parent-group/child-group-1/variable-1`
+      // full name, `parent-group` and `child-group-1` will be nested keys in the object,
+      // with `variable-1` holding the actual token value.
+      //
+      // Now that we have our token constructed, we simply need to grab the last
+      // segment (`variable-1`) and write our token to it.
+      const lastPart = parts.at(-1);
+
+      if (!lastPart) {
+        throw new Error(`Empty token name for variable "${variable.id}".`);
+      }
+
+      tokenGroup[lastPart] = token;
+    }
+  }
+
+  return {
+    deletedButReferencedVariableNames,
+    tokens,
+  };
+}
+
+/**
+ * Converts a Figma variable's `resolvedType` to a Design Token `$type`.
+ */
+function getTokenTypeFromVariable({
+  scopes,
+  variable,
+}: {
+  scopes: VariableScope[];
+  variable: LocalVariable;
+}) {
+  switch (variable.resolvedType) {
+    case 'COLOR': {
+      return 'color';
+    }
+    case 'FLOAT': {
+      // Checking `scopes.includes('FONT_WEIGHT')` would
+      // be great, but unfortunately our font-weight scopes
+      // are coming back as `FONT_STYLE` from Figma's API.
+      // They're scoped as "Font Weight" in Figma's UI though.
+      // We'll keep looking into why that is.
+      //
+      // For now, we have to rely on `FONT_STYLE` and the
+      // variable name starting with `weight/` to determine
+      // if it is a weight and then manually remap the type.
+      if (
+        scopes.includes('FONT_STYLE') &&
+        variable.name.toLowerCase().startsWith('weight/')
+      ) {
+        return 'fontWeight';
+      }
+
+      // `line-height` and `opacity` CSS properties are numbers¹.
+      // There are more number types, of course, but we don't have needs
+      // for them at the moment.
+      //
+      // 1: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#numbers
+      if (scopes.includes('LINE_HEIGHT') || scopes.includes('OPACITY')) {
+        return 'number';
+      }
+
+      // Unitless CSS values are numeric values without a specific unit of
+      // measurement (like px, em, rem, etc.). They are typically used as
+      // multipliers or ratios.
+      //
+      // `font-weight`, `opacity,` and `line-height` are currently the only
+      // variables that are unitless from Figma. All other scopes that are
+      // numbers should be converted from pixels to rems.
+      //
+      // As time goes on, we may collect additional unitless CSS values
+      // that come back as a FLOAT from Figma. When that is the case, we'll
+      // need to update this `case` block to handle these new units.
+      //
+      // Because we've handled fontWeight and other unitless cases above,
+      // it's safe to assume that if we land here, we want these values in
+      // rems and thus, can use the `dimension` type.
+      return 'dimension';
+    }
+    case 'STRING': {
+      if (scopes.includes('FONT_FAMILY')) {
+        return 'fontFamily';
+      }
+
+      return 'string';
+    }
+
+    default: {
+      throw new Error(
+        `Could not convert Figma's "${variable.resolvedType}" resolvedType to a Design Token $type.`,
+      );
+    }
+  }
+}
diff --git a/src/figma/is-design-token.ts b/src/figma/is-design-token.ts
new file mode 100644
index 000000000..425c1eabb
--- /dev/null
+++ b/src/figma/is-design-token.ts
@@ -0,0 +1,13 @@
+import type { DesignToken } from './types.js';
+
+/**
+ * Type guard to verify a provided object is a Design Token.
+ * Used across multiple files for type narrowing.
+ */
+export default (object: unknown): object is DesignToken => {
+  if (typeof object !== 'object' || object === null) {
+    return false;
+  }
+
+  return '$type' in object && '$value' in object;
+};
diff --git a/src/figma/run.ts b/src/figma/run.ts
new file mode 100644
index 000000000..6a7343124
--- /dev/null
+++ b/src/figma/run.ts
@@ -0,0 +1,44 @@
+import fetchFigmaVariables from './fetch-figma-variables.js';
+import getCSSVariables from './get-css-variables.js';
+import getDesignTokens from './get-design-tokens.js';
+import writeDesignTokens from './write-design-tokens.js';
+import writeStylesheets from './write-stylesheets.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables. Converts those
+ * variables into a Design Token format. Uses the Design
+ * Tokens to build CSS custom properties, grouped by
+ * the Figma collection.  Writes stylesheets for each
+ * Figma collection with the CSS custom properties.
+ *
+ * This process has many layers and requires a general understanding
+ * of Figma variables, collections, and modes¹. A general understanding
+ * of the W3C Design Token format draft² is also recommended.
+ *
+ * 1: https://github.com/open-wc/custom-elements-manifest/blob/e7ec7424aa418b220fc4ec7e70464b4d6f35b596/packages/analyzer/src/features/analyse-phase/creators/createClass.js#L151-L153
+ * 2: https://tr.designtokens.org/format
+ */
+async function run() {
+  const figmaVariables = await fetchFigmaVariables({
+    fileId: figmaFileId,
+    token: figmaToken,
+  });
+
+  const tokens = getDesignTokens(figmaVariables);
+
+  await writeDesignTokens({
+    directory: tokensDirectory,
+    tokens,
+  });
+
+  const cssVariables = await getCSSVariables(tokensDirectory);
+
+  await writeStylesheets({ directory: stylesheetsDirectory, cssVariables });
+}
+
+await run();
diff --git a/src/figma/types.ts b/src/figma/types.ts
new file mode 100644
index 000000000..bfbf63967
--- /dev/null
+++ b/src/figma/types.ts
@@ -0,0 +1,118 @@
+import {
+  type VariableCodeSyntax,
+  type VariableScope,
+} from '@figma/rest-api-spec';
+
+// A Token is an object in a standardized, machine-readable format.
+//
+// We follow the Design Tokens Format Module¹ W3C draft. It provides
+// a technical specification for a file format to exchange Design Tokens
+// between different tools. While we don't leverage additional tooling
+// at the moment, this allows us to potentially leverage one in the future,
+// while also putting our tokens in a format that is easily readable by humans
+// and parseable by code.
+//
+// Tokens are distinct from Figma variables, in that a token² is a name/value
+// pair (with other properties), while a variable in Figma stores multiple
+// values, one for each mode.
+//
+// The `$type` property of a Token follows the format specified in the types
+// documentation³.
+//
+// We support a subset of the types listed in the document. `duration` and
+// `cubic-bezier` are not currently applicable to our variables.
+//
+// Adding support for these types would lead to additional code for
+// processing them. It's better to be explicit in the types we support and
+// keep an eye on the W3C document for updates. If a Figma variable is
+// created using a currently unsupported type, we can add it then.
+//
+// Additionally, we diverge slightly from the draft W3C types by allowing
+// for a `string` type. This is to better align with the resolved types
+// for Figma variables.
+//
+// `font-style` CSS properties, for example, should use a `string` type until
+// the specification provides additional guidance on these properties.
+// At the moment, these types are being considered, but no
+// guidance currently exists⁴.
+//
+// 1: https://tr.designtokens.org/format/
+// 2: https://tr.designtokens.org/format/#design-token
+// 3: https://tr.designtokens.org/format/#types
+// 4: https://tr.designtokens.org/format/#additional-types
+
+interface BaseToken {
+  $description?: string;
+  $extensions?: {
+    // Used to store Figma-specific variable properties.
+    //
+    // https://tr.designtokens.org/format/#extensions-0
+    'com.figma'?: {
+      codeSyntax?: VariableCodeSyntax;
+      hiddenFromPublishing?: boolean;
+      mode?: string;
+      scopes?: VariableScope[];
+    };
+  };
+}
+
+interface ColorToken extends BaseToken {
+  $type: 'color';
+  $value: string;
+}
+
+interface DimensionToken extends BaseToken {
+  $type: 'dimension';
+  $value: { value: number; unit: 'rem' };
+}
+
+interface FontFamilyToken extends BaseToken {
+  $type: 'fontFamily';
+  $value: string;
+}
+
+interface FontWeightToken extends BaseToken {
+  $type: 'fontWeight';
+  $value: number;
+}
+
+interface NumberToken extends BaseToken {
+  $type: 'number';
+  $value: number;
+}
+
+interface StringToken extends BaseToken {
+  $type: 'string';
+  $value: string;
+}
+
+export type DesignToken =
+  | ColorToken
+  | DimensionToken
+  | FontFamilyToken
+  | FontWeightToken
+  | NumberToken
+  | StringToken;
+
+export interface TokenGroup {
+  [key: string]: DesignToken | TokenGroup;
+}
+
+/**
+ * Defines what we expect a Design Tokens file to look like.
+ *
+ * This format mostly adheres to the draft W3C spec for Design Tokens¹.
+ * As mentioned at the top of this file, we did make a few changes to `$type`.
+ *
+ * We expect each tokens file to define tokens for a single variable collection and mode.
+ * There isn't a way currently to represent modes or themes in the W3C community group
+ * Design Token specification. Once the spec resolves how it wants to handle modes, we'll
+ * need to update our code to reflect the new standard. For the time being, we add mode
+ * information via the `$extensions['com.figma']` property.
+ *
+ * There's an open discussion² one can follow for more information.
+ *
+ * 1: https://tr.designtokens.org/format/#file-format
+ * 2: https://github.com/design-tokens/community-group/issues/210
+ */
+export type TokensFile = TokenGroup;
diff --git a/src/figma/write-design-tokens.ts b/src/figma/write-design-tokens.ts
new file mode 100644
index 000000000..ecfce898b
--- /dev/null
+++ b/src/figma/write-design-tokens.ts
@@ -0,0 +1,36 @@
+import { mkdir, writeFile } from 'node:fs/promises';
+import yoctoSpinner from 'yocto-spinner';
+import type { TokenGroup } from './types.js';
+
+/**
+ * Writes the tokens to the provided directory. Having them on disk helps with
+ * debugging by letting us manually verify them against what's shown in
+ * Figma's UI.
+ */
+export default async ({
+  directory,
+  tokens,
+}: {
+  directory: string;
+  tokens: Record<string, TokenGroup>;
+}) => {
+  const spinner = yoctoSpinner({ text: 'Writing tokens to files…\n' }).start();
+
+  try {
+    await mkdir(directory, { recursive: true });
+
+    for (const [fileName, fileContent] of Object.entries(tokens)) {
+      await writeFile(
+        `${directory}/${fileName}`,
+        JSON.stringify(fileContent, null, 2),
+      );
+    }
+
+    spinner.success(
+      `Token files have been written to the "${directory}" directory.`,
+    );
+  } catch (error) {
+    spinner.error('An error occurred writing the tokens to disc.');
+    throw error;
+  }
+};
diff --git a/src/figma/write-stylesheets.ts b/src/figma/write-stylesheets.ts
new file mode 100644
index 000000000..87db1277f
--- /dev/null
+++ b/src/figma/write-stylesheets.ts
@@ -0,0 +1,89 @@
+import { mkdir, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { format } from 'prettier';
+
+/**
+ * Writes CSS variables to the provided directory.
+ */
+export default async ({
+  cssVariables,
+  directory,
+}: {
+  cssVariables: Record<string, string[]>;
+  directory: string;
+}) => {
+  if (!cssVariables) {
+    throw new Error('There were no CSS variables to write.');
+  }
+
+  if (!directory) {
+    throw new Error('A directory to write the stylesheets to is required.');
+  }
+
+  const spinner = yoctoSpinner({
+    text: 'Converting variables to stylesheets…\n',
+  }).start();
+
+  for (const [collection, variables] of Object.entries(cssVariables)) {
+    let cssContent = '/* Generated file. Do not edit directly. */';
+
+    // We need to handle the CSS generation for our color collections
+    // quite a bit differently from the other stylesheets. Light and dark
+    // stylesheets use the same variable names, but different variable
+    // values. They also set `color-scheme` and use different selectors
+    // between light and dark modes.
+    if (collection.startsWith('color-')) {
+      const theme = collection.replace('color-', '');
+
+      if (theme === 'light') {
+        cssContent += `
+          :root,
+          :host,
+          .theme-light {
+            color-scheme: light;
+            ${variables.join('\n  ')}
+          }`;
+      }
+
+      if (theme === 'dark') {
+        cssContent += `
+          :host,
+          .theme-dark {
+            color-scheme: dark;
+            ${variables.join('\n  ')}
+          }`;
+      }
+    } else {
+      // All non-color collections simply target `:root`.
+      cssContent += `
+        :root {
+          ${variables.join('\n  ')}
+        }`;
+    }
+
+    try {
+      const formattedContent = await format(cssContent, {
+        parser: 'css',
+      });
+
+      const cssPath = path.join(
+        process.cwd(),
+        `./${directory}/${collection}.css`,
+      );
+
+      const stylesheetsDirectory = path.dirname(cssPath);
+
+      await mkdir(stylesheetsDirectory, { recursive: true });
+
+      await writeFile(cssPath, formattedContent, 'utf8');
+    } catch (error) {
+      spinner.error('An error occurred generating the stylesheets.');
+      throw error;
+    }
+  }
+
+  spinner.success(
+    `Stylesheets have been written to the "${directory}" directory.`,
+  );
+};
diff --git a/terser.js b/terser.js
index b4727d6b5..290986768 100644
--- a/terser.js
+++ b/terser.js
@@ -11,6 +11,7 @@ const paths = await globby([
   '!**/cem-analyzer-plugins/**',
   '!**/coverage/**',
   '!**/eslint/**',
+  '!**/figma/**',
   '!**/stylelint/**',
   '!**/ts-morph*/**',
 ]);


Reviews:
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T19:16:01Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T19:19:32Z
user: login: danwenzel
body: Nice work! 🎉 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-02-27T21:25:02Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T16:53:24Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:30:31Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:31:15Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:32:20Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:36:11Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:37:33Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:50:30Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:51:27Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:51:41Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T17:52:57Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T18:01:42Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T18:14:50Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T18:19:08Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T18:21:57Z
user: login: clintcs
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T19:14:47Z
user: login: danwenzel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T19:47:17Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:06:10Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:13:03Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:13:31Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:16:12Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:25:03Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:47:40Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:48:43Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T22:24:55Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T22:51:11Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T22:54:49Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T23:02:34Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T23:56:24Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T23:57:53Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T23:58:20Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T23:59:10Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:02:39Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:03:12Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:04:00Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:04:23Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:04:48Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:05:13Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:05:46Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:06:00Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:06:15Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:06:54Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:11:54Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:12:34Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:14:15Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T01:27:24Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T02:12:57Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T12:57:41Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T13:19:07Z

Review comments:
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
+  if (!tokensJson) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(tokensJson);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: Throughout the rest of the code, I `throw` when things go wrong.  But this case doesn't warrant throwing and preventing the process from completing. It's more informational than anything. So `console.warn` it is.
created_at: 2025-02-27T18:43:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
+  if (!tokensJson) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(tokensJson);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
+
+function isRGBA(object: unknown): object is RGBA {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'r' in object &&
+    'g' in object &&
+    'b' in object &&
+    'a' in object &&
+    typeof (object as RGBA).r === 'number' &&
+    typeof (object as RGBA).g === 'number' &&
+    typeof (object as RGBA).b === 'number' &&
+    typeof (object as RGBA).a === 'number'
+  );
+}
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: Determining what shape an object is from the API response requires checks like this so that we get type guards and type narrowing.
created_at: 2025-02-27T18:45:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // Dashes are used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on the dash here to get what we're after.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+
+        if (theme === 'dark') {
+          generatedContent += `\n:host,\n.theme-dark {\n  color-scheme: dark;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+      } else {
+        // All non-color collections simply target `:root`.
+        generatedContent += `\n:root {\n  ${cssVariables.join('\n  ')}\n}\n`;
+      }
+
+      const cssPath = path.join(
+        process.cwd(),
+        `./${outputDirectory}/${fileName}.css`,
+      );
+
+      const directory = path.dirname(cssPath);
+
+      await mkdir(directory, { recursive: true });
+
+      await writeFile(cssPath, generatedContent, 'utf8');
+    }
+
+    spinner.success(
+      `Stylesheets have been written to the "${outputDirectory}" directory.`,
+    );
+  } catch (error) {
+    spinner.error('An error occurred generating the stylesheets.');
+    throw error;
+  }
+};
+
+/**
+ * Loops over every design token, looks at the `$type`, and determines
+ * how to create a CSS property and value from it.
+ */
+function generateCSSVariablesFromTokens({
+  prefix,
+  tokens,
+}: {
+  prefix: string;
+  tokens: TokenGroup;
+}): string[] {
+  const cssVariables: string[] = [];
+
+  for (const [key, value] of Object.entries(tokens)) {
+    if (isToken(value)) {
+      // Skip tokens with empty or undefined scopes.
+      // If the scope is empty, it's an extended style.
+      // Extended styles are local to a component and not
+      // meant for global use.
+      //
+      // These are similar in concept to our "private"-prefixed
+      // CSS variables in code, but on the Figma side.
+      if (!value.$extensions?.['com.figma']?.scopes?.length) {
+        continue;
+      }
+
+      let cssValue: string;
+
+      switch (value.$type) {
+        case 'color':
+        case 'string': {
+          cssValue = value.$value;
+          break;
+        }
+        case 'dimension': {
+          const dimension = value.$value;
+          cssValue = `${dimension.value}${dimension.unit}`;
+          break;
+        }
+        case 'number':
+        case 'fontWeight': {
+          cssValue = value.$value.toString();
+          break;
+        }
+        case 'fontFamily': {
+          cssValue = `"${value.$value}"`;
+          break;
+        }
+
+        default: {
+          throw new Error(`Unknown $type for "${JSON.stringify(value)}".`);
path: src/figma/generate-stylesheets.ts
user: login: ynotdraw
body: This should never happen (said every developer!) given all of the checks prior to this point.

But why `JSON.stringify`? It may not be needed, but if `value` happened to be an object somehow at this point, without `JSON.stringify` we'd get everyone's favorite `[object Object]`.  Easier to just always stringify to play it safe.
created_at: 2025-02-27T18:56:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,107 @@
+import {
+  type VariableCodeSyntax,
+  type VariableScope,
+} from '@figma/rest-api-spec';
+
+// A Token is an object in a standardized, machine-readable format.
+//
+// We follow the Design Tokens Format Module (https://tr.designtokens.org/format/)
+// W3C draft. It provides a technical specification for a file format to exchange
+// design tokens between different tools. While we don't leverage additional tooling
+// at the moment, this allows us to potentially leverage one in the future, while also
+// putting our tokens in a format that is easily readable by humans and parseable by code.
+//
+// Tokens are distinct from Figma variables, in that a token (https://tr.designtokens.org/format/#design-token)
+// is a name/value pair (with other properties), while a variable in Figma stores multiple values,
+// one for each mode.
+//
+// The `$type` property of a Token follows the format specified at https://tr.designtokens.org/format/#types.
+//
+// We support a subset of the types listed in the document. `duration`
+// and `cubic-bezier` are not currently applicable to our variables.
+//
+// Adding support for these types would lead to additional code for
+// processing them. It's better to be explicit in the types we support and
+// keep an eye on the W3C document for updates. If a Figma variable is
+// created using a currently unsupported type, we can add it then.
+//
+// Additionally, we diverge slightly from the draft W3C types by allowing
+// for a `string` type. This is to better align with the resolved types
+// for Figma variables.
+//
+// `font-style` CSS properties, for example, should use a `string` type until
+// the specification provides additional guidance on these properties.
+// At the moment, these types are being considered, but no
+// guidance currently exists (https://tr.designtokens.org/format/#additional-types).
+
+interface BaseToken {
path: src/figma/types.ts
user: login: ynotdraw
body: I think using discriminated unions cleaned things up quite a bit here and helped with types. It does add more code, but it allows us to scope `$value`'s type based on `$type`. If you want to see a commit _before_ I added these check out https://github.com/CrowdStrike/glide-core/pull/732/commits/fe4e62f8418e9734b5188a80941cbe725540cc84.  I liked how I could remove some `typeof` checks 👍 
created_at: 2025-02-27T19:10:15Z
author_association: COLLABORATOR
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: Before getting to this file, I'd recommend checking out `run.ts` first and going function-by-function rather than how GitHub presents these files in a review.  May be easier to follow all of the changes.
created_at: 2025-02-27T19:11:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
+  if (!tokensJson) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(tokensJson);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: I looked at existing packages to handle `rgb(a) => hex`.  I don't think they're worth it since this is 15 lines of code. Let me know if you feel differently.
created_at: 2025-02-27T19:13:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
path: src/figma/run.ts
user: login: ynotdraw
body: For now I'm writing to this temporary directory so we don't clobber the current stylesheets. But in my next PR, I'll update this to `src/styles/variables`. I'll also update the `.gitignore` when that happens 👍 
created_at: 2025-02-27T19:19:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: How about a multiline template literal instead of `\n`? If you want to clean it up after, you could run it through Prettier:

```ts
import { format } from 'prettier';
await format(code);
```
created_at: 2025-02-27T19:46:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // Dashes are used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on the dash here to get what we're after.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+
+        if (theme === 'dark') {
+          generatedContent += `\n:host,\n.theme-dark {\n  color-scheme: dark;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+      } else {
+        // All non-color collections simply target `:root`.
+        generatedContent += `\n:root {\n  ${cssVariables.join('\n  ')}\n}\n`;
+      }
+
+      const cssPath = path.join(
+        process.cwd(),
+        `./${outputDirectory}/${fileName}.css`,
+      );
+
+      const directory = path.dirname(cssPath);
+
+      await mkdir(directory, { recursive: true });
+
+      await writeFile(cssPath, generatedContent, 'utf8');
+    }
+
+    spinner.success(
+      `Stylesheets have been written to the "${outputDirectory}" directory.`,
+    );
+  } catch (error) {
+    spinner.error('An error occurred generating the stylesheets.');
+    throw error;
+  }
+};
+
+/**
+ * Loops over every design token, looks at the `$type`, and determines
+ * how to create a CSS property and value from it.
+ */
+function generateCSSVariablesFromTokens({
+  prefix,
+  tokens,
+}: {
+  prefix: string;
+  tokens: TokenGroup;
+}): string[] {
+  const cssVariables: string[] = [];
+
+  for (const [key, value] of Object.entries(tokens)) {
+    if (isToken(value)) {
+      // Skip tokens with empty or undefined scopes.
+      // If the scope is empty, it's an extended style.
+      // Extended styles are local to a component and not
+      // meant for global use.
+      //
+      // These are similar in concept to our "private"-prefixed
+      // CSS variables in code, but on the Figma side.
+      if (!value.$extensions?.['com.figma']?.scopes?.length) {
+        continue;
+      }
+
+      let cssValue: string;
+
+      switch (value.$type) {
+        case 'color':
+        case 'string': {
+          cssValue = value.$value;
+          break;
+        }
+        case 'dimension': {
+          const dimension = value.$value;
+          cssValue = `${dimension.value}${dimension.unit}`;
+          break;
+        }
+        case 'number':
+        case 'fontWeight': {
+          cssValue = value.$value.toString();
+          break;
+        }
+        case 'fontFamily': {
+          cssValue = `"${value.$value}"`;
+          break;
+        }
+
+        default: {
+          throw new Error(`Unknown $type for "${JSON.stringify(value)}".`);
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: Mind adding a code comment with some of the above?
created_at: 2025-02-27T22:52:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
path: src/figma/get-figma-variables.ts
user: login: clintcs
body: Configuring the token or simply getting one? Might also be nice to link to the relevant section of CONTRIBUTING.md`
created_at: 2025-02-27T22:54:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
path: src/figma/run.ts
user: login: clintcs
body: Nit. Any chance you could make all these lines a similar width?
created_at: 2025-03-03T15:40:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
path: src/figma/run.ts
user: login: clintcs
body: For consistency, how about `writeDesignTokens`? Or else `generateTokens` above?

Whatever you decide, don't forget about `is-tokens.ts` and `write-tokens.ts` 🙂.
created_at: 2025-03-03T15:48:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
+import { mkdir, writeFile } from 'node:fs/promises';
+import yoctoSpinner from 'yocto-spinner';
+import type { TokenGroup } from './types.js';
+
+/**
+ * Writes all design tokens to disc for convenience and
+ * so that they aren't stored solely in memory.
+ *
+ * Viewing the design tokens in the outputDirectory aids
+ * in troubleshooting and verification against Figma's UI.
+ */
path: src/figma/write-tokens.ts
user: login: clintcs
body: ```suggestion
/**
 * Writes the tokens to `outputDirectory`. Having them on disk helps with 
 * debugging by letting us manually verify them against what's shown in
 * Figma's UI.
 */
```
created_at: 2025-03-03T15:57:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
+    );
+  }
+
+  const spinner = yoctoSpinner({ text: 'Fetching Figma variables…' }).start();
+  let tokensJson: GetLocalVariablesResponse;
+
+  try {
+    const response = await fetch(
+      `https://api.figma.com/v1/files/${fileId}/variables/local?scopes=file_variables:read&branch_data=true`,
+      {
+        method: 'GET',
+        headers: {
+          'X-FIGMA-TOKEN': token,
+        },
+      },
+    );
+
+    if (!response.ok) {
+      throw new Error(
+        `The Figma API failed with a ${response.status} error code.`,
+      );
+    }
+
+    tokensJson = (await response.json()) as GetLocalVariablesResponse;
+
+    spinner.success('Figma variables collected.');
+
+    return tokensJson;
path: src/figma/get-figma-variables.ts
user: login: clintcs
body: Nit. Best to include in a `try` only what can throw for clarity, so the reader knows at a glance what throws and what doesn't.
created_at: 2025-03-03T16:02:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: ```suggestion
 * Each variable follows this naming convention to align with Design:
```
created_at: 2025-03-03T16:03:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: ```suggestion
 * Reads the `.tokens.json` files generated from the previous step
 * to generate stylesheets, grouped by collection and mode, from
 * all of the design tokens.
```
created_at: 2025-03-03T16:03:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: ```suggestion
      // It's safe to drop the extension completely. We only care about the file
      // name from this point on.
```
created_at: 2025-03-03T16:04:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: Does all of this need to be inside a `try`?
created_at: 2025-03-03T16:05:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+
+        if (theme === 'dark') {
+          generatedContent += `\n:host,\n.theme-dark {\n  color-scheme: dark;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+      } else {
+        // All non-color collections simply target `:root`.
+        generatedContent += `\n:root {\n  ${cssVariables.join('\n  ')}\n}\n`;
+      }
+
+      const cssPath = path.join(
+        process.cwd(),
+        `./${outputDirectory}/${fileName}.css`,
+      );
+
+      const directory = path.dirname(cssPath);
+
+      await mkdir(directory, { recursive: true });
+
+      await writeFile(cssPath, generatedContent, 'utf8');
+    }
+
+    spinner.success(
+      `Stylesheets have been written to the "${outputDirectory}" directory.`,
+    );
+  } catch (error) {
+    spinner.error('An error occurred generating the stylesheets.');
+    throw error;
+  }
+};
+
+/**
+ * Loops over every design token, looks at the `$type`, and determines
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: ```suggestion
 * Loops over every design token, looks at its `$type`, and determines
```
created_at: 2025-03-03T16:06:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
path: src/figma/run.ts
user: login: clintcs
body: I'll admit I find markdown with links pretty hard to read because I likely wouldn't hover over `run`. Instead, I would just read the comment because it's already in front of me. Anyone else do the same?
created_at: 2025-03-03T16:10:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
path: src/figma/run.ts
user: login: clintcs
body: Would it make sense to immediately throw if `process.env.FIGMA_TOKEN` isn't defined? 
created_at: 2025-03-03T16:11:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
path: src/figma/run.ts
user: login: clintcs
body: `variables` here and when passed to `generateDesignTokens()` but `tokensJson` inside of `generateDesignTokens()`?

Also, does the reader need to know at a glance that the tokens are grouped by collection? Or can `tokensGroupedByCollection` simply be `tokens` given the reader can hover `generateDesignTokens()` to get more information if he wants?
created_at: 2025-03-03T16:14:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
path: src/figma/run.ts
user: login: clintcs
body: How about `fetchFigmaVariables()` so it's clear that it's a network request and not something else? 

If you go that route, you might also be able to get away with renaming `generateDesignTokens()` to `getDesignTokens()`.
created_at: 2025-03-03T16:16:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
path: src/figma/run.ts
user: login: clintcs
body: What do you think? Does `run.js` need to know about `figmaToken` and `figmaFileId`? Or can both variables live in `get-figma-variables.ts`?
created_at: 2025-03-03T16:22:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
path: src/figma/run.ts
user: login: clintcs
body: Nit. That it's a directory for output is probably assumed given the function has "write" in its name.

```suggestion
    directory: tokensDirectory,
```
created_at: 2025-03-03T16:23:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
+  });
+
+  await generateStylesheets({
path: src/figma/run.ts
user: login: clintcs
body: This does more than generate stylesheets, eh? 

"generate" could normally imply writing them to disk as well. But it doesn't in the case of `generateDesignTokens()`. So I'd assume this function also doesn't do any writing.

What would you say to breaking it up into two separate files? That might better align with `writeTokens()` and `generateDesignTokens()`.
created_at: 2025-03-03T16:25:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
+    );
+  }
+
+  const spinner = yoctoSpinner({ text: 'Fetching Figma variables…' }).start();
+  let tokensJson: GetLocalVariablesResponse;
path: src/figma/get-figma-variables.ts
user: login: clintcs
body: Not JSON, eh?

```suggestion
  let tokens: GetLocalVariablesResponse;
```
created_at: 2025-03-03T16:30:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
+import { mkdir, writeFile } from 'node:fs/promises';
path: src/figma/write-tokens.ts
user: login: clintcs
body: `exists` and `existsSync` are [deprecated](https://nodejs.org/api/fs.html#fsexistspath-callback).

```suggestion
import { mkdir, stat, writeFile } from 'node:fs/promises';
```
created_at: 2025-03-03T16:41:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
path: src/figma/write-tokens.ts
user: login: clintcs
body: ```suggestion
```
created_at: 2025-03-03T16:41:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
path: src/figma/generate-design-tokens.ts
user: login: clintcs
body: Not JSON 🙂.
created_at: 2025-03-03T16:43:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
+  if (!tokensJson) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(tokensJson);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
+
+function isRGBA(object: unknown): object is RGBA {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'r' in object &&
+    'g' in object &&
+    'b' in object &&
+    'a' in object &&
+    typeof (object as RGBA).r === 'number' &&
+    typeof (object as RGBA).g === 'number' &&
+    typeof (object as RGBA).b === 'number' &&
+    typeof (object as RGBA).a === 'number'
+  );
+}
+
+function isVariableAlias(object: unknown): object is VariableAlias {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'type' in object &&
+    (object as VariableAlias).type === 'VARIABLE_ALIAS' &&
+    'id' in object &&
+    typeof (object as VariableAlias).id === 'string'
+  );
+}
+
+function getTokenValueFromVariable({
+  $type,
+  modeId,
+  scopes,
+  variable,
+  variables,
+}: {
+  $type: Token['$type'];
+  modeId: string;
+  scopes: VariableScope[];
+  variable: LocalVariable;
+  variables: Record<string, LocalVariable>;
+}): string | number | { value: number; unit: 'rem' } {
+  const value = variable.valuesByMode[modeId];
+
+  // When a variable is an object, it's either a color
+  // or a `VariableAlias`.
+  if (typeof value === 'object') {
+    // A `VariableAlias` is a variable that references
+    // another variable.
+    if (isVariableAlias(value)) {
+      const aliasedVariable = variables[value.id];
+
+      if (!aliasedVariable) {
+        throw new Error(`Aliased variable not found for "${value.id}".`);
+      }
+
+      const [maybeRgba] = Object.values(aliasedVariable.valuesByMode);
+
+      // In some cases, a variable is only aliased one level deep
+      // and we can extract the value immediately.
+      // In other cases, a variable is aliased again (and again!),
+      // which means we recursively need to call this function
+      // until we get to a non-aliased variable value that we can parse.
+      return isRGBA(maybeRgba)
+        ? rgbToHex(maybeRgba)
+        : getTokenValueFromVariable({
+            $type,
+            modeId,
+            scopes,
+            variable: aliasedVariable,
+            variables,
+          });
+    }
+
+    if (isRGBA(value)) {
+      return rgbToHex(value);
+    }
+
+    throw new Error(
+      `The variable "${JSON.stringify(value)}" is an object, but is unable to be parsed to a hex value or variable alias.`,
+    );
+  }
+
+  if (typeof value === 'number') {
+    // https://tr.designtokens.org/format/#dimension
+    //
+    // Figma's API returns numbers as pixels, but we prefer using
+    // rem instead. Assuming a `dimension` should use a rem unit
+    // may not be true in the future, so we may need to update accordingly
+    // as new use cases come in.
+    if ($type === 'dimension') {
+      return { value: value / 16, unit: 'rem' };
+    }
+
+    return value;
+  }
+
+  return String(value);
+}
+
+function buildTokensFromVariables(
+  localVariablesResponse: GetLocalVariablesResponse,
+) {
+  const tokens: Record<string, TokensFile> = {};
+  const deletedButReferencedVariableNames: string[] = [];
+
+  const { variables, variableCollections } = localVariablesResponse.meta;
+
+  for (const variable of Object.values(variables)) {
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // A `remote` variable is a type of variable that is defined and managed in a separate file
+    // and can be used across multiple files or projects.
+    // We should not use remote variables in our Design System as we instead rely on
+    // local variables from the provided Figma file.
+    if (variable.remote) {
+      continue;
+    }
+
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // `deletedButReferenced` indicates that the variable was deleted in the editor,
+    // but the document may still contain references to the variable.
+    // References to the variable may exist through bound values or variable aliases.
+    //
+    // We should not generate a token from these variables, but we can notify the
+    // design team of their existence, in hopes they can be properly removed from
+    // the Figma side of things.
+    if (variable.deletedButReferenced) {
+      deletedButReferencedVariableNames.push(variable.name);
+      continue;
+    }
+
+    const collection = variableCollections[variable.variableCollectionId];
+
+    if (!collection) {
+      throw new Error(
+        `Could not find a collection with the id of "${variable.variableCollectionId}".`,
+      );
+    }
+
+    for (const mode of collection.modes) {
+      // "mode" is used to differentiate between "light" and "dark" mode variables;
+      // however, Figma also adds a default mode when one is not specified.
+      // Typically this is labeled as "Mode 1". We could rely on "Mode 1", or we
+      // could instead be explicit with our known, custom modes - "light" and "dark".
+      //
+      // If the time comes where we have additional modes to support,
+      // we'll need to modify this logic.
+      const modeName =
+        mode.name.toLowerCase() === 'light' ||
+        mode.name.toLowerCase() === 'dark'
+          ? mode.name.toLowerCase()
+          : null;
+
+      // Token file names use the `.tokens.json` extension to follow the
+      // file extension (https://tr.designtokens.org/format/#file-extensions)
+      // guidance provided by the draft specification.
+      //
+      // Tokens are grouped based on the Figma collection they are a part of.
+      // Some collections also have a mode specified, as outlined above.
+      // Others do not, which we refer to as "modeless collections".
+      // These are used for variables that don't need different values based on
+      // the theme, for example. They contain a single set of values for each variable.
+      //
+      // The naming convention we use is `{collection}.tokens.json` for modeless collections.
+      // Collections with modes should use kebab casing, e.g., `{collection}-{mode}.tokens.json`.
+      //
+      // Adjusting this naming convention will have consequences in other parts of the
+      // export process, so take care in making changes here.
+      //
+      // Additionally, we use the collection and optional mode as the file name
+      // as it aligns closely with what users see in Figma's UI. This helps with
+      // troubleshooting.
+      const tokenFileName = modeName
+        ? `${collection.name.toLowerCase()}-${modeName}.tokens.json`
+        : `${collection.name.toLowerCase()}.tokens.json`;
+
+      if (!tokens[tokenFileName]) {
+        tokens[tokenFileName] = {};
+      }
+
+      let tokenGroup: TokenGroup = tokens[tokenFileName];
+
+      // In Figma, variables can be organized by adding them to groups within a collection.
+      // Here's how that looks in Figma's UI:
+      //
+      // color-collection/
+      // ├─ parent-group/
+      // │  ├─ child-group-1/
+      // │  │  ├─ variable-1
+      // │  │  ├─ variable-2
+      // │  ├─ child-group-2/
+      // │  │  ├─ variable-3
+      //
+      // In our API response, a Figma variable's name includes the group(s) it is part of, separated
+      // by a `/`. `parent-group/child-group-1/variable-1` would be the full name for `variable-1`
+      // in the example above.
+      //
+      // To make it easier to do a diff between Figma's UI and our generated
+      // token JSON files, we put them in a similar format, where each group is a separate
+      // key.
+      //
+      // {
+      //   "parent-group": {
+      //     "child-group": {
+      //       "variable-1": #000000;
+      //     }
+      //   }
+      // }
+      //
+      // To do this, we break the full name into parts separated by `/`.
+      const parts = variable.name.split('/');
+
+      for (let index = 0; index < parts.length - 1; index++) {
path: src/figma/generate-design-tokens.ts
user: login: clintcs
body: Do you need to use an imperative loop? Or can do `for...of` with `parts.values()`?


created_at: 2025-03-03T16:45:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
+import { mkdir, writeFile } from 'node:fs/promises';
path: src/figma/write-tokens.ts
user: login: ynotdraw
body: > Using fs.stat() to check for the existence of a file before calling fs.open(), fs.readFile(), or fs.writeFile() is not recommended. Instead, user code should open/read/write the file directly and handle the error raised if the file is not available.

Sounds like maybe we shouldn't use `stat` either, because we do `writeFile`. I'll revisit.
created_at: 2025-03-03T17:30:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
+  if (!tokensJson) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(tokensJson);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
+
+function isRGBA(object: unknown): object is RGBA {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'r' in object &&
+    'g' in object &&
+    'b' in object &&
+    'a' in object &&
+    typeof (object as RGBA).r === 'number' &&
+    typeof (object as RGBA).g === 'number' &&
+    typeof (object as RGBA).b === 'number' &&
+    typeof (object as RGBA).a === 'number'
+  );
+}
+
+function isVariableAlias(object: unknown): object is VariableAlias {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'type' in object &&
+    (object as VariableAlias).type === 'VARIABLE_ALIAS' &&
+    'id' in object &&
+    typeof (object as VariableAlias).id === 'string'
+  );
+}
+
+function getTokenValueFromVariable({
+  $type,
+  modeId,
+  scopes,
+  variable,
+  variables,
+}: {
+  $type: Token['$type'];
+  modeId: string;
+  scopes: VariableScope[];
+  variable: LocalVariable;
+  variables: Record<string, LocalVariable>;
+}): string | number | { value: number; unit: 'rem' } {
+  const value = variable.valuesByMode[modeId];
+
+  // When a variable is an object, it's either a color
+  // or a `VariableAlias`.
+  if (typeof value === 'object') {
+    // A `VariableAlias` is a variable that references
+    // another variable.
+    if (isVariableAlias(value)) {
+      const aliasedVariable = variables[value.id];
+
+      if (!aliasedVariable) {
+        throw new Error(`Aliased variable not found for "${value.id}".`);
+      }
+
+      const [maybeRgba] = Object.values(aliasedVariable.valuesByMode);
+
+      // In some cases, a variable is only aliased one level deep
+      // and we can extract the value immediately.
+      // In other cases, a variable is aliased again (and again!),
+      // which means we recursively need to call this function
+      // until we get to a non-aliased variable value that we can parse.
+      return isRGBA(maybeRgba)
+        ? rgbToHex(maybeRgba)
+        : getTokenValueFromVariable({
+            $type,
+            modeId,
+            scopes,
+            variable: aliasedVariable,
+            variables,
+          });
+    }
+
+    if (isRGBA(value)) {
+      return rgbToHex(value);
+    }
+
+    throw new Error(
+      `The variable "${JSON.stringify(value)}" is an object, but is unable to be parsed to a hex value or variable alias.`,
+    );
+  }
+
+  if (typeof value === 'number') {
+    // https://tr.designtokens.org/format/#dimension
+    //
+    // Figma's API returns numbers as pixels, but we prefer using
+    // rem instead. Assuming a `dimension` should use a rem unit
+    // may not be true in the future, so we may need to update accordingly
+    // as new use cases come in.
+    if ($type === 'dimension') {
+      return { value: value / 16, unit: 'rem' };
+    }
+
+    return value;
+  }
+
+  return String(value);
+}
+
+function buildTokensFromVariables(
+  localVariablesResponse: GetLocalVariablesResponse,
+) {
+  const tokens: Record<string, TokensFile> = {};
+  const deletedButReferencedVariableNames: string[] = [];
+
+  const { variables, variableCollections } = localVariablesResponse.meta;
+
+  for (const variable of Object.values(variables)) {
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // A `remote` variable is a type of variable that is defined and managed in a separate file
+    // and can be used across multiple files or projects.
+    // We should not use remote variables in our Design System as we instead rely on
+    // local variables from the provided Figma file.
+    if (variable.remote) {
+      continue;
+    }
+
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // `deletedButReferenced` indicates that the variable was deleted in the editor,
+    // but the document may still contain references to the variable.
+    // References to the variable may exist through bound values or variable aliases.
+    //
+    // We should not generate a token from these variables, but we can notify the
+    // design team of their existence, in hopes they can be properly removed from
+    // the Figma side of things.
+    if (variable.deletedButReferenced) {
+      deletedButReferencedVariableNames.push(variable.name);
+      continue;
+    }
+
+    const collection = variableCollections[variable.variableCollectionId];
+
+    if (!collection) {
+      throw new Error(
+        `Could not find a collection with the id of "${variable.variableCollectionId}".`,
+      );
+    }
+
+    for (const mode of collection.modes) {
+      // "mode" is used to differentiate between "light" and "dark" mode variables;
+      // however, Figma also adds a default mode when one is not specified.
+      // Typically this is labeled as "Mode 1". We could rely on "Mode 1", or we
+      // could instead be explicit with our known, custom modes - "light" and "dark".
+      //
+      // If the time comes where we have additional modes to support,
+      // we'll need to modify this logic.
+      const modeName =
+        mode.name.toLowerCase() === 'light' ||
+        mode.name.toLowerCase() === 'dark'
+          ? mode.name.toLowerCase()
+          : null;
+
+      // Token file names use the `.tokens.json` extension to follow the
+      // file extension (https://tr.designtokens.org/format/#file-extensions)
+      // guidance provided by the draft specification.
+      //
+      // Tokens are grouped based on the Figma collection they are a part of.
+      // Some collections also have a mode specified, as outlined above.
+      // Others do not, which we refer to as "modeless collections".
+      // These are used for variables that don't need different values based on
+      // the theme, for example. They contain a single set of values for each variable.
+      //
+      // The naming convention we use is `{collection}.tokens.json` for modeless collections.
+      // Collections with modes should use kebab casing, e.g., `{collection}-{mode}.tokens.json`.
+      //
+      // Adjusting this naming convention will have consequences in other parts of the
+      // export process, so take care in making changes here.
+      //
+      // Additionally, we use the collection and optional mode as the file name
+      // as it aligns closely with what users see in Figma's UI. This helps with
+      // troubleshooting.
+      const tokenFileName = modeName
+        ? `${collection.name.toLowerCase()}-${modeName}.tokens.json`
+        : `${collection.name.toLowerCase()}.tokens.json`;
+
+      if (!tokens[tokenFileName]) {
+        tokens[tokenFileName] = {};
+      }
+
+      let tokenGroup: TokenGroup = tokens[tokenFileName];
+
+      // In Figma, variables can be organized by adding them to groups within a collection.
+      // Here's how that looks in Figma's UI:
+      //
+      // color-collection/
+      // ├─ parent-group/
+      // │  ├─ child-group-1/
+      // │  │  ├─ variable-1
+      // │  │  ├─ variable-2
+      // │  ├─ child-group-2/
+      // │  │  ├─ variable-3
+      //
+      // In our API response, a Figma variable's name includes the group(s) it is part of, separated
+      // by a `/`. `parent-group/child-group-1/variable-1` would be the full name for `variable-1`
+      // in the example above.
+      //
+      // To make it easier to do a diff between Figma's UI and our generated
+      // token JSON files, we put them in a similar format, where each group is a separate
+      // key.
+      //
+      // {
+      //   "parent-group": {
+      //     "child-group": {
+      //       "variable-1": #000000;
+      //     }
+      //   }
+      // }
+      //
+      // To do this, we break the full name into parts separated by `/`.
+      const parts = variable.name.split('/');
+
+      for (let index = 0; index < parts.length - 1; index++) {
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: Sure, I can switch. We should make a lint rule about this if we have strong feelings using `for...of` over a for loop.
created_at: 2025-03-03T17:31:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: But it _is_ the raw JSON response from the API no? Maybe just `tokens` is fine because it's an object at that point?
created_at: 2025-03-03T17:32:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
path: src/figma/run.ts
user: login: ynotdraw
body: I was thinking that having them in `run.ts` would make it immediately clear what configuration options are required for this process to run.  So while it is specific to `get-figma-variables.ts`, I worried that burying them within that file would mean someone may skip over them.  We `throw` and things happen if that step breaks, but that's why I opted for this path instead.

Does that change your thoughts at all?
created_at: 2025-03-03T17:36:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
path: src/figma/run.ts
user: login: ynotdraw
body: I can try to adjust these a bit, sure.  But I think we need some automated tooling if we feel strongly about this. This is one thing we can eliminate completely from PR review discussions if it's automated away for us.
created_at: 2025-03-03T17:37:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
path: src/figma/get-figma-variables.ts
user: login: ynotdraw
body: > Configuring the token or simply getting one?

What do you prefer? One must generate a Figma API token, then configure their environment to expose it to our code here.
created_at: 2025-03-03T17:50:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
path: src/figma/run.ts
user: login: ynotdraw
body: Depending on https://github.com/CrowdStrike/glide-core/pull/732/files#r1977914989, if it all moves to `get-figma-variables.ts`, then we could do it there.
created_at: 2025-03-03T17:51:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
path: src/figma/run.ts
user: login: ynotdraw
body: @danwenzel what do you think?
created_at: 2025-03-03T17:51:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
path: src/figma/run.ts
user: login: ynotdraw
body: Sure!
created_at: 2025-03-03T17:52:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
path: src/figma/get-figma-variables.ts
user: login: clintcs
body: ```suggestion
      '"FIGMA_TOKEN" is a required environment variable. See [`CONTRIBUTING.md`](https://github.com/CrowdStrike/glide-core/blob/main/CONTRIBUTING.md#updating-style-variables) for more information.',
```
created_at: 2025-03-03T17:54:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
+    );
+  }
+
+  const spinner = yoctoSpinner({ text: 'Fetching Figma variables…' }).start();
+  let tokensJson: GetLocalVariablesResponse;
path: src/figma/get-figma-variables.ts
user: login: clintcs
body: Aye. That method doesn't return JSON. It parses JSON and returns a JavaScript object.
created_at: 2025-03-03T17:55:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
path: src/figma/run.ts
user: login: clintcs
body: I could see that too. No strong preference.
created_at: 2025-03-03T17:57:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
path: src/figma/run.ts
user: login: clintcs
body: No strong preference. Just struck me as a basic (if tedious) thing to do. Lint rules are, unfortunately, lacking in this area.
created_at: 2025-03-03T17:59:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
+import { mkdir, writeFile } from 'node:fs/promises';
path: src/figma/write-tokens.ts
user: login: clintcs
body: Does the race condition they're warning about apply to this case?
created_at: 2025-03-03T18:00:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
path: src/figma/generate-design-tokens.ts
user: login: clintcs
body: Ah, right. Well, not JSON either way.

```suggestion
export default (response: GetLocalVariablesResponse) => {
```

I'll add that it's a bit odd to be passing a response around instead of the result of the response—the data—which is all `generate-design-tokens.ts` is interested in.

If the response is empty, then `get-figma-variables.ts` should probably throw instead of kicking the can over to `get-figma-variables.ts`. The response's `error` field should also be checked by  `get-figma-variables.ts` unless `error` is always `false` when `response.ok` is truthy.
created_at: 2025-03-03T18:14:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
+  if (!tokensJson) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(tokensJson);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
+
+function isRGBA(object: unknown): object is RGBA {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'r' in object &&
+    'g' in object &&
+    'b' in object &&
+    'a' in object &&
+    typeof (object as RGBA).r === 'number' &&
+    typeof (object as RGBA).g === 'number' &&
+    typeof (object as RGBA).b === 'number' &&
+    typeof (object as RGBA).a === 'number'
+  );
+}
+
+function isVariableAlias(object: unknown): object is VariableAlias {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'type' in object &&
+    (object as VariableAlias).type === 'VARIABLE_ALIAS' &&
+    'id' in object &&
+    typeof (object as VariableAlias).id === 'string'
+  );
+}
+
+function getTokenValueFromVariable({
+  $type,
+  modeId,
+  scopes,
+  variable,
+  variables,
+}: {
+  $type: Token['$type'];
+  modeId: string;
+  scopes: VariableScope[];
+  variable: LocalVariable;
+  variables: Record<string, LocalVariable>;
+}): string | number | { value: number; unit: 'rem' } {
+  const value = variable.valuesByMode[modeId];
+
+  // When a variable is an object, it's either a color
+  // or a `VariableAlias`.
+  if (typeof value === 'object') {
+    // A `VariableAlias` is a variable that references
+    // another variable.
+    if (isVariableAlias(value)) {
+      const aliasedVariable = variables[value.id];
+
+      if (!aliasedVariable) {
+        throw new Error(`Aliased variable not found for "${value.id}".`);
+      }
+
+      const [maybeRgba] = Object.values(aliasedVariable.valuesByMode);
+
+      // In some cases, a variable is only aliased one level deep
+      // and we can extract the value immediately.
+      // In other cases, a variable is aliased again (and again!),
+      // which means we recursively need to call this function
+      // until we get to a non-aliased variable value that we can parse.
+      return isRGBA(maybeRgba)
+        ? rgbToHex(maybeRgba)
+        : getTokenValueFromVariable({
+            $type,
+            modeId,
+            scopes,
+            variable: aliasedVariable,
+            variables,
+          });
+    }
+
+    if (isRGBA(value)) {
+      return rgbToHex(value);
+    }
+
+    throw new Error(
+      `The variable "${JSON.stringify(value)}" is an object, but is unable to be parsed to a hex value or variable alias.`,
+    );
+  }
+
+  if (typeof value === 'number') {
+    // https://tr.designtokens.org/format/#dimension
+    //
+    // Figma's API returns numbers as pixels, but we prefer using
+    // rem instead. Assuming a `dimension` should use a rem unit
+    // may not be true in the future, so we may need to update accordingly
+    // as new use cases come in.
+    if ($type === 'dimension') {
+      return { value: value / 16, unit: 'rem' };
+    }
+
+    return value;
+  }
+
+  return String(value);
+}
+
+function buildTokensFromVariables(
+  localVariablesResponse: GetLocalVariablesResponse,
+) {
+  const tokens: Record<string, TokensFile> = {};
+  const deletedButReferencedVariableNames: string[] = [];
+
+  const { variables, variableCollections } = localVariablesResponse.meta;
+
+  for (const variable of Object.values(variables)) {
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // A `remote` variable is a type of variable that is defined and managed in a separate file
+    // and can be used across multiple files or projects.
+    // We should not use remote variables in our Design System as we instead rely on
+    // local variables from the provided Figma file.
+    if (variable.remote) {
+      continue;
+    }
+
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // `deletedButReferenced` indicates that the variable was deleted in the editor,
+    // but the document may still contain references to the variable.
+    // References to the variable may exist through bound values or variable aliases.
+    //
+    // We should not generate a token from these variables, but we can notify the
+    // design team of their existence, in hopes they can be properly removed from
+    // the Figma side of things.
+    if (variable.deletedButReferenced) {
+      deletedButReferencedVariableNames.push(variable.name);
+      continue;
+    }
+
+    const collection = variableCollections[variable.variableCollectionId];
+
+    if (!collection) {
+      throw new Error(
+        `Could not find a collection with the id of "${variable.variableCollectionId}".`,
+      );
+    }
+
+    for (const mode of collection.modes) {
+      // "mode" is used to differentiate between "light" and "dark" mode variables;
+      // however, Figma also adds a default mode when one is not specified.
+      // Typically this is labeled as "Mode 1". We could rely on "Mode 1", or we
+      // could instead be explicit with our known, custom modes - "light" and "dark".
+      //
+      // If the time comes where we have additional modes to support,
+      // we'll need to modify this logic.
+      const modeName =
+        mode.name.toLowerCase() === 'light' ||
+        mode.name.toLowerCase() === 'dark'
+          ? mode.name.toLowerCase()
+          : null;
+
+      // Token file names use the `.tokens.json` extension to follow the
+      // file extension (https://tr.designtokens.org/format/#file-extensions)
+      // guidance provided by the draft specification.
+      //
+      // Tokens are grouped based on the Figma collection they are a part of.
+      // Some collections also have a mode specified, as outlined above.
+      // Others do not, which we refer to as "modeless collections".
+      // These are used for variables that don't need different values based on
+      // the theme, for example. They contain a single set of values for each variable.
+      //
+      // The naming convention we use is `{collection}.tokens.json` for modeless collections.
+      // Collections with modes should use kebab casing, e.g., `{collection}-{mode}.tokens.json`.
+      //
+      // Adjusting this naming convention will have consequences in other parts of the
+      // export process, so take care in making changes here.
+      //
+      // Additionally, we use the collection and optional mode as the file name
+      // as it aligns closely with what users see in Figma's UI. This helps with
+      // troubleshooting.
+      const tokenFileName = modeName
+        ? `${collection.name.toLowerCase()}-${modeName}.tokens.json`
+        : `${collection.name.toLowerCase()}.tokens.json`;
+
+      if (!tokens[tokenFileName]) {
+        tokens[tokenFileName] = {};
+      }
+
+      let tokenGroup: TokenGroup = tokens[tokenFileName];
+
+      // In Figma, variables can be organized by adding them to groups within a collection.
+      // Here's how that looks in Figma's UI:
+      //
+      // color-collection/
+      // ├─ parent-group/
+      // │  ├─ child-group-1/
+      // │  │  ├─ variable-1
+      // │  │  ├─ variable-2
+      // │  ├─ child-group-2/
+      // │  │  ├─ variable-3
+      //
+      // In our API response, a Figma variable's name includes the group(s) it is part of, separated
+      // by a `/`. `parent-group/child-group-1/variable-1` would be the full name for `variable-1`
+      // in the example above.
+      //
+      // To make it easier to do a diff between Figma's UI and our generated
+      // token JSON files, we put them in a similar format, where each group is a separate
+      // key.
+      //
+      // {
+      //   "parent-group": {
+      //     "child-group": {
+      //       "variable-1": #000000;
+      //     }
+      //   }
+      // }
+      //
+      // To do this, we break the full name into parts separated by `/`.
+      const parts = variable.name.split('/');
+
+      for (let index = 0; index < parts.length - 1; index++) {
path: src/figma/generate-design-tokens.ts
user: login: clintcs
body: Unicorn—missing in action. It [should](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-for-loop.md) have had us covered. 
created_at: 2025-03-03T18:19:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
path: src/figma/get-figma-variables.ts
user: login: clintcs
body: Should this file throw? Or should `run.ts`, where the value is established, throw?
created_at: 2025-03-03T18:21:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
path: src/figma/run.ts
user: login: ynotdraw
body: > Also, does the reader need to know at a glance that the tokens are grouped by collection? Or can tokensGroupedByCollection simply be tokens given the reader can hover generateDesignTokens() to get more information if he wants?

Good point. Yeah, I'll simplify since the longer details are tucked into the individual functions.
created_at: 2025-03-03T19:21:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
path: src/figma/generate-stylesheets.ts
user: login: ynotdraw
body: Related https://github.com/CrowdStrike/glide-core/pull/732#discussion_r1978497233

Yeah, IO operations and `JSON.parse` could throw and we want to catch and re-throw the error so we can call `spinner.error` reliably when things go south. If we weren't using `spinner`, then it wouldn't matter and we could simply let the errors bubble up.

I did end up separating this logic out a little bit to help with some of the error messaging.
created_at: 2025-03-03T19:35:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
+import { mkdir, writeFile } from 'node:fs/promises';
path: src/figma/write-tokens.ts
user: login: ynotdraw
body: Yeah, so it turns out I don't need either of these since if the directory doesn't exist, I'm going to create it anyway. `recursive: true` seems to be enough.  TIL!

```javascript
await mkdir(directory, { recursive: true });
```

Thanks for pointing this out.
created_at: 2025-03-03T19:46:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
path: src/figma/run.ts
user: login: danwenzel
body: Yeah, same here.  Just the URL would be fine with me
created_at: 2025-03-03T19:47:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
+  });
+
+  await generateStylesheets({
path: src/figma/run.ts
user: login: ynotdraw
body: > What would you say to breaking it up into two separate files? That might better align with writeTokens() and generateDesignTokens().

Fair point.  Breaking them up may make sense, but it'll introduce an additional loop over everything.  At the moment, we read through each file, then create a `.css` file based on the collection and then begin writing the variables for that collection to disc based on `generateCSSVariablesFromTokens()`.

With the proposed change, I'll need to break this up into two steps.  I'll read all of the files, store which collection (eventual file name) I'm in, call `generateCSSVariablesFromTokens()`, and store those variables with that collection. That'd be the first function - "`getVariables()`"?

Then the second function ( "`writeStylesheets()`"?) would be looping over those collections _again_ to write them to disc. 

What we have here only takes one loop around everything, although it does do more than mentioned in the function name.  Going back to our previous discussion, maybe the reader in `run.ts` doesn't care about all of these implementation details.  Maybe `writeStylesheets()` as the function name is enough.

Any preference from your end? 
created_at: 2025-03-03T19:52:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
path: src/figma/generate-stylesheets.ts
user: login: ynotdraw
body: Still looking at this one. I'll report back 🫡 
created_at: 2025-03-03T19:53:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: > If the response is empty, then get-figma-variables.ts should probably throw instead of kicking the can over to get-figma-variables.ts.

Good call, resolved!

> The response's error field should also be checked by get-figma-variables.ts unless error is always false when response.ok is truthy.

It looks like, at least for the types, `error` is always `false`.  There isn't a ton of information on [Figma's docs](https://www.figma.com/developers/api?fuid=1111467023992153920#variables) around `error` either.  I'll add something in just to be safe.
created_at: 2025-03-03T19:57:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
+  });
+
+  await generateStylesheets({
path: src/figma/run.ts
user: login: clintcs
body: No strong preference, again. But breaking them up makes sense to me. What's your concern with another loop?
created_at: 2025-03-03T20:13:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
+import { mkdir, writeFile } from 'node:fs/promises';
path: src/figma/write-tokens.ts
user: login: clintcs
body: Very nice!
created_at: 2025-03-03T20:13:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
path: src/figma/generate-design-tokens.ts
user: login: clintcs
body: Sounds good!
created_at: 2025-03-03T20:16:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
+  });
+
+  await generateStylesheets({
path: src/figma/run.ts
user: login: ynotdraw
body: Nothing really, other than just wondering if it's an implementation detail that other people won't necessarily care about.  Your proposal does mean it's more consistent though, which I like. I'm just reasoning about things (publicly).  So we'd have:

1. `fetchFigmaVariables()`
2. `getDesignTokens()`
3. `writeDesignTokens()`
4. `getCSSVariables()` (new)
5. `writeStylesheets()` (new)

Seem good? If so, I'll start on this.
created_at: 2025-03-03T20:25:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
+  });
+
+  await generateStylesheets({
path: src/figma/run.ts
user: login: clintcs
body: I dig it. I think it'll add some clarity to `run.ts`.
created_at: 2025-03-03T20:47:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
+  });
+
+  await generateStylesheets({
path: src/figma/run.ts
user: login: ynotdraw
body: Yup, agreed. About to push up some changes to address everything else. Will just have this and prettier to work out.  Thanks!
created_at: 2025-03-03T20:48:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
path: src/figma/generate-stylesheets.ts
user: login: ynotdraw
body: I ended up throwing everything in a single line and run it through prettier. It cut down on some of the content.

Does this read better than before or still prefer multilining it?

https://github.com/CrowdStrike/glide-core/commit/657d0bc916c6fc6c6deef7bacdd5c0ce3b4fad75
created_at: 2025-03-03T22:24:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: I think it would read better for most people, including me, formatted as CSS is normally formatted. 

I get that it's all quite a bit of largely imperative code. But I don't think compressing it to a single line, `\n` or not, helps the situation. It just kind of is what it is.

No doubt the first snippet reads better than the second:

```ts
cssContent += `
  :root,
  :host,
  .theme-light {
    color-scheme: light;
    ${cssVariables.join('\n  ')
  }
}`;
```

```ts
cssContent += `:root, :host, .theme-light {color-scheme: light; ${cssVariables.join('\n  ')}}`;
```
created_at: 2025-03-03T22:51:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
path: src/figma/generate-stylesheets.ts
user: login: clintcs
body: That said, it's yours to merge. I think it's in good shape.
created_at: 2025-03-03T22:54:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
path: src/figma/generate-stylesheets.ts
user: login: ynotdraw
body: Nah, I'll get this updated. I can see the benefits. I'd prefer everyone be on board with the changes as I've got some time still before moving on to the next thing. 

Have a few more changes, should have a PR tonight. I'll comment at the root when it's all ready for re-review.
created_at: 2025-03-03T23:02:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // Dashes are used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on the dash here to get what we're after.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+
+        if (theme === 'dark') {
+          generatedContent += `\n:host,\n.theme-dark {\n  color-scheme: dark;\n  ${cssVariables.join('\n  ')}\n}\n`;
+        }
+      } else {
+        // All non-color collections simply target `:root`.
+        generatedContent += `\n:root {\n  ${cssVariables.join('\n  ')}\n}\n`;
+      }
+
+      const cssPath = path.join(
+        process.cwd(),
+        `./${outputDirectory}/${fileName}.css`,
+      );
+
+      const directory = path.dirname(cssPath);
+
+      await mkdir(directory, { recursive: true });
+
+      await writeFile(cssPath, generatedContent, 'utf8');
+    }
+
+    spinner.success(
+      `Stylesheets have been written to the "${outputDirectory}" directory.`,
+    );
+  } catch (error) {
+    spinner.error('An error occurred generating the stylesheets.');
+    throw error;
+  }
+};
+
+/**
+ * Loops over every design token, looks at the `$type`, and determines
+ * how to create a CSS property and value from it.
+ */
+function generateCSSVariablesFromTokens({
+  prefix,
+  tokens,
+}: {
+  prefix: string;
+  tokens: TokenGroup;
+}): string[] {
+  const cssVariables: string[] = [];
+
+  for (const [key, value] of Object.entries(tokens)) {
+    if (isToken(value)) {
+      // Skip tokens with empty or undefined scopes.
+      // If the scope is empty, it's an extended style.
+      // Extended styles are local to a component and not
+      // meant for global use.
+      //
+      // These are similar in concept to our "private"-prefixed
+      // CSS variables in code, but on the Figma side.
+      if (!value.$extensions?.['com.figma']?.scopes?.length) {
+        continue;
+      }
+
+      let cssValue: string;
+
+      switch (value.$type) {
+        case 'color':
+        case 'string': {
+          cssValue = value.$value;
+          break;
+        }
+        case 'dimension': {
+          const dimension = value.$value;
+          cssValue = `${dimension.value}${dimension.unit}`;
+          break;
+        }
+        case 'number':
+        case 'fontWeight': {
+          cssValue = value.$value.toString();
+          break;
+        }
+        case 'fontFamily': {
+          cssValue = `"${value.$value}"`;
+          break;
+        }
+
+        default: {
+          throw new Error(`Unknown $type for "${JSON.stringify(value)}".`);
path: src/figma/generate-stylesheets.ts
user: login: ynotdraw
body: ✅  https://github.com/CrowdStrike/glide-core/pull/732/commits/06043f50a84970799cb98de8c64cf5d426e76a83
created_at: 2025-03-03T23:56:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
path: src/figma/get-figma-variables.ts
user: login: ynotdraw
body: ✅  https://github.com/CrowdStrike/glide-core/pull/732/commits/b5c9b5a4a6d0e5998815cbbe7f7bf2019b7193db
created_at: 2025-03-03T23:57:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
path: src/figma/run.ts
user: login: ynotdraw
body: ✅  Tidied things up throughout other commits. Let me know if anything in particular sticks out.
created_at: 2025-03-03T23:58:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
path: src/figma/run.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/2918b2293e613833502c5147ce20c8a9fb19489c
✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/dcb643bdf153420df7ef1c2ee8d6acc4d16e1710
created_at: 2025-03-03T23:59:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
path: src/figma/run.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/78e93ea33a108bd46c25de72cb72c606eac4d9a8
created_at: 2025-03-04T00:02:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
path: src/figma/run.ts
user: login: ynotdraw
body: ✅ chatted this in the link above. Should be good here.
created_at: 2025-03-04T00:03:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
path: src/figma/run.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/956f1b6bb24e28562b9685b080a8882fcd0883d5
created_at: 2025-03-04T00:03:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
path: src/figma/run.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/93b13b104fb5eaf8daa5c6935d63771074543c7b
created_at: 2025-03-04T00:04:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
path: src/figma/run.ts
user: login: ynotdraw
body: Okay, if no strong preference let's leave this here for now and adapt if needed later on.
created_at: 2025-03-04T00:04:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
path: src/figma/run.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/956f1b6bb24e28562b9685b080a8882fcd0883d5
created_at: 2025-03-04T00:05:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
+    );
+  }
+
+  const spinner = yoctoSpinner({ text: 'Fetching Figma variables…' }).start();
+  let tokensJson: GetLocalVariablesResponse;
path: src/figma/get-figma-variables.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/902d644cbc7deccfa43f76af8c967f1b7bb95274
created_at: 2025-03-04T00:05:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
+import { mkdir, writeFile } from 'node:fs/promises';
path: src/figma/write-tokens.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/614952b15b89140ff806c967f1ff9a6411933027
created_at: 2025-03-04T00:06:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+import { existsSync } from 'node:fs';
path: src/figma/write-tokens.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/614952b15b89140ff806c967f1ff9a6411933027

also discussed at https://github.com/CrowdStrike/glide-core/pull/732#discussion_r1977836193
created_at: 2025-03-04T00:06:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: - ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/6245c5c8d4215708f83932a2614bc70851b6f3b3
- ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/ff79a4023141bb4e10421a2ae67346c08adf47bd
- ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/4096d1455dadc40a35350f8ffa0de9ca6721c99b
created_at: 2025-03-04T00:06:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type Token, type TokenGroup, type TokensFile } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` from Figma's API to
+ * generate [design tokens](https://tr.designtokens.org/format/#design-token)
+ * grouped by the Figma collection.
+ */
+export default (tokensJson: GetLocalVariablesResponse) => {
+  if (!tokensJson) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(tokensJson);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
+
+function isRGBA(object: unknown): object is RGBA {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'r' in object &&
+    'g' in object &&
+    'b' in object &&
+    'a' in object &&
+    typeof (object as RGBA).r === 'number' &&
+    typeof (object as RGBA).g === 'number' &&
+    typeof (object as RGBA).b === 'number' &&
+    typeof (object as RGBA).a === 'number'
+  );
+}
+
+function isVariableAlias(object: unknown): object is VariableAlias {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'type' in object &&
+    (object as VariableAlias).type === 'VARIABLE_ALIAS' &&
+    'id' in object &&
+    typeof (object as VariableAlias).id === 'string'
+  );
+}
+
+function getTokenValueFromVariable({
+  $type,
+  modeId,
+  scopes,
+  variable,
+  variables,
+}: {
+  $type: Token['$type'];
+  modeId: string;
+  scopes: VariableScope[];
+  variable: LocalVariable;
+  variables: Record<string, LocalVariable>;
+}): string | number | { value: number; unit: 'rem' } {
+  const value = variable.valuesByMode[modeId];
+
+  // When a variable is an object, it's either a color
+  // or a `VariableAlias`.
+  if (typeof value === 'object') {
+    // A `VariableAlias` is a variable that references
+    // another variable.
+    if (isVariableAlias(value)) {
+      const aliasedVariable = variables[value.id];
+
+      if (!aliasedVariable) {
+        throw new Error(`Aliased variable not found for "${value.id}".`);
+      }
+
+      const [maybeRgba] = Object.values(aliasedVariable.valuesByMode);
+
+      // In some cases, a variable is only aliased one level deep
+      // and we can extract the value immediately.
+      // In other cases, a variable is aliased again (and again!),
+      // which means we recursively need to call this function
+      // until we get to a non-aliased variable value that we can parse.
+      return isRGBA(maybeRgba)
+        ? rgbToHex(maybeRgba)
+        : getTokenValueFromVariable({
+            $type,
+            modeId,
+            scopes,
+            variable: aliasedVariable,
+            variables,
+          });
+    }
+
+    if (isRGBA(value)) {
+      return rgbToHex(value);
+    }
+
+    throw new Error(
+      `The variable "${JSON.stringify(value)}" is an object, but is unable to be parsed to a hex value or variable alias.`,
+    );
+  }
+
+  if (typeof value === 'number') {
+    // https://tr.designtokens.org/format/#dimension
+    //
+    // Figma's API returns numbers as pixels, but we prefer using
+    // rem instead. Assuming a `dimension` should use a rem unit
+    // may not be true in the future, so we may need to update accordingly
+    // as new use cases come in.
+    if ($type === 'dimension') {
+      return { value: value / 16, unit: 'rem' };
+    }
+
+    return value;
+  }
+
+  return String(value);
+}
+
+function buildTokensFromVariables(
+  localVariablesResponse: GetLocalVariablesResponse,
+) {
+  const tokens: Record<string, TokensFile> = {};
+  const deletedButReferencedVariableNames: string[] = [];
+
+  const { variables, variableCollections } = localVariablesResponse.meta;
+
+  for (const variable of Object.values(variables)) {
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // A `remote` variable is a type of variable that is defined and managed in a separate file
+    // and can be used across multiple files or projects.
+    // We should not use remote variables in our Design System as we instead rely on
+    // local variables from the provided Figma file.
+    if (variable.remote) {
+      continue;
+    }
+
+    // https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    //
+    // `deletedButReferenced` indicates that the variable was deleted in the editor,
+    // but the document may still contain references to the variable.
+    // References to the variable may exist through bound values or variable aliases.
+    //
+    // We should not generate a token from these variables, but we can notify the
+    // design team of their existence, in hopes they can be properly removed from
+    // the Figma side of things.
+    if (variable.deletedButReferenced) {
+      deletedButReferencedVariableNames.push(variable.name);
+      continue;
+    }
+
+    const collection = variableCollections[variable.variableCollectionId];
+
+    if (!collection) {
+      throw new Error(
+        `Could not find a collection with the id of "${variable.variableCollectionId}".`,
+      );
+    }
+
+    for (const mode of collection.modes) {
+      // "mode" is used to differentiate between "light" and "dark" mode variables;
+      // however, Figma also adds a default mode when one is not specified.
+      // Typically this is labeled as "Mode 1". We could rely on "Mode 1", or we
+      // could instead be explicit with our known, custom modes - "light" and "dark".
+      //
+      // If the time comes where we have additional modes to support,
+      // we'll need to modify this logic.
+      const modeName =
+        mode.name.toLowerCase() === 'light' ||
+        mode.name.toLowerCase() === 'dark'
+          ? mode.name.toLowerCase()
+          : null;
+
+      // Token file names use the `.tokens.json` extension to follow the
+      // file extension (https://tr.designtokens.org/format/#file-extensions)
+      // guidance provided by the draft specification.
+      //
+      // Tokens are grouped based on the Figma collection they are a part of.
+      // Some collections also have a mode specified, as outlined above.
+      // Others do not, which we refer to as "modeless collections".
+      // These are used for variables that don't need different values based on
+      // the theme, for example. They contain a single set of values for each variable.
+      //
+      // The naming convention we use is `{collection}.tokens.json` for modeless collections.
+      // Collections with modes should use kebab casing, e.g., `{collection}-{mode}.tokens.json`.
+      //
+      // Adjusting this naming convention will have consequences in other parts of the
+      // export process, so take care in making changes here.
+      //
+      // Additionally, we use the collection and optional mode as the file name
+      // as it aligns closely with what users see in Figma's UI. This helps with
+      // troubleshooting.
+      const tokenFileName = modeName
+        ? `${collection.name.toLowerCase()}-${modeName}.tokens.json`
+        : `${collection.name.toLowerCase()}.tokens.json`;
+
+      if (!tokens[tokenFileName]) {
+        tokens[tokenFileName] = {};
+      }
+
+      let tokenGroup: TokenGroup = tokens[tokenFileName];
+
+      // In Figma, variables can be organized by adding them to groups within a collection.
+      // Here's how that looks in Figma's UI:
+      //
+      // color-collection/
+      // ├─ parent-group/
+      // │  ├─ child-group-1/
+      // │  │  ├─ variable-1
+      // │  │  ├─ variable-2
+      // │  ├─ child-group-2/
+      // │  │  ├─ variable-3
+      //
+      // In our API response, a Figma variable's name includes the group(s) it is part of, separated
+      // by a `/`. `parent-group/child-group-1/variable-1` would be the full name for `variable-1`
+      // in the example above.
+      //
+      // To make it easier to do a diff between Figma's UI and our generated
+      // token JSON files, we put them in a similar format, where each group is a separate
+      // key.
+      //
+      // {
+      //   "parent-group": {
+      //     "child-group": {
+      //       "variable-1": #000000;
+      //     }
+      //   }
+      // }
+      //
+      // To do this, we break the full name into parts separated by `/`.
+      const parts = variable.name.split('/');
+
+      for (let index = 0; index < parts.length - 1; index++) {
path: src/figma/generate-design-tokens.ts
user: login: ynotdraw
body: Thanks for the catch - turns out the config needed a small change to run properly.

✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/4d2b808ec8f59c5feaec84c2f127722818669da2
created_at: 2025-03-04T00:11:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,200 @@
+import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import yoctoSpinner from 'yocto-spinner';
+import { type TokenGroup } from './types.js';
+import isToken from './is-token.js';
+
+/**
+ * Reads the `.tokens.json` files generated from the previous step
+ * to generate CSS stylesheets, grouped by collection and mode,
+ * from all of the design tokens.
+ *
+ * Each variable follows this naming convention to align with design:
+ *
+ * --glide-core-{collection}-{category*}-{scope*}-{property}-{variant*}--{state*}
+ *   * = optional
+ *
+ */
+export default async ({
+  outputDirectory,
+  tokensDirectory,
+}: {
+  outputDirectory: string;
+  tokensDirectory: string;
+}) => {
+  const spinner = yoctoSpinner({
+    text: 'Converting tokens to stylesheets…\n',
+  }).start();
+
+  try {
+    const files = await readdir(tokensDirectory);
+
+    for (const file of files) {
+      // We follow the naming convention outlined by the
+      // W3C draft by applying `.tokens.json` as the file extension.
+      // At this stage it is safe to drop the extension completely
+      // as we only care about the file name from this point
+      // on.
+      const fileName = file.replace('.tokens.json', '');
+
+      // Variable names should include the Figma collection they
+      // are a part of.
+      //
+      // Because our temporary files are stored by collection,
+      // we can use the file name to extract the collection and include
+      // it in our variable name to match the naming convention
+      // outlined at the top of this file.
+      //
+      // A dash is used to separate a collection from its mode
+      // (e.g., {collection}-{mode}) from the previous step,
+      // so we can safely split on it here to get the collection name.
+      const tokenCollectionName = file.includes('-')
+        ? fileName.split('-').at(0)
+        : fileName;
+
+      if (!tokenCollectionName) {
+        throw new Error(
+          `Could not determine the collection from the file "${file}"`,
+        );
+      }
+
+      const fileContent = await readFile(
+        path.join(tokensDirectory, file),
+        'utf8',
+      );
+
+      const tokens = JSON.parse(fileContent.toString()) as TokenGroup;
+
+      const cssVariables = generateCSSVariablesFromTokens({
+        prefix: `--glide-core-${tokenCollectionName}`,
+        tokens,
+      });
+
+      let generatedContent = '/* Generated file. Do not edit directly. */';
+
+      // We need to handle the CSS generation for our color collections
+      // quite a bit differently from the other stylesheets. Light and dark
+      // stylesheets use the same variable names, but different variable
+      // values. They also set `color-scheme` and use different selectors
+      // between light and dark modes.
+      if (file.startsWith('color-')) {
+        const theme = fileName.replace('color-', '');
+
+        if (theme === 'light') {
+          generatedContent += `\n:root,\n:host,\n.theme-light {\n  color-scheme: light;\n  ${cssVariables.join('\n  ')}\n}\n`;
path: src/figma/generate-stylesheets.ts
user: login: ynotdraw
body: ✅ https://github.com/CrowdStrike/glide-core/pull/732/commits/6c99314dfb4314cc8807b48916fad1c377f2eb51
created_at: 2025-03-04T00:12:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
path: src/figma/get-figma-variables.ts
user: login: ynotdraw
body: Discussed this over on https://github.com/CrowdStrike/glide-core/pull/732#discussion_r1977914989 
created_at: 2025-03-04T00:14:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+import generateStylesheets from './generate-stylesheets.js';
+import getFigmaVariables from './get-figma-variables.js';
+import generateDesignTokens from './generate-design-tokens.js';
+import writeTokens from './write-tokens.js';
+
+const figmaToken = process.env.FIGMA_TOKEN ?? '';
+const figmaFileId = 'A4B1kaT5HVLqcijwK4GXzt';
+const tokensDirectory = 'tokens';
+const stylesheetsDirectory = 'stylesheets';
+
+/**
+ * Queries Figma's API for variables, converts them to a
+ * Design Token format, and generates CSS custom property
+ * stylesheets from the tokens.
+ *
+ * This process has many layers and requires a general understanding
+ * of [Figma variables, collections, and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes).
+ * A general understanding of the W3C [Design Token format](https://tr.designtokens.org/format)
+ * draft is also recommended.
+ */
+async function run() {
+  const variables = await getFigmaVariables({
+    token: figmaToken,
+    fileId: figmaFileId,
+  });
+
+  const tokensGroupedByCollection = generateDesignTokens(variables);
+
+  await writeTokens({
+    tokensGroupedByCollection,
+    outputDirectory: tokensDirectory,
+  });
+
+  await generateStylesheets({
path: src/figma/run.ts
user: login: ynotdraw
body: Alright, think I got it all straightened out at https://github.com/CrowdStrike/glide-core/pull/732/commits/cfce641a07648d67e201f22231724e9cccd30877.  Let me know!
created_at: 2025-03-04T01:27:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { type GetLocalVariablesResponse } from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+
+/**
+ * Fetches the variables from Figma's API and returns them as JSON.
+ *
+ * https://www.figma.com/developers/api?fuid=1111467023992153920#variables
+ */
+export default async ({ token, fileId }: { token: string; fileId: string }) => {
+  if (!token) {
+    throw new Error(
+      '"FIGMA_TOKEN" is a required environment variable. View CONTRIBUTING.md for more information on configuring this token.',
+    );
+  }
+
+  const spinner = yoctoSpinner({ text: 'Fetching Figma variables…' }).start();
+  let tokensJson: GetLocalVariablesResponse;
+
+  try {
+    const response = await fetch(
+      `https://api.figma.com/v1/files/${fileId}/variables/local?scopes=file_variables:read&branch_data=true`,
+      {
+        method: 'GET',
+        headers: {
+          'X-FIGMA-TOKEN': token,
+        },
+      },
+    );
+
+    if (!response.ok) {
+      throw new Error(
+        `The Figma API failed with a ${response.status} error code.`,
+      );
+    }
+
+    tokensJson = (await response.json()) as GetLocalVariablesResponse;
+
+    spinner.success('Figma variables collected.');
+
+    return tokensJson;
path: src/figma/get-figma-variables.ts
user: login: ynotdraw
body: Related https://github.com/CrowdStrike/glide-core/pull/732#discussion_r1978081301.

I wouldn't wrap so much in a `try/catch` if it weren't for the spinner state.  Without explicitly calling `spinner.error()`, it can get stuck in a state like this.  By catching potentially known errors, as well as throwing our own, it makes handling this state easier with a wider `try/catch` net.

<img width="196" alt="Screenshot 2025-03-03 at 9 04 35 PM" src="https://github.com/user-attachments/assets/391be53a-bdf6-44a9-9d6d-904fde7c5a0c" />

You still get a nice stack trace of where things went wrong.

<img width="635" alt="Screenshot 2025-03-03 at 9 08 43 PM" src="https://github.com/user-attachments/assets/3bef831f-cabb-46e3-b1fb-a615c7ab760b" />

I tried to wrap things in a `try` where it made sense, but most of the time it was at the start of a larger operation as a bit of a catch-all. I could keep breaking things up, but at this point I think we could be over-optimizing/analyzing things. I do appreciate the nits though! I just don't know that this one is worth it.
created_at: 2025-03-04T02:12:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,118 @@
+import {
+  type VariableCodeSyntax,
+  type VariableScope,
+} from '@figma/rest-api-spec';
+
+// A Token is an object in a standardized, machine-readable format.
+//
+// We follow the Design Tokens Format Module¹ W3C draft. It provides
+// a technical specification for a file format to exchange Design Tokens
+// between different tools. While we don't leverage additional tooling
+// at the moment, this allows us to potentially leverage one in the future,
+// while also putting our tokens in a format that is easily readable by humans
+// and parseable by code.
+//
+// Tokens are distinct from Figma variables, in that a token² is a name/value
+// pair (with other properties), while a variable in Figma stores multiple
+// values, one for each mode.
+//
+// The `$type` property of a Token follows the format specified in the types
+// documentation³.
+//
+// We support a subset of the types listed in the document. `duration` and
+// `cubic-bezier` are not currently applicable to our variables.
+//
+// Adding support for these types would lead to additional code for
+// processing them. It's better to be explicit in the types we support and
+// keep an eye on the W3C document for updates. If a Figma variable is
+// created using a currently unsupported type, we can add it then.
+//
+// Additionally, we diverge slightly from the draft W3C types by allowing
+// for a `string` type. This is to better align with the resolved types
+// for Figma variables.
+//
+// `font-style` CSS properties, for example, should use a `string` type until
+// the specification provides additional guidance on these properties.
+// At the moment, these types are being considered, but no
+// guidance currently exists⁴.
+//
+// 1: https://tr.designtokens.org/format/
+// 2: https://tr.designtokens.org/format/#design-token
+// 3: https://tr.designtokens.org/format/#types
+// 4: https://tr.designtokens.org/format/#additional-types
+
+interface BaseToken {
+  $description?: string;
+  $extensions?: {
+    // Used to store Figma-specific variable properties¹.
+    //
+    // 1: https://tr.designtokens.org/format/#extensions-0
path: src/figma/types.ts
user: login: clintcs
body: Feel free to ignore. But no need for a superscript in this case. The URL can only refer to one thing.
created_at: 2025-03-04T12:56:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,414 @@
+import {
+  type GetLocalVariablesResponse,
+  type VariableScope,
+  type LocalVariable,
+  type VariableAlias,
+  type RGB,
+  type RGBA,
+} from '@figma/rest-api-spec';
+import yoctoSpinner from 'yocto-spinner';
+import { type DesignToken, type TokenGroup, type TokensFile } from './types.js';
+import isDesignToken from './is-design-token.js';
+
+/**
+ * Uses the `GetLocalVariablesResponse` meta from Figma's API to
+ * generate Design Tokens¹ grouped by the Figma collection.
+ *
+ * 1: https://tr.designtokens.org/format/#design-token
+ */
+export default (meta: GetLocalVariablesResponse['meta']) => {
+  if (!meta) {
+    throw new Error('The JSON response from Figma was empty.');
+  }
+
+  const spinner = yoctoSpinner({ text: 'Parsing variables…\n' }).start();
+
+  try {
+    const { deletedButReferencedVariableNames, tokens } =
+      buildTokensFromVariables(meta);
+
+    if (deletedButReferencedVariableNames.length > 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `\nThe following variables were listed as "deletedButReferenced". Report these to the design team for cleanup.\n${deletedButReferencedVariableNames?.map((variable) => `\n- ${variable}`)?.join('')}\n\n`,
+      );
+    }
+
+    spinner.success('Tokens parsed.');
+
+    return tokens;
+  } catch (error) {
+    spinner.error(
+      'An error occurred generating tokens from the Figma variables.',
+    );
+
+    throw error;
+  }
+};
+
+function rgbaNumberToHex(value: number) {
+  const hex = Math.round(value * 255).toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+function rgbToHex(color: RGB | RGBA) {
+  const a = 'a' in color ? color.a : 1;
+
+  const hex = [
+    rgbaNumberToHex(color.r),
+    rgbaNumberToHex(color.g),
+    rgbaNumberToHex(color.b),
+  ].join('');
+
+  return `#${hex}${a === 1 ? '' : rgbaNumberToHex(a)}`;
+}
+
+function isRGBA(object: unknown): object is RGBA {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'r' in object &&
+    'g' in object &&
+    'b' in object &&
+    'a' in object &&
+    typeof (object as RGBA).r === 'number' &&
+    typeof (object as RGBA).g === 'number' &&
+    typeof (object as RGBA).b === 'number' &&
+    typeof (object as RGBA).a === 'number'
+  );
+}
+
+function isVariableAlias(object: unknown): object is VariableAlias {
+  return (
+    typeof object === 'object' &&
+    object !== null &&
+    'type' in object &&
+    (object as VariableAlias).type === 'VARIABLE_ALIAS' &&
+    'id' in object &&
+    typeof (object as VariableAlias).id === 'string'
+  );
+}
+
+function getTokenValueFromVariable({
+  $type,
+  modeId,
+  scopes,
+  variable,
+  variables,
+}: {
+  $type: DesignToken['$type'];
+  modeId: string;
+  scopes: VariableScope[];
+  variable: LocalVariable;
+  variables: Record<string, LocalVariable>;
+}): string | number | { value: number; unit: 'rem' } {
+  const value = variable.valuesByMode[modeId];
+
+  // When a variable is an object, it's either a color
+  // or a `VariableAlias`.
+  if (typeof value === 'object') {
+    // A `VariableAlias` is a variable that references
+    // another variable.
+    if (isVariableAlias(value)) {
+      const aliasedVariable = variables[value.id];
+
+      if (!aliasedVariable) {
+        throw new Error(`Aliased variable not found for "${value.id}".`);
+      }
+
+      const [maybeRgba] = Object.values(aliasedVariable.valuesByMode);
+
+      // In some cases, a variable is only aliased one level deep
+      // and we can extract the value immediately.
+      // In other cases, a variable is aliased again (and again!),
+      // which means we recursively need to call this function
+      // until we get to a non-aliased variable value that we can parse.
+      return isRGBA(maybeRgba)
+        ? rgbToHex(maybeRgba)
+        : getTokenValueFromVariable({
+            $type,
+            modeId,
+            scopes,
+            variable: aliasedVariable,
+            variables,
+          });
+    }
+
+    if (isRGBA(value)) {
+      return rgbToHex(value);
+    }
+
+    throw new Error(
+      `The variable "${JSON.stringify(value)}" is an object, but is unable to be parsed to a hex value or variable alias.`,
+    );
+  }
+
+  if (typeof value === 'number') {
+    // Figma's API returns numbers as pixels, but we prefer using
+    // rem instead. Assuming a dimension¹ should use a rem unit
+    // may not be true in the future, so we may need to update accordingly
+    // as new use cases come in.
+    //
+    // 1: https://tr.designtokens.org/format/#dimension
+    if ($type === 'dimension') {
+      return { value: value / 16, unit: 'rem' };
+    }
+
+    return value;
+  }
+
+  return String(value);
+}
+
+function buildTokensFromVariables({
+  variables,
+  variableCollections,
+}: {
+  variables: GetLocalVariablesResponse['meta']['variables'];
+  variableCollections: GetLocalVariablesResponse['meta']['variableCollections'];
+}) {
+  const tokens: Record<string, TokensFile> = {};
+  const deletedButReferencedVariableNames: string[] = [];
+
+  for (const variable of Object.values(variables)) {
+    // A remote variable¹ is a type of variable that is defined and managed in a separate file
+    // and can be used across multiple files or projects.
+    // We should not use remote variables in our Design System as we instead rely on
+    // local variables from the provided Figma file.
+    //
+    // 1: https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    if (variable.remote) {
+      continue;
+    }
+
+    // `deletedButReferenced¹` indicates that the variable was deleted in the editor,
+    // but the document may still contain references to the variable.
+    // References to the variable may exist through bound values or variable aliases.
+    //
+    // We should not generate a token from these variables, but we can notify the
+    // design team of their existence, in hopes they can be properly removed from
+    // Figma.
+    //
+    // 1: https://www.figma.com/developers/api?fuid=1111467023992153920#variables-types
+    if (variable.deletedButReferenced) {
+      deletedButReferencedVariableNames.push(variable.name);
+      continue;
+    }
+
+    const collection = variableCollections[variable.variableCollectionId];
+
+    if (!collection) {
+      throw new Error(
+        `Could not find a collection with the id of "${variable.variableCollectionId}".`,
+      );
+    }
+
+    for (const mode of collection.modes) {
+      // "mode" is used to differentiate between "light" and "dark" mode variables;
+      // however, Figma also adds a default mode when one is not specified.
+      // Typically this is labeled as "Mode 1". We could rely on "Mode 1", or we
+      // could instead be explicit with our known, custom modes - "light" and "dark".
+      //
+      // If the time comes where we have additional modes to support,
+      // we'll need to modify this logic.
+      const modeName =
+        mode.name.toLowerCase() === 'light' ||
+        mode.name.toLowerCase() === 'dark'
+          ? mode.name.toLowerCase()
+          : null;
+
+      // Token file names use the `.tokens.json` extension to follow the
+      // file extension¹ guidance provided by the draft specification.
+      //
+      // Tokens are grouped based on the Figma collection they are a part of.
+      // Some collections also have a mode specified, as outlined above.
+      // Others do not, which we refer to as "modeless collections".
+      // These are used for variables that don't need different values based on
+      // the theme, for example. They contain a single set of values for each variable.
+      //
+      // The naming convention we use is `{collection}.tokens.json` for modeless collections.
+      // Collections with modes should use kebab casing, e.g., `{collection}-{mode}.tokens.json`.
+      //
+      // Adjusting this naming convention will have consequences in other parts of the
+      // export process, so take care in making changes here.
+      //
+      // Additionally, we use the collection and optional mode as the file name
+      // as it aligns closely with what users see in Figma's UI. This helps with
+      // troubleshooting.
+      //
+      // 1: https://tr.designtokens.org/format/#file-extensions
+      const tokenFileName = modeName
+        ? `${collection.name.toLowerCase()}-${modeName}.tokens.json`
+        : `${collection.name.toLowerCase()}.tokens.json`;
+
+      if (!tokens[tokenFileName]) {
+        tokens[tokenFileName] = {};
+      }
+
+      let tokenGroup: TokenGroup = tokens[tokenFileName];
+
+      // In Figma, variables can be organized by adding them to groups within a collection.
+      // Here's how that looks in Figma's UI:
+      //
+      // color-collection/
+      // ├─ parent-group/
+      // │  ├─ child-group-1/
+      // │  │  ├─ variable-1
+      // │  │  ├─ variable-2
+      // │  ├─ child-group-2/
+      // │  │  ├─ variable-3
+      //
+      // In our API response, a Figma variable's name includes the group(s) it is part of,
+      // separated by a `/`. `parent-group/child-group-1/variable-1` would be the full name
+      // for `variable-1` in the example above.
+      //
+      // To make it easier to do a diff between Figma's UI and our generated
+      // token JSON files, we put them in a similar format, where each group is a separate
+      // key.
+      //
+      // {
+      //   "parent-group": {
+      //     "child-group": {
+      //       "variable-1": #000000;
+      //     }
+      //   }
+      // }
+      //
+      // To do this, we break the full name into parts separated by `/`.
+      const parts = variable.name.split('/');
+
+      for (const part of parts.values()) {
path: src/figma/get-design-tokens.ts
user: login: clintcs
body: Oops.

```suggestion
      for (const part of parts) {
```
created_at: 2025-03-04T13:19:07Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-02-27T16:55:24Z
body: ###  ⚠️  No Changeset found

Latest commit: 41ca016013c71ca21ba1e229cc1a36b5ba9bacec

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/CrowdStrike/glide-core/new/figma-variable-syncing?filename=.changeset/selfish-moons-provide.md&value=---%0A%22%40crowdstrike%2Fglide-core%22%3A%20patch%0A---%0A%0AAdd%20new%20Figma%20Variable%20syncing%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-27T17:05:01Z
body: ## Preview URL
https://glide-core.crowdstrike-ux.workers.dev/figma-variable-syncing
<!-- Sticky Pull Request Comment -->
author_association: CONTRIBUTOR
user: login: ynotdraw
created_at: 2025-03-04T02:13:14Z
body: Okay @clintcs @danwenzel , think I covered everything. Please re-review!
author_association: COLLABORATOR
user: login: clintcs
created_at: 2025-03-04T12:57:58Z
body: Real nice, @ynotdraw. Thanks for going through everything.
author_association: COLLABORATOR