diff --git a/.changeset/many-bottles-hope.md b/.changeset/many-bottles-hope.md new file mode 100644 index 000000000..026019ff6 --- /dev/null +++ b/.changeset/many-bottles-hope.md @@ -0,0 +1,6 @@ +--- +"@cypress-design/css": patch +--- + +To avoid having every color in the universe in the **safelist**, add the IconExtractor. +Also, remove the entire safelist from the css plugins windi config. diff --git a/css/src/icon-color-plugins.ts b/css/src/icon-color-plugins.ts index 6090a019e..85c5a4d7b 100644 --- a/css/src/icon-color-plugins.ts +++ b/css/src/icon-color-plugins.ts @@ -8,6 +8,8 @@ import createPlugin from 'windicss/plugin' import { reduce, kebabCase, isObject } from 'lodash' import { colors } from './colors' +import { DefaultExtractor } from 'vite-plugin-windicss' +import { Extractor } from 'windicss/types/interfaces' interface RuleConfig { name: string @@ -157,3 +159,49 @@ export const IconDuotoneColorsPlugin = createPlugin( addUtilities(addIconUtilityClasses(theme)) } ) + +export const ICON_ATTRIBUTE_NAMES_TO_CLASS_GENERATOR = { + fillColor: (attrValue: string) => `icon-light-${attrValue}`, + strokeColor: (attrValue: string) => `icon-dark-${attrValue}`, + secondaryFillColor: (attrValue: string) => + `icon-light-secondary-${attrValue}`, + secondaryStrokeColor: (attrValue: string) => + `icon-dark-secondary-${attrValue}`, +} as const + +function isIconAttribute( + attrName: string +): attrName is keyof typeof ICON_ATTRIBUTE_NAMES_TO_CLASS_GENERATOR { + return ICON_ATTRIBUTE_NAMES_TO_CLASS_GENERATOR.hasOwnProperty(attrName) +} + +/** + * transforms the attributes of icons into classes + * to be kept in the windicss css file after purgecss + */ +export const IconExtractor: Extractor = { + extensions: ['vue', 'js', 'ts', 'tsx'], + extractor: (code, id) => { + const { tags, classes = [], attributes } = DefaultExtractor(code, id) + + const additionalColorClasses = + attributes?.names.reduce((set, attrName, index) => { + if (isIconAttribute(attrName)) { + set.add( + ICON_ATTRIBUTE_NAMES_TO_CLASS_GENERATOR[attrName]( + attributes.values[index] + ) + ) + } + return set + }, new Set()) ?? new Set() + + return { + tags, + get classes() { + return [...classes, ...Array.from(additionalColorClasses)] + }, + attributes, + } + }, +} diff --git a/css/src/index.ts b/css/src/index.ts index 498c22b9b..e7346de37 100644 --- a/css/src/index.ts +++ b/css/src/index.ts @@ -31,3 +31,5 @@ export const CyCSSWebpackPlugin = (options: UserOptions) => new WebpackPlugin(getConfig(options)) export * from './colors' + +export { ICON_ATTRIBUTE_NAMES_TO_CLASS_GENERATOR } from './icon-color-plugins' diff --git a/css/src/safelist.ts b/css/src/safelist.ts deleted file mode 100644 index 83e0faf8a..000000000 --- a/css/src/safelist.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * WindiCSS will strip out any styles that aren't used. - * We do a lot of dynamic stuff, and we're not too concerned - * with bundle size, so this is a pretty greedy list - */ -import { colors } from './colors' -import { map, reduce, kebabCase } from 'lodash' - -const textSafelist = ['xs', 'sm', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl'] - .map((v) => `text-${v}`) - .join(' ') -const colorSafelist = reduce( - { - ...colors, - transparent: { ONLY: true }, - current: { ONLY: true }, - white: { ONLY: true }, - black: { ONLY: true }, - }, - (acc, variants, colorName) => { - const name = kebabCase(colorName) - - return `${acc} - ${map(variants, (_: string, k: string) => { - if (k === 'DEFAULT') return `` - const variantName = k === 'ONLY' ? name : `${name}-${k}` - return ` - icon-light-${variantName} - icon-dark-${variantName} - icon-light-secondary-${variantName} - icon-dark-secondary-${variantName} - bg-${variantName} - text-${variantName} - before:bg-${variantName} - before:text-${variantName}` - }).join(' ')}` - }, - ' bg-white bg-black text-white text-black' -) - -export const safelist = `${textSafelist} ${colorSafelist}` diff --git a/css/src/windi.config.ts b/css/src/windi.config.ts index f77f34a2d..b5c19af57 100644 --- a/css/src/windi.config.ts +++ b/css/src/windi.config.ts @@ -1,8 +1,7 @@ import { defineConfig } from 'windicss/helpers' // @ts-ignore import InteractionVariants from '@windicss/plugin-interaction-variants' -import { IconDuotoneColorsPlugin } from './icon-color-plugins' -import { safelist } from './safelist' +import { IconDuotoneColorsPlugin, IconExtractor } from './icon-color-plugins' import { colors } from './colors' import { shortcuts } from './shortcuts' @@ -29,7 +28,6 @@ export default defineConfig({ }, }, }, - safelist, variants: { // What's hocus? // Hocus is a portmanteau of hover + focus. This is useful because @@ -54,5 +52,6 @@ export default defineConfig({ shortcuts, extract: { exclude: ['node_modules/**/*', '.git/**/*'], + extractors: [IconExtractor], }, }) diff --git a/package.json b/package.json index cdda0e66d..cfae512cc 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "concurrently": "^7.1.0", "cypress": "^10.1.0", "cypress-axe": "^0.14.0", + "cypress-real-events": "^1.7.0", "eslint": "^8.17.0", "eslint-plugin-no-only-tests": "^2.6.0", "execa": "^6.1.0", @@ -69,8 +70,5 @@ }, "gitHooks": { "pre-commit": "lint-staged" - }, - "devDependencies": { - "cypress-real-events": "^1.7.0" } -} +} \ No newline at end of file diff --git a/storybook/intro/.storybook/main.js b/storybook/intro/.storybook/main.js index e7e941ab8..141d532f2 100644 --- a/storybook/intro/.storybook/main.js +++ b/storybook/intro/.storybook/main.js @@ -1,4 +1,5 @@ -const { CyCSSWebpackPlugin } = require('@cypress-design/css') +const { CyCSSWebpackPlugin, colors } = require('@cypress-design/css') +const { map, reduce, kebabCase } = require('lodash') const path = require('path') const CopyWebpackPlugin = require('copy-webpack-plugin') @@ -8,7 +9,6 @@ module.exports = { '@storybook/addon-links', '@storybook/addon-essentials', 'storybook-addon-designs', - // "@storybook/addon-interactions", ], framework: '@storybook/react', refs: (config, { configType }) => { @@ -78,6 +78,27 @@ module.exports = { path.resolve(__dirname, '../stories/src/*.tsx'), ], }, + safelist: reduce( + { ...colors, transparent: { ONLY: true }, current: { ONLY: true } }, + (acc, variants, colorName) => { + const name = kebabCase(colorName) + + return `${acc} + ${map(variants, (_, k) => { + if (k === 'DEFAULT') return `` + const variantName = k === 'ONLY' ? name : `${name}-${k}` + return ` + bg-${variantName} + text-${variantName} + before:bg-${variantName} + before:text-${variantName} + icon-light-${variantName} + icon-dark-${variantName} + icon-light-secondary-${variantName} + icon-dark-secondary-${variantName}` + }).join(' ')}` + } + ), }) ) return config