Use monotone SVG like UnoCSS-[presetIcon] do. [英文文章]

76 阅读4分钟

This article was wrriten by zzx58@juejin, 2023/12/15.

Prelude

**Important: ** If you needed using UnoCSS dynamically somewhere and want it to be as elegant as possible, don't forget to create a safeList. You can manage them centrally.

There is no need to reiterate some necessary concepts that already written down in official tutorials.

And*** * -> It will be more like a  util tool oriented note. 

So*** * -> Check the FYI section at the end of this article.

Final*** * -> Uh....Think that....It's will be a hard work and unnecessary to some extent, to move those needed or useful infos here. Check those FYI resources will be more recommanded. I will do my best within my personal acceptance range.

Notice*** * -> Ways handling SVGs in this article only support monotone icons*** *. Check FYI Notice and Antfu label. Ways in Antfu label, through checking currentColor wont work in my case. But might be useful for you, for hardcoded pallete

Article Background.

This feature have been almost three months since i obtain. UnoCSS have been around for a long time. I have seen plenty ways in using SVGs in our project through the juejin community. But it seems like i haven't seen any articles like the way in article do. So i decide to write this article down. I think it's elegant to use SVGs like the way UnoCSS-presetIcon do. It has some benefits in project DX (develop experience) and business logic handling.

Needed Imports

The below imports include the needed stuff in two processing ways that solving SVGs to IconifyJSON or CustomLoader.


// {projectRootPath}/uno.config.ts

import { stringToColor, compareColors } from "@iconify/utils/lib/colors";

import {

  importDirectory,

  cleanupSVG,

  parseColors,

  runSVGO,

  deOptimisePaths,

} from "@iconify/tools";

import type { IconSet } from "@iconify/tools";

import type { IconifyJSON } from "@iconify/types";

import type { CustomIconLoader } from "@iconify/utils/lib/loader/types";

SVG - Cleanup & ParseColor


// {projectRootPath}/uno.config.ts

const cleanup_parseColor_SVG = async (iconSet: IconSet) => {

  await iconSet.forEach(async (name) => {

    // Notice the '!' exclamation mark.

    const svg = iconSet.toSVG(name)!;

    // console.log("Source SVG:", svg.toString());

    /* Clean up the svg. */

    cleanupSVG(svg);

    // console.log("Cleaned up SVG:", svg.toString());

    // Notice the '!' exclamation mark.

    const blackColor = stringToColor("black")!;

    // Check `parseColors` function hint for more info.

    await parseColors(svg, {

      defaultColor: "currentColor",

      // color parameter in tutorial is parsedColor.

      // callback: (attr, colorString, color) => {

      callback: (attr, colorString, parsedColor, tagName, item) => {

        if (parsedColor && compareColors(parsedColor, blackColor)) return "currentColor";

        // Below logic reason not clear.

        switch (parsedColor?.type) {

          case "none":

          case "current":

            return parsedColor;

        }

        // Below error will be throw, in my case: my source SVGs contains color value `#fff`, seems it's not allowed. Don't know why at present.

        // throw new Error(`Unexpected color "${colorString}" in attribute ${attr}`);

        // So i manually handle this problem by returning "currentColor".

        // console.log(attr, colorString, parsedColor, tagName, item);

        return "currentColor";

      },

    });

  });

  // console.log("ColorParsed SVG:", svg.toString());

  return svg;

};

Processing Way One.

This way is which i'm using now.


// {projectRootPath}/uno.config.ts

async function loadCustomIconSet_IconifyJSON(

  svgsFolderPath: string

): Promise<IconifyJSON> {

  const iconSet = await importDirectory(svgsFolderPath, {

    // What is this for?

    prefix: "svg",

  });

  // console.log(iconSet.list().length);

  // `await` before iconSet.forEach is needed!! Think about it, you have to wait for it to finish color parsing.

  // And it's also where you can change the SVG style parsing logic, could try by your self.

  await iconSet.forEach(async (name) => {

    const svg = await cleanup_parseColor_SVG(iconSet, name);

    // console.log('After Cleaning up and Before Optimise:', svg.toString());

    /* Optimise */

    runSVGO(svg);

    // console.log('After Cleaning up, parsedColor, and Before Optimise:', svg.toString());

    /* Update paths for compatibility with old software. */

    await deOptimisePaths(svg);

    // console.log('After Update paths:', svg.toString());

    iconSet.fromSVG(name, svg);

    // console.log('iconSet:',iconSet);

  });

  /* Export as IconifyJSON */

  return iconSet.export();

}

