2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-28 04:59:07 +00:00

chore: tailwind plugin redesign

This commit is contained in:
Vjacheslav Trushkin 2024-05-10 10:00:08 +03:00
parent 3365d71a42
commit f6eb380836
5 changed files with 331 additions and 112 deletions

View File

@ -6,42 +6,100 @@
<link href="../dist/output.css" rel="stylesheet" />
</head>
<body class="m-0 p-2">
<p>
Few icons that change color on hover (first icon also changes icon
on hover):
<span class="text-3xl demo">
<section class="mb-2">
<h1 class="text-2xl">Main plugin</h1>
<p>
Monotone icons (changes color, blue):
<span class="text-2xl demo">
<span class="iconify mdi-light--home"></span>
<span
class="iconify mdi-light--arrow-left text-blue-600"
></span>
<span
class="iconify vscode-icons--file-type-light-ini text-blue-600"
></span>
</span>
</p>
<p>
Color icons:
<span class="text-2xl demo">
<span class="iconify-color mdi-light--home"></span>
<span
class="iconify-color vscode-icons--file-type-firebase"
></span>
<span
class="iconify-color vscode-icons--file-type-js-official"
></span>
</span>
</p>
</section>
<section class="mb-2">
<h1 class="text-2xl">Dynamic selectors plugin, custom options</h1>
<p>Icons should scale to 1.5em, which is about 24px</p>
<p>
Monotone icons (red, blue):
<span
class="icon-[mdi-light--arrow-left] hover:icon-hover-[mdi-light--arrow-right]"
class="custom-monotone i-mdi-light-home text-red-600"
></span>
<span class="icon-[mdi-light--forum]"></span>
</span>
</p>
<p>
Custom icons, imported from "svg" directory (first icon's animation
slowed down using "customise" option):
<span class="text-3xl demo">
<span class="icon-[custom--spinner1]"></span>
<span class="icon-[custom--spinner2]"></span>
</span>
</p>
<p>
Icons with hardcoded palette:
<span class="text-3xl demo">
<span
class="icon-[vscode-icons--file-type-access] hover:icon-hover-[vscode-icons--file-type-access2]"
class="custom-monotone i-custom-spinner1 text-blue-600"
></span>
<span class="icon-[vscode-icons--file-type-vue]"></span>
</span>
</p>
<p>
Clean selector:
<span
class="icon--mdi-light icon--mdi-light--home text-3xl demo"
></span>
</p>
<p>
Custom size:
<span class="h-12 w-12 scaled-icon-[mdi-light--forum]"></span>
</p>
</p>
<p>
Failed icon (should not be pre-rendered):
<span
class="custom-monotone i-mdi-light-arrow-left text-red-600"
></span>
</p>
<p>
Colored icons:
<span class="custom-background i-mdi-light-home"></span>
<span class="custom-background i-custom-spinner2"></span>
</p>
</section>
<section class="mb-2">
<h1 class="text-2xl">Dynamic selectors plugin</h1>
<p>
Few icons that change color on hover (first icon also changes
icon on hover):
<span class="text-2xl demo">
<span
class="icon-[mdi-light--arrow-left] hover:icon-hover-[mdi-light--arrow-right]"
></span>
<span class="icon-[mdi-light--forum]"></span>
</span>
</p>
<p>
Custom icons, imported from "svg" directory (first icon's
animation slowed down using "customise" option):
<span class="text-2xl demo">
<span class="icon-[custom--spinner1]"></span>
<span class="icon-[custom--spinner2]"></span>
</span>
</p>
<p>
Icons with hardcoded palette:
<span class="text-3xl demo">
<span
class="icon-[vscode-icons--file-type-access] hover:icon-hover-[vscode-icons--file-type-access2]"
></span>
<span class="icon-[vscode-icons--file-type-vue]"></span>
</span>
</p>
</section>
<section class="mb-2">
<h1 class="text-2xl">Clean selectors plugin (deprecated)</h1>
<p>
Monotone icon: 1em size (changing color), text-2xl size (red):
<span class="icon--mdi-light icon--mdi-light--home demo"></span>
<span
class="icon--mdi-light icon--mdi-light--home text-2xl text-red-600"
></span>
</p>
<p>
Custom size set via width/height:
<span class="h-12 w-12 scaled-icon-[mdi-light--forum]"></span>
</p>
</section>
</body>
</html>

View File

