<div id="app">
  <div id="generate">
    <!-- //Primary color -->
    <input type="color" class="" v-model="primary" name="primary" id="primary"></input>
    <label for="primary">Primary color</label>

    <!-- //Secondary color -->
    <input type="color" class="" v-model="secondary" name="secondary" id="secondary"></input>
    <label for="secondary">Secondary color</label>

    <!-- //Surface color -->
    <input type="color" class="" v-model="surface" name="surface" id="surface"></input>
    <label for="surface">Surface color</label>
    <br />
    <button @click="update">Generate palette</button>
    <button @click="clear">Clear</button>
  </div>
  <div v-if="colorList.length" class="palette">
    <div v-for="color of colorList" :style="{'background-color' : color.color, 'color' : color.text }">{{ color.name }}<br />{{ color.color }}<br />{{ color.contrastAA }} AA, {{ color.contrastAAA }} AAA,</div>
  </div>
  <h4 v-if="colorList.length">Copy a code, open <a href="https://www.figma.com/community/plugin/971807532885631684">this nice plugin</a>, import the code as JSON, ... , profit</h4>
  <div v-if="colorList.length" class="figma-code" onclick="selectText(this.id)" id="figma-code">
    <code>
      {{ code }}
    </code>
  </div>
  <p v-else>List is empty.</p>

</div>
#app {
  font-family: Helvetica, Arial, sans-serif;
  text-align: left;
  color: #2c3e50;
  margin-top: 60px;
  width: 640px;
}
#generate {
  margin-bottom: 2em;
}
.palette {
  display: grid;
  grid-template-columns: 160px 160px 160px 160px;
  grid-template-rows: 100px 100px;
  grid-auto-flow: row;
}
.palette div {
  padding: 1em;
  width: 160px;
  border: 1px solid #fff;
}
.figma-code {
  background-color: #eaeaea;
  width: 100%;
  padding: 1em;
}
function generatePalette(primary, secondary, surface) {
  let positive, negative, surfaceMid, surfaceHigh, strokes;
  let primaryLum = chroma(primary).get("oklch.l");

  // Positive and Negative colors
  if (primaryLum < 0.25) {
    positive = chroma(primary)
      .set("oklch.l", "0.25")
      .set("oklch.c", "0.25")
      .set("oklch.h", "145");
  } else if (primaryLum > 0.4) {
    positive = chroma(primary)
      .set("oklch.l", "0.40")
      .set("oklch.c", "0.25")
      .set("oklch.h", "145");
  } else {
    positive = chroma(primary).set("oklch.h", "145");
  }
  if (chroma(positive).get("oklch.c") < 0.14) {
    positive = chroma(positive).set("oklch.c", "0.14");
  }
  negative = chroma(positive)
    .set("oklch.h", "26")
    .set("oklch.c", "0.225")
    .set("oklch.l", chroma(positive).get("oklch.l"))
    .hex();

  // Secondary
  secondary = secondary
    ? secondary
    : chroma(primary).set("oklch.h", "+45").set("oklch.l", primaryLum);

  // Surface and strokes
  let surfaceLow = surface
    ? chroma(surface).luminance(0.92)
    : chroma(primary).luminance(0.92);

  surfaceMid = chroma(surfaceLow).luminance(0.95).hex();
  surfaceHigh = chroma(surfaceLow).luminance(0.99).hex();
  strokes = chroma(surfaceLow).set("oklch.l", "0.87").hex();

  let colorCodes = [
    primary,
    secondary,
    positive,
    negative,
    surfaceLow,
    surfaceMid,
    surfaceHigh,
    strokes
  ];

  let colorNames = [
    "primary",
    "secondary",
    "positive",
    "negative",
    "surfaceLow",
    "surfaceMid",
    "surfaceHigh",
    "strokes"
  ];

  // Create color Objects with paired text styles
  let colorsArray = [];
  for (const i in colorCodes) {
    const textColor =
      chroma(colorCodes[i]).luminance() < 0.19 ? "#fafafa" : "#0a0a0a";

    let contrast = chroma.contrast(colorCodes[i], textColor);
    let contrastAA = contrast < 4.5 ? "❌" : "✅";
    let contrastAAA = contrast < 7 ? "❌" : "✅";

    let color = {
      name: colorNames[i],
      color: chroma(colorCodes[i]).hex(),
      text: textColor,
      contrastAA,
      contrastAAA
    };
    colorsArray.push(color);
  }

  return colorsArray;
}

function createCode(colors, noTextColors) {
  let code = [];
  for (const i in colors) {
    let basicColor = { name: colors[i].name, color: colors[i].color };
    code.push(basicColor);
    if (!noTextColors.some((el) => el === colors[i].name)) {
      let textColor = { name: colors[i].name + "-text", color: colors[i].text };
      code.push(textColor);
    }
  }
  return code;
}

const app = Vue.createApp({
  data() {
    return {
      primary: "#002E7A",
      secondary: "",
      surface: "",
      colorList: [],
      code: [],
      noTextColors: ["surfaceMid", "surfaceHigh", "strokes"]
    };
  },
  methods: {
    update() {
      this.colorList = generatePalette(
        this.primary,
        this.secondary,
        this.surface
      );
      this.code = createCode(this.colorList, this.noTextColors);
    },
    clear() {
      this.colorList = [];
      this.primary = "#AA00FF";
      this.secondary = "";
      this.surface = "";
    }
  }
});

app.mount("#app");

function selectText(id) {
  var sel, range;
  var el = document.getElementById(id); //get element id
  if (window.getSelection && document.createRange) {
    //Browser compatibility
    sel = window.getSelection();
    if (sel.toString() == "") {
      //no text selection
      window.setTimeout(function () {
        range = document.createRange(); //range object
        range.selectNodeContents(el); //sets Range
        sel.removeAllRanges(); //remove all ranges from selection
        sel.addRange(range); //add Range to a Selection.
      }, 1);
    }
  } else if (document.selection) {
    //older ie
    sel = document.selection.createRange();
    if (sel.text == "") {
      //no text selection
      range = document.body.createTextRange(); //Creates TextRange object
      range.moveToElementText(el); //sets Range
      range.select(); //make selection.
    }
  }
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.37/vue.global.prod.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.4.2/chroma.min.js