diff --git a/.changeset/free-turkeys-lick.md b/.changeset/free-turkeys-lick.md new file mode 100644 index 0000000000..9c30c9e5b0 --- /dev/null +++ b/.changeset/free-turkeys-lick.md @@ -0,0 +1,22 @@ +--- +"flowbite-react": patch +--- + +Use CSS variables for `tailwindcss@v4` instead of inline colors. + +### Changes + +- [x] automatically generate `tailwindcss@v4` config file +- [x] use css variables for the `v4` config + +### Breaking changes + +Only applicable to `tailwindcss@v4`: + +before +`@plugin "flowbite-react/plugins/tailwindcss"` - this points to the legacy JS plugin (that uses inline colors) + +after +`@import "flowbite-react/plugins/tailwindcss"` - this points to the CSS-based plugin (that uses CSS variables) + +The breaking change is self-managed once upgrading `flowbite-react` and starting/building the app, which runs the `flowbite-react dev` or respectively `flowbite-react build` command that triggers the patch/fix/migration automatically. diff --git a/bun.lock b/bun.lock index 35a6295eaa..e047a9e776 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "root", diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore index 6cacdc638f..d971cbfada 100644 --- a/packages/ui/.gitignore +++ b/packages/ui/.gitignore @@ -1,3 +1,4 @@ coverage dist src/metadata +src/plugin/tailwindcss/index.css diff --git a/packages/ui/package.json b/packages/ui/package.json index ac1bcd1d96..2b728c7b81 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -150,7 +150,8 @@ "require": { "types": "./dist/plugin/tailwindcss/index.d.cts", "default": "./dist/plugin/tailwindcss/index.cjs" - } + }, + "style": "./dist/plugin/tailwindcss/index.css" }, "./plugin/tailwindcss/*": { "import": { @@ -252,10 +253,11 @@ "format": "prettier . --write", "format:check": "prettier . --check", "generate-metadata": "bun scripts/generate-metadata.ts", + "generate-tailwind-plugin-css-file": "bun scripts/generate-tailwind-plugin-css-file.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "prepack": "clean-package", - "prepare": "bun run generate-metadata", + "prepare": "bun run generate-metadata && bun run generate-tailwind-plugin-css-file", "prepublishOnly": "bun run build", "test": "bun test scripts src/cli src/helpers && vitest", "test:coverage": "vitest run --coverage", diff --git a/packages/ui/rollup.config.js b/packages/ui/rollup.config.js index 1e76ddf72d..94a81bb31b 100644 --- a/packages/ui/rollup.config.js +++ b/packages/ui/rollup.config.js @@ -4,8 +4,10 @@ import esbuild from "rollup-plugin-esbuild"; import { rollupPluginUseClient } from "rollup-plugin-use-client"; import packageJson from "./package.json"; +const tailwindPluginCssFile = "plugin/tailwindcss/index.css"; + let entries = await Array.fromAsync(new Glob("src/**/*").scan()); -entries = entries.filter((path) => !path.includes(".test.")).sort(); +entries = entries.filter((path) => !path.includes(".test.") && !path.includes(tailwindPluginCssFile)).sort(); const external = [ "ast-types", @@ -53,10 +55,12 @@ export default { plugins: [ cleanOutputDir(), generateMetadata(), + generateTailwindPluginCssFile(), esbuild({ sourceMap: false, }), rollupPluginUseClient(), + copyTailwindPluginCssFile(), generateDts(), generateRspackPlugin(), ], @@ -90,6 +94,24 @@ function generateMetadata() { }; } +function generateTailwindPluginCssFile() { + return { + name: "generate-tailwind-plugin-css-file", + async buildStart() { + await $`bun run generate-tailwind-plugin-css-file`; + }, + }; +} + +function copyTailwindPluginCssFile() { + return { + name: "copy-tailwind-plugin-css-file", + async buildEnd() { + await Bun.write(`${outputDir}/${tailwindPluginCssFile}`, await Bun.file(`src/${tailwindPluginCssFile}`).text()); + }, + }; +} + function generateDts() { return { name: "generate-dts", diff --git a/packages/ui/scripts/generate-tailwind-plugin-css-file.ts b/packages/ui/scripts/generate-tailwind-plugin-css-file.ts new file mode 100644 index 0000000000..82e3d12fd7 --- /dev/null +++ b/packages/ui/scripts/generate-tailwind-plugin-css-file.ts @@ -0,0 +1,60 @@ +import prettier from "prettier"; +import { colors } from "../src/plugin/tailwindcss/colors"; +import { backgroundImage, boxShadow } from "../src/plugin/tailwindcss/theme"; + +let VARS = ""; +let COLORS = ""; + +for (const colorName in colors) { + const colorShades = colors[colorName as keyof typeof colors]; + + for (const shade in colorShades) { + VARS += `--${colorName}-${shade}: ${colorShades[shade as unknown as keyof typeof colorShades]};\n`; + } + VARS += `\n`; + + for (const shade in colorShades) { + COLORS += `--color-${colorName}-${shade}: var(--${colorName}-${shade});\n`; + } + COLORS += `\n`; +} + +const BACKGROUND_IMAGES = Object.entries(backgroundImage) + .map(([name, value]) => `--background-image-${name}: ${value};`) + .join("\n"); + +const SHADOWS = Object.entries(boxShadow) + .map(([name, value]) => `--shadow-${name}: ${value};`) + .join("\n"); + +const content = ` +:root { + ${VARS} +} + +@theme inline { + ${COLORS} +} + +@theme { + ${BACKGROUND_IMAGES} +} + +@theme { + ${SHADOWS} +} +`; + +const outputFile = "src/plugin/tailwindcss/index.css"; + +try { + await Bun.write( + outputFile, + await prettier.format(content, { + parser: "css", + }), + ); + console.log(`Tailwind plugin CSS file generated successfully: ${outputFile}`); +} catch (error) { + console.error(`Failed generating Tailwind plugin CSS file: ${outputFile}`, error); +} diff --git a/packages/ui/src/cli/commands/build.ts b/packages/ui/src/cli/commands/build.ts index d29a8ceb21..e69d22c9ff 100644 --- a/packages/ui/src/cli/commands/build.ts +++ b/packages/ui/src/cli/commands/build.ts @@ -7,6 +7,7 @@ import { findFiles } from "../utils/find-files"; import { getConfig } from "../utils/get-config"; import { setupInit } from "./setup-init"; import { setupOutputDirectory } from "./setup-output-directory"; +import { setupTailwind } from "./setup-tailwind"; export async function build() { await setupOutputDirectory(); @@ -14,6 +15,7 @@ export async function build() { try { const config = await getConfig(); await setupInit(config); + await setupTailwind(); const initLogger = createInitLogger(config); const importedComponents: string[] = []; diff --git a/packages/ui/src/cli/commands/dev.ts b/packages/ui/src/cli/commands/dev.ts index 7fe278123e..839d39ccdc 100644 --- a/packages/ui/src/cli/commands/dev.ts +++ b/packages/ui/src/cli/commands/dev.ts @@ -21,11 +21,13 @@ import { getConfig } from "../utils/get-config"; import { setupGitIgnore } from "./setup-gitignore"; import { setupInit } from "./setup-init"; import { setupOutputDirectory } from "./setup-output-directory"; +import { setupTailwind } from "./setup-tailwind"; export async function dev() { await setupOutputDirectory(); let config = await getConfig(); await setupInit(config); + await setupTailwind(); const initLogger = createInitLogger(config); if (config.components.length) { diff --git a/packages/ui/src/cli/commands/setup-tailwind.ts b/packages/ui/src/cli/commands/setup-tailwind.ts index 05dca47b85..ae9134fd42 100644 --- a/packages/ui/src/cli/commands/setup-tailwind.ts +++ b/packages/ui/src/cli/commands/setup-tailwind.ts @@ -74,13 +74,17 @@ async function setupTailwindV4() { .join(path.relative(path.dirname(file), process.cwd()), classListFilePath) .replace(/\\/g, "/"); - const pluginRegex = new RegExp( + const pluginRegex_OLD = new RegExp( `@plugin\\s+['"](${pluginDirectivePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})['"](;|\\s|$)`, ); + const pluginRegex = new RegExp( + `@import\\s+['"](${pluginDirectivePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})['"](;|\\s|$)`, + ); const sourceRegex = new RegExp( `@source\\s+['"](${sourceDirectivePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})['"](;|\\s|$)`, ); + const hasPluginDirective_OLD = pluginRegex_OLD.test(content); const hasPluginDirective = pluginRegex.test(content); const hasSourceDirective = sourceRegex.test(content); @@ -88,9 +92,17 @@ async function setupTailwindV4() { continue; } + if (hasPluginDirective_OLD) { + const pluginImportIndex = lines.findIndex((line) => pluginRegex_OLD.test(line)); + if (pluginImportIndex !== -1) { + console.log(`Removing old flowbite-react plugin import directive from ${file}...`); + lines.splice(pluginImportIndex, 1); + } + } + const directivesToAdd = []; if (!hasPluginDirective) { - const pluginDirective = `@plugin ${quoteType}${pluginDirectivePath}${quoteType};`; + const pluginDirective = `@import ${quoteType}${pluginDirectivePath}${quoteType};`; directivesToAdd.push(pluginDirective); } if (!hasSourceDirective) {