@ -1,6 +1,7 @@
const {
addCleanIconSelectors,
addDynamicIconSelectors,
addIconSelectors,
} = require('@iconify/tailwind');
const {
importDirectorySync,
@ -59,6 +60,30 @@ customSet.forEachSync((name, type) => {
module.exports = {
content: ['./src/*.html'],
plugins: [
// Main plugin, default options
addIconSelectors(['mdi-light', 'vscode-icons']),
// Main plugin, custom options
addIconSelectors({
maskSelector: '.custom-monotone',
backgroundSelector: '.custom-background',
// Like UnoCSS
iconSelector: '.i-{prefix}-{name}',
scale: 1.5,
prefixes: [
{
prefix: 'mdi-light',
icons: ['home'],
customise: (content) =>
content.replace(/currentColor/g, '#40f'),
},
{
prefix: 'custom',
source: customSet.export(),
customise: (content) =>
content.replace(/currentColor/g, '#f20'),
},
],
}),
// Plugin with clean selectors: requires writing all used icons in first parameter
addCleanIconSelectors(['mdi-light:home']),
// Plugin with dynamic selectors

View File

@ -1,21 +1,8 @@
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
import type { IconifyJSON } from '@iconify/types';
// Callback for customising icon
type IconifyCustomiseCallback = (
content: string,
name: string,
prefix: string
) => string;
// Callback for loading icon set
type IconifyJSONLoaderCallback = () => IconifyJSON;
// Source for icon set: icon set, filename, or callback that loads icon set
export type IconifyIconSetSource =
| IconifyJSON
| string
| IconifyJSONLoaderCallback;
// Source for icon set: icon set, filename, or synchronous callback that loads icon set
export type IconifyIconSetSource = IconifyJSON | string | (() => IconifyJSON);
/**
* Common options
@ -26,7 +13,7 @@ export interface CommonIconifyPluginOptions {
iconSets?: Record<string, IconifyIconSetSource>;
// Replace icon content
customise?: IconifyCustomiseCallback;
customise?: (content: string, name: string, prefix: string) => string;
}
/**
@ -53,26 +40,12 @@ export interface DynamicIconifyPluginOptions
scale?: number;
}
/**
* Options for reusable selectors
*/
export interface ReusableIconifyPluginOptions {
// Selector for mask, defaults to ".iconify"
mask?: string;
// Selector for background, defaults to ".iconify-color"
background?: string;
// Variable name, defaults to "svg"
varName?: string;
}
/**
* Options for main plugin
*/
// Icons to include: array of names or callback
type IconsListOption = string[] | ((name: string) => boolean);
export type IconsListOption = string[] | ((name: string) => boolean);
// Source filename or icon set
type IconSetSource = string | IconifyJSON;
@ -88,16 +61,44 @@ interface IconSetOptions {
// Icons to load
icons?: IconsListOption;
// Customise callback. If set, it will be used instead of global customise callback
customise?: (content: string, name: string) => string;
}
// Array of icon sets to load
type IconifyPluginListOptions = (string | IconSetOptions)[];
// Full object
export interface IconifyPluginOptionsObject
extends ReusableIconifyPluginOptions {
export interface IconifyPluginOptionsObject {
// Selector for mask, defaults to ".iconify"
// If empty string, mask selector will not be generated
maskSelector?: string;
// Extra rules to add to mask selector
extraMaskRules?: Record<string, string>;
// Selector for background, defaults to ".iconify-color"
// If empty string, background selector will not be generated
backgroundSelector?: string;
// Extra rules to add to background selector
extraBackgroundRules?: Record<string, string>;
// Selector for icons, default is `.{prefix}--{name}`
iconSelector?: string;
// Variable name that contains icon, defaults to "svg"
varName?: string;
// Scale for icons, defaults to 1
scale?: number;
// Prefixes to load
prefixes: IconifyPluginListOptions;
// Customise callback
customise?: (content: string, name: string, prefix: string) => string;
}
// Full options

View File

@ -5,12 +5,13 @@ import type {
CleanIconifyPluginOptions,
DynamicIconifyPluginOptions,
IconifyPluginOptions,
IconifyPluginOptionsObject,
} from './helpers/options';
import { getCommonCSSRules } from '@iconify/utils/lib/css/common';
import { getCSSRulesForPlugin } from './preparsed';
/**
* Generate styles for dynamic selector: class="icon-[mdi-light--home]"
* Generate styles for dynamic selector
*
* Usage in HTML: <span class="icon-[mdi-light--home]" />
*/
export function addDynamicIconSelectors(options?: DynamicIconifyPluginOptions) {
const prefix = options?.prefix || 'icon';
@ -28,8 +29,30 @@ export function addDynamicIconSelectors(options?: DynamicIconifyPluginOptions) {
});
}
/**
* Generate rules for mask, background and selected icon sets
*
* Icons should combine either mask or background selector and icon selector
*
* This plugin generates only square icons. Icons that are not square will be resized to fit square.
*
* Usage in HTML: <span class="iconify mdi-light--home" />
*/
export function addIconSelectors(options: IconifyPluginOptions) {
const rules = getCSSRulesForPlugin(options);
return plugin(({ addUtilities }) => {
addUtilities(rules);
});
}
/**
* Generate styles for preset list of icons
*
* Requires knowing full list of icons
*
* Usage in HTML: <span class="icon--mdi-light icon--mdi-light--home" />
*
* @deprecated Use addIconSelectors instead
*/
export function addCleanIconSelectors(
icons: string[] | string,
@ -41,49 +64,6 @@ export function addCleanIconSelectors(
});
}
/**
* Iconify plugin
*
* TODO: export it when ready
*/
function iconifyPlugin(options: IconifyPluginOptions) {
return plugin(({ addUtilities }) => {
const rules = Object.create(null) as Record<
string,
Record<string, string>
>;
// Convert options to object
const fullOptions: IconifyPluginOptionsObject = Array.isArray(options)
? {
prefixes: options,
}
: options;
// Variable name, default to 'svg' (cannot be empty string)
const varName = fullOptions.varName || 'svg';
// Add common rules
const mask = fullOptions.mask ?? '.iconify';
const background = fullOptions.background ?? '.iconify-color';
if (mask) {
rules[mask] = getCommonCSSRules({
mode: 'mask',
varName,
});
}
if (background) {
rules[background] = getCommonCSSRules({
mode: 'background',
varName,
});
}
addUtilities(rules);
// TODO: add icon sets
});
}
/**
* Export types
*/

View File

@ -0,0 +1,155 @@
import type { IconifyJSON } from '@iconify/types';
import {
generateItemCSSRules,
getCommonCSSRules,
} from '@iconify/utils/lib/css/common';
import { defaultIconProps } from '@iconify/utils/lib/icon/defaults';
import { parseIconSet } from '@iconify/utils/lib/icon-set/parse';
import type {
IconifyPluginOptions,
IconifyPluginOptionsObject,
IconsListOption,
} from './helpers/options';
import { loadIconSet } from './helpers/loader';
/**
* Get CSS rules for main plugin
*/
export function getCSSRulesForPlugin(options: IconifyPluginOptions) {
const rules = Object.create(null) as Record<string, Record<string, string>>;
// Convert options to object
const fullOptions: IconifyPluginOptionsObject = Array.isArray(options)
? {
prefixes: options,
}
: options;
// Variable name, default to 'svg' (cannot be empty string)
const varName = fullOptions.varName || 'svg';
// Scale icons
const overrideRules: Record<string, string> = fullOptions.scale
? {
width: fullOptions.scale + 'em',
height: fullOptions.scale + 'em',
}
: {};
// Add common rules
const maskSelector = fullOptions.maskSelector ?? '.iconify';
const backgroundSelector =
fullOptions.backgroundSelector ?? '.iconify-color';
if (maskSelector) {
rules[maskSelector] = Object.assign(
getCommonCSSRules({
mode: 'mask',
varName,
}),
overrideRules,
fullOptions.extraMaskRules || {}
);
}
if (backgroundSelector) {
rules[backgroundSelector] = Object.assign(
getCommonCSSRules({
mode: 'background',
varName,
}),
overrideRules,
fullOptions.extraBackgroundRules || {}
);
}
// Add icon sets
const iconSelector = fullOptions.iconSelector || '.{prefix}--{name}';
fullOptions.prefixes.forEach((item) => {
let prefix: string;
let iconSet: IconifyJSON | undefined;
let iconsList: IconsListOption | undefined;
let customise: ((content: string, name: string) => string) | undefined;
// Load icon set
if (typeof item === 'string') {
// Prefix
prefix = item;
iconSet = loadIconSet(prefix);
} else if (item.source) {
// Source, possibly with prefix
iconSet = loadIconSet(item.source);
prefix = item.prefix || iconSet?.prefix;
iconsList = item.icons;
customise = item.customise;
if (!prefix) {
throw new Error(
'Custom icon set does not have a prefix. Please set "prefix" property'
);
}
} else {
// Prefix
prefix = item.prefix || iconSet?.prefix;
iconSet = prefix ? loadIconSet(prefix) : undefined;
iconsList = item.icons;
customise = item.customise;
}
// Validate it
if (!iconSet) {
throw new Error(
`Cannot load icon set for "${prefix}". Install "@iconify-json/${prefix}" as dev dependency?`
);
}
if (!prefix) {
throw new Error(
'Bad icon set entry, must have either "prefix" or "source" set'
);
}
// Load icons
parseIconSet(iconSet, (name, data) => {
// Check if icon should be rendered
if (iconsList) {
if (Array.isArray(iconsList)) {
if (!iconsList.includes(name)) {
return;
}
} else if (!iconsList(name)) {
return;
}
}
// Customise icon
const body = customise
? customise(data.body, name)
: fullOptions.customise
? fullOptions.customise(data.body, name, prefix)
: data.body;
// Generate CSS
const iconRules = generateItemCSSRules(
{
...defaultIconProps,
...data,
body,
},
{
mode: 'mask', // not used because varName is set, but required
varName,
forceSquare: true,
}
);
// Generate selector
const selector = iconSelector
.replace('{prefix}', prefix)
.replace('{name}', name);
// Add to rules
rules[selector] = iconRules;
});
});
// Return
return rules;
}