Processing way Two.

This method is from official tutorial, but it didn't work, don't know reason at present.

Check below link for detail.

github.com/iconify/too…


// {projectRootPath}/uno.config.ts

function loadCustomIconSet_CustomIconLoader(

  svgsFolderPath: string

): CustomIconLoader {

  const promise = new Promise<IconSet>(async (resolve, reject) => {

    const iconSet = await importDirectory(svgsFolderPath, {

      // What is this for?

      prefix: "svg",

    });

    // console.log(iconSet.list().length);

    // `await` before iconSet.forEach is needed!! Think about it, you have to wait for it to finish color parsing.

    // And it's also where you can change the SVG style parsing logic, could try by your self.

    await iconSet

      .forEach(async (name) => {

        const svg = await cleanup_parseColor_SVG(iconSet, name);

        // console.log('After Cleaning up, parsedColor, and Before Optimise:', svg.toString());

        runSVGO(svg);

        // console.log('After Optimise and Before Update paths:', svg.toString());

        await deOptimisePaths(svg);

        // console.log('After Update paths:', svg.toString());

        iconSet.fromSVG(name, svg);

        // console.log('iconSet:',iconSet);

      })

      .then(() => {

        resolve(iconSet);

      })

      .catch((err) => {

        reject(err);

      });

  });

  return async (name: string) => {

    const iconSet = await promise;

    return iconSet.toSVG(name)?.toMinifiedString();

  };

}

// You might want this to check CustomIconLoader function logic.

const test = async () => {

  const result = loadCustomIconSet_CustomIconLoader("./assets/svgs/overview");

  // console.log(await result("d-collapse"));

  console.log(await result("new-teams"));

};

test();

Using in UnoCSS config file.


// {projectRootPath}/uno.config.ts

import {

  defineConfig,

  presetIcons,

} from "unocss";

export default defineConfig({

  // 但 `presets` 被指定时,默认的预设将会被禁用,

  // 因此你可以在你原有的 App 上使用纯 CSS 图标而不需要担心 CSS 冲突的问题。

  presets: [

    // xxx...

    presetIcons({

      autoInstall: false,

      collections: {

        // Notice here. Arrow func used. Understanding `Awaitable<IconifyJSON>` through collections type definition.

        'custom-svg': () => loadCustomIconSet_IconifyJSON('./assets/svgs/overview')

        // Below method don't work.

        // "custom-svg": loadCustomIconSet_CustomIconLoader(

        //   "./assets/svgs/overview"

        // ),

      },

    }),

  ]

});

UnoCSS safeList

**Important: ** If you needed using UnoCSS dynamically somewhere and want it to be as elegant as possible, don't forget to create a safeList. You can manage them centrally.


safelist: [...needed_unocss_constants],

FYI | References

Leran some English.

  • FYI: For you informations.ne

  • DX: Develop experience.

  • ...have been around for a long time...

  • reiterate

  • at present

  • Prelude

  • acceptance range

Verbose logs.


/*

  


*/

console.log(attr, colorString, parsedColor, tagName);

/*

fill #fff { type: 'rgb', r: 255, g: 255, b: 255, alpha: 1 } undefined                                                           

fill #fff { type: 'rgb', r: 255, g: 255, b: 255, alpha: 1 } path                                                                

fill #fff { type: 'rgb', r: 255, g: 255, b: 255, alpha: 1 } path                                                                

fill #fff { type: 'rgb', r: 255, g: 255, b: 255, alpha: 1 } undefined 

*/

console.log("Source SVG:", svg.toString());

/*

Source SVG: 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26.65" height="25.223" viewBox="0 0 26.65 25.223">
<defs>
<style>.a{fill:#fff}</style>
<clipPath id="a"><rect class="a" width="26.65" height="25.223"/></clipPath></defs>
<g clip-path="url(#a)">
<path class="a" d="M5.145,28.427c-1.388-.045-2.326-.187-2.736-.595a3.112,3.112,0,0,1-.48-2.127,9.061,9.061,0,0,1,5.5-7.771A.964.964,0,0,0,7.585,16.2,3.722,3.722,0,0,1,9.463,9.261a2.831,2.831,0,0,1,.425.025.854.854,0,0,0,.809-.931c0-.889-.884-1.023-1.233-1.023a5.648,5.648,0,0,0-4.15,9.482,10.933,10.933,0,0,0-3.406,3.23A10.209,10.209,0,0,0,0,25.705c0,1.718.314,2.763,1.048,3.494.85.846,2.094,1.091,4.022,1.156A.907.907,0,0,0,6,29.33a.883.883,0,0,0-.859-.9"
transform="translate(0 -6.277)"/>
<path class="a" d="M57.043,10.431a5.6,5.6,0,1,0-5.653,0A11.908,11.908,0,0,0,44.063,20.8c0,4.877,2.219,4.415,10.153,4.415S64.37,25.673,64.37,20.8a11.908,11.908,0,0,0-7.327-10.365" 
transform="translate(-37.719)"/>
</g>
</svg>

*/

console.log("Cleaned up SVG:", svg.toString());

/*

Cleaned up SVG: 
<svg xmlns="http://www.w3.org/2000/svg" width="22.596" height="27" 
viewBox="0 0 22.596 27">
<path d="M123.574,61.017a12.957,12.957,0,0,0-5.951,2.3.494.494,0,0,1-.728-.1l-2.926-4.493A1.191,1.191,0,0,1,115,56.889h5.717a1.228,1.228,0,0,1,.985.484l2.193,2.889a.475.475,0,0,1-.323.755m12.61-2.3-2.9,4.495a.5.5,0,0,1-.731.1,12.881,12.881,0,0,0-5.947-2.3.473.473,0,0,1-.323-.752l2.153-2.883a1.236,1.236,0,0,1,.993-.492h5.72a1.19,1.19,0,0,1,1.038,1.824m-5.347,13.522-2.185,2.056.516,2.9c.173.975-.4,1.4-1.327.931l-2.7-1.372-2.7,1.372c-.905.46-1.5.061-1.327-.931l.516-2.905-2.187-2.053c-.734-.691-.525-1.361.508-1.506l3.018-.424,1.349-2.639c.452-.888,1.178-.9,1.641,0l1.351,2.639,3.018.423c1.015.144,1.256.807.507,1.506m-5.691-9.74a10.7,10.7,0,1,0,11.075,10.7,10.891,10.891,0,0,0-11.075-10.7" 
transform="translate(-113.777 -56.889)" fill="#fff"/></svg>

*/

console.log("ColorParsed SVG:", svg.toString());

/*

ColorParsed SVG: <svg xmlns="http://www.w3.org/2000/svg" width="26.65" height="25.223" viewBox="0 0 26.65 25.223">
<defs>
<style>
.a {

        fill: currentColor;

}
</style>
<clipPath id="a">
<rect class="a" width="26.65" height="25.223"/>
</clipPath>
</defs>
<g clip-path="url(#a)">
<path class="a" d="M5.145,28.427c-1.388-.045-2.326-.187-2.736-.595a3.112,3.112,0,0,1-.48-2.127,9.061,9.061,0,0,1,5.5-7.771A.964.964,0,0,0,7.585,16.2,3.722,3.722,0,0,1,9.463,9.261a2.831,2.831,0,0,1,.425.025.854.854,0,0,0,.809-.931c0-.889-.884-1.023-1.233-1.023a5.648,5.648,0,0,0-4.15,9.482,10.933,10.933,0,0,0-3.406,3.23A10.209,10.209,0,0,0,0,25.705c0,1.718.314,2.763,1.048,3.494.85.846,2.094,1.091,4.022,1.156A.907.907,0,0,0,6,29.33a.883.883,0,0,0-.859-.9" 
transform="translate(0 -6.277)"/>
<path class="a" d="M57.043,10.431a5.6,5.6,0,1,0-5.653,0A11.908,11.908,0,0,0,44.063,20.8c0,4.877,2.219,4.415,10.153,4.415S64.37,25.673,64.37,20.8a11.908,11.908,0,0,0-7.327-10.365" 
transform="translate(-37.719)"/>
</g>
</svg>

*/

console.log(
  "After Cleaning up, parsedColor, and Before Optimise:",
  svg.toString()
);

/*

After Cleaning up, parsedColor, and Before Optimise: 
<svg xmlns="http://www.w3.org/2000/svg" width="26.65" height="25.223" viewBox="0 0 26.65 25.223">
<defs>
<style>
.a {
        fill: currentColor;
}
</style>
<clipPath id="a">
<rect class="a" width="26.65" height="25.223"/>
</clipPath>
</defs>
<g clip-path="url(#a)"><path class="a" d="M5.145,28.427c-1.388-.045-2.326-.187-2.736-.595a3.112,3.112,0,0,1-.48-2.127,9.061,9.061,0,0,1,5.5-7.771A.964.964,0,0,0,7.585,16.2,3.722,3.722,0,0,1,9.463,9.261a2.831,2.831,0,0,1,.425.025.854.854,0,0,0,.809-.931c0-.889-.884-1.023-1.233-1.023a5.648,5.648,0,0,0-4.15,9.482,10.933,10.933,0,0,0-3.406,3.23A10.209,10.209,0,0,0,0,25.705c0,1.718.314,2.763,1.048,3.494.85.846,2.094,1.091,4.022,1.156A.907.907,0,0,0,6,29.33a.883.883,0,0,0-.859-.9" 
transform="translate(0 -6.277)"/>
<path class="a" d="M57.043,10.431a5.6,5.6,0,1,0-5.653,0A11.908,11.908,0,0,0,44.063,20.8c0,4.877,2.219,4.415,10.153,4.415S64.37,25.673,64.37,20.8a11.908,11.908,0,0,0-7.327-10.365" 
transform="translate(-37.719)"/>
</g>
</svg>

*/

console.log("After Optimise and Before Update paths:", svg.toString());

/*

After Optimise and Before Update paths: 
<svg xmlns="http://www.w3.org/2000/svg" width="26.65" height="25.223" viewBox="0 0 26.65 25.223">
<defs>
<clipPath id="svgID0">
<path d="M0 0h26.65v25.223H0z" class="svgID0"/>
</clipPath>
<style>.a{fill:currentColor}</style>
</defs>
<g clip-path="url(#svgID0)">
<path d="M5.145 22.15c-1.388-.045-2.326-.187-2.736-.595a3.112 3.112 0 01-.48-2.127 9.061 9.061 0 015.5-7.771.964.964 0 00.156-1.734 3.722 3.722 0 011.878-6.939 2.831 2.831 0 01.425.025.854.854 0 00.809-.931c0-.889-.884-1.023-1.233-1.023a5.648 5.648 0 00-4.15 9.482 10.933 10.933 0 00-3.406 3.23A10.209 10.209 0 000 19.428c0 1.718.314 2.763 1.048 3.494.85.846 2.094 1.091 4.022 1.156A.907.907 0 006 23.053a.883.883 0 00-.859-.9" class="svgID0"/>
<path d="M19.324 10.431a5.6 5.6 0 10-5.653 0A11.908 11.908 0 006.344 20.8c0 4.877 2.219 4.415 10.153 4.415s10.154.458 10.154-4.415a11.908 11.908 0 00-7.327-10.365" class="svgID0"/>
</g>
</svg>

*/

console.log("After Update paths:", svg.toString());

/*

After Update paths: 
<svg xmlns="http://www.w3.org/2000/svg" width="26.65" height="25.223" viewBox="0 0 26.65 25.223">
<defs>
<clipPath id="svgID0"><path d="M0 0h26.65v25.223H0z" class="svgID0"/></clipPath>
<style>.a{fill:currentColor}</style>
</defs>
<g clip-path="url(#svgID0)">
<path d="M5.145 22.15c-1.388-.045-2.326-.187-2.736-.595a3.112 3.112 0 0 1-.48-2.127a9.061 9.061 0 0 1 5.5-7.771a.964.964 0 0 0 .156-1.734a3.722 3.722 0 0 1 1.878-6.939a2.831 2.831 0 0 1 .425.025a.854.854 0 0 0 .809-.931c0-.889-.884-1.023-1.233-1.023a5.648 5.648 0 0 0-4.15 9.482a10.933 10.933 0 0 0-3.406 3.23A10.209 10.209 0 0 0 0 19.428c0 1.718.314 2.763 1.048 3.494c.85.846 2.094 1.091 4.022 1.156A.907.907 0 0 0 6 23.053a.883.883 0 0 0-.859-.9" class="svgID0"/>
<path d="M19.324 10.431a5.6 5.6 0 1 0-5.653 0A11.908 11.908 0 0 0 6.344 20.8c0 4.877 2.219 4.415 10.153 4.415s10.154.458 10.154-4.415a11.908 11.908 0 0 0-7.327-10.365" class="svgID0"/>
</g>
</svg>

*/