diff --git a/.changeset/tricky-planes-shake.md b/.changeset/tricky-planes-shake.md
new file mode 100644
index 0000000000..c2785cbb64
--- /dev/null
+++ b/.changeset/tricky-planes-shake.md
@@ -0,0 +1,35 @@
+---
+"flowbite-react": minor
+---
+
+# Breaking changes
+
+- removed `flowbite-react patch` CLI command
+
+## Changes
+
+- `flowbite-react/.gitignore`, `flowbite-react/config.json` self manages, regenerates and repairs
+- new generated file `init.(jsx|tsx)` in `.flowbite-react/` directory that syncs up values from `config.json` that also are needed in React app runtime (similar to how a React context works)
+ - synced on CLI commands: `init`, `build`, `dev`, `register`
+- If you have custom configuration in `.flowbite-react/config.json` (different `dark`/`prefix`/`version` values), you must render `` at the root level of your app to sync runtime with node config values
+ - notify users to include `` at the root level if custom `dark`, `prefix`, or `version` values are detected in the configuration file
+- expose `flowbite-react/store` import path
+- check if `flowbite-react` is installed when `npx flowbite-react@latest init`
+ - bump the version to latest if below `0.11.x`
+- remove redundant `{ flag: "w" }` in `fs.writeFile`
+- update `dark-mode.md` and `prefix.md` documentation to reflect the `` changes
+- add Tailwind CSS version support in theme mode handling and fix dark theme toggle class in Tailwind CSS v4
+
+## Migration Guide
+
+1. Remove `flowbite-react patch` from your `package.json`
+
+ ```diff
+ {
+ "scripts": {
+ - "postinstall": "flowbite-react patch"
+ }
+ }
+ ```
+
+2. Add `` (import from `.flowbite-react/init.(jsx|tsx)`) at the root level of your app if you have custom configuration in `.flowbite-react/config.json` (different `dark`/`prefix`/`version` values).
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 243706b0a7..fd7c3264cb 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -7,7 +7,7 @@ runs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
- bun-version: 1.2.2
+ bun-version: 1.2.18
- name: Setup Node
uses: actions/setup-node@v4
diff --git a/apps/storybook/.flowbite-react/config.json b/apps/storybook/.flowbite-react/config.json
index c987f54cc8..b861718e37 100644
--- a/apps/storybook/.flowbite-react/config.json
+++ b/apps/storybook/.flowbite-react/config.json
@@ -1,9 +1,10 @@
{
- "$schema": "../../../node_modules/flowbite-react/schema.json",
+ "$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": [],
"dark": true,
"path": "src/components",
"prefix": "",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 3
}
diff --git a/apps/storybook/.flowbite-react/init.tsx b/apps/storybook/.flowbite-react/init.tsx
new file mode 100644
index 0000000000..1ef82b42b1
--- /dev/null
+++ b/apps/storybook/.flowbite-react/init.tsx
@@ -0,0 +1,22 @@
+/* eslint-disable */
+// @ts-nocheck
+// biome-ignore-all lint: auto-generated file
+
+// This file is auto-generated by the flowbite-react CLI.
+// Do not edit this file directly.
+// Instead, edit the .flowbite-react/config.json file.
+
+import { StoreInit } from "flowbite-react/store/init";
+import React from "react";
+
+export const config = {
+ dark: true,
+ prefix: "",
+ version: 3,
+};
+
+export function ThemeInit() {
+ return ;
+}
+
+ThemeInit.displayName = "ThemeInit";
diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.tsx
similarity index 64%
rename from apps/storybook/.storybook/preview.ts
rename to apps/storybook/.storybook/preview.tsx
index bd89877adc..39e20432bc 100644
--- a/apps/storybook/.storybook/preview.ts
+++ b/apps/storybook/.storybook/preview.tsx
@@ -1,5 +1,7 @@
import { withThemeByClassName } from "@storybook/addon-themes";
-import type { Preview } from "@storybook/react";
+import type { Decorator, Preview } from "@storybook/react";
+import React from "react";
+import { ThemeInit } from "../.flowbite-react/init";
import "./style.css";
@@ -17,7 +19,13 @@ const preview: Preview = {
},
};
-export const decorators = [
+export const decorators: Decorator[] = [
+ (Story) => (
+ <>
+
+
+ >
+ ),
withThemeByClassName({
themes: {
light: "light",
diff --git a/apps/storybook/tsconfig.json b/apps/storybook/tsconfig.json
index 21dd10605c..011de0cee9 100644
--- a/apps/storybook/tsconfig.json
+++ b/apps/storybook/tsconfig.json
@@ -21,5 +21,6 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["src"]
+ "include": ["**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules", "storybook-static"]
}
diff --git a/apps/web/.flowbite-react/config.json b/apps/web/.flowbite-react/config.json
index 5145e8553a..60d26e6446 100644
--- a/apps/web/.flowbite-react/config.json
+++ b/apps/web/.flowbite-react/config.json
@@ -1,9 +1,10 @@
{
- "$schema": "../../../node_modules/flowbite-react/schema.json",
+ "$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": [],
"dark": true,
"path": "components",
"prefix": "",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 3
}
diff --git a/apps/web/.flowbite-react/init.tsx b/apps/web/.flowbite-react/init.tsx
new file mode 100644
index 0000000000..1ef82b42b1
--- /dev/null
+++ b/apps/web/.flowbite-react/init.tsx
@@ -0,0 +1,22 @@
+/* eslint-disable */
+// @ts-nocheck
+// biome-ignore-all lint: auto-generated file
+
+// This file is auto-generated by the flowbite-react CLI.
+// Do not edit this file directly.
+// Instead, edit the .flowbite-react/config.json file.
+
+import { StoreInit } from "flowbite-react/store/init";
+import React from "react";
+
+export const config = {
+ dark: true,
+ prefix: "",
+ version: 3,
+};
+
+export function ThemeInit() {
+ return ;
+}
+
+ThemeInit.displayName = "ThemeInit";
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 65fb77e9dd..9ef2a042c2 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -3,6 +3,7 @@ import { Inter as InterFont } from "next/font/google";
import type { Metadata, Viewport } from "next/types";
import type { PropsWithChildren } from "react";
import { FathomScript } from "~/components/fathom-script";
+import { ThemeInit } from "../.flowbite-react/init";
import "~/styles/globals.css";
@@ -60,6 +61,7 @@ export default function RootLayout({ children }: PropsWithChildren) {
+
{children}
diff --git a/apps/web/content/docs/customize/config.mdx b/apps/web/content/docs/customize/config.mdx
index 1d44154148..e77e7c5a12 100644
--- a/apps/web/content/docs/customize/config.mdx
+++ b/apps/web/content/docs/customize/config.mdx
@@ -40,7 +40,8 @@ The configuration file follows this JSON Schema:
},
"prefix": {
"description": "Optional prefix to apply to all Tailwind CSS classes",
- "type": "string"
+ "type": "string",
+ "default": ""
},
"rsc": {
"description": "Whether to include the 'use client' directive for React Server Components",
@@ -51,9 +52,15 @@ The configuration file follows this JSON Schema:
"description": "Whether to use TypeScript (.tsx) or JavaScript (.jsx) for component creation",
"type": "boolean",
"default": true
+ },
+ "version": {
+ "description": "The version of Tailwind CSS to use",
+ "type": "number",
+ "enum": [3, 4],
+ "default": 4
}
},
- "required": ["components", "dark", "path", "prefix", "rsc", "tsx"]
+ "required": ["components", "dark", "path", "prefix", "rsc", "tsx", "version"]
}
```
@@ -101,6 +108,13 @@ For detailed instructions on configuring and using prefixes, see the [Prefix](/d
- Default: `true`
- Description: Whether to use TypeScript (.tsx) or JavaScript (.jsx) for component creation. When set to `false`, components will be created with .jsx extension.
+#### `version`
+
+- Type: `number`
+- Options: `3`, `4`
+- Default: `4`
+- Description: The version of Tailwind CSS to use.
+
## Automatic Class Generation
The automatic class generation system works in two modes:
@@ -120,10 +134,12 @@ Example config for automatic mode:
{
"$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": [],
+ "dark": true,
"path": "src/components",
"prefix": "",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 4
}
```
@@ -142,10 +158,12 @@ Example config for manual mode:
{
"$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": ["Button", "Card", "Modal"],
+ "dark": true,
"path": "src/components",
"prefix": "",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 4
}
```
diff --git a/apps/web/content/docs/customize/custom-components.mdx b/apps/web/content/docs/customize/custom-components.mdx
index 261f4b3055..69f63d8c05 100644
--- a/apps/web/content/docs/customize/custom-components.mdx
+++ b/apps/web/content/docs/customize/custom-components.mdx
@@ -25,10 +25,12 @@ You can customize these options in your config file `.flowbite-react/config.json
{
"$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": [],
+ "dark": true,
"path": "src/components",
"prefix": "",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 4
}
```
diff --git a/apps/web/content/docs/customize/dark-mode.mdx b/apps/web/content/docs/customize/dark-mode.mdx
index 4b055fd68c..e1c6fc08d9 100644
--- a/apps/web/content/docs/customize/dark-mode.mdx
+++ b/apps/web/content/docs/customize/dark-mode.mdx
@@ -55,28 +55,9 @@ declare const useThemeMode: () => {
To completely disable dark mode class generation in your Flowbite React application, you should use both of the following methods:
-### 1. Using the ThemeConfig Component
+### 1. Using the Configuration File
-First, disable dark mode at the application level by setting the `dark` prop to `false` in the `ThemeConfig` component:
-
-```jsx
-import { ThemeConfig } from "flowbite-react";
-
-function App() {
- return (
- <>
-
- {/* Your application */}
- >
- );
-}
-```
-
-This approach prevents the dark mode toggle functionality from working in your application's runtime.
-
-### 2. Using the Configuration File
-
-Additionally, you must disable dark mode in your build configuration by setting the `dark` property to `false` in your `.flowbite-react/config.json` file:
+First, disable dark mode in your build configuration by setting the `dark` property to `false` in your `.flowbite-react/config.json` file:
```json {4}
{
@@ -86,13 +67,32 @@ Additionally, you must disable dark mode in your build configuration by setting
"path": "src/components",
"prefix": "",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 4
}
```
This method prevents dark mode classes from being generated during the build process, which reduces your CSS bundle size.
-> **Important**: For complete dark mode disabling, both methods should be used together. The ThemeConfig approach affects runtime behavior, while the config.json approach affects build-time class generation.
+### 2. Using the ThemeInit Component
+
+When you have custom configuration in your `.flowbite-react/config.json` (including disabling dark mode), you must render the `ThemeInit` component at the root level of your application to sync the runtime with your configuration:
+
+```jsx
+// src/App.tsx
+import { ThemeInit } from "../.flowbite-react/init";
+
+function App() {
+ return (
+ <>
+
+ {/* Your application */}
+ >
+ );
+}
+```
+
+> **Important**: The configuration file approach affects build-time class generation, while the ThemeInit component ensures your runtime behavior matches your configuration.
## Framework Integration
diff --git a/apps/web/content/docs/customize/prefix.mdx b/apps/web/content/docs/customize/prefix.mdx
index 12a14a5d42..e8e1b65f30 100644
--- a/apps/web/content/docs/customize/prefix.mdx
+++ b/apps/web/content/docs/customize/prefix.mdx
@@ -19,7 +19,8 @@ To set a custom prefix for Flowbite React components, modify the `prefix` proper
"path": "components",
"prefix": "tw",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 4
}
```
@@ -49,16 +50,17 @@ In Tailwind CSS v4, the prefix cannot contain special characters (like hyphens).
## Update Your React Application
-Next, render the `ThemeConfig` component at the **root of your app** with the same `prefix` property:
+When you have custom configuration in your `.flowbite-react/config.json` (including a custom prefix), you must render the `ThemeInit` component at the root level of your application to sync the runtime with your configuration:
-```tsx {1,6}
-import { ThemeConfig } from "flowbite-react";
+```tsx
+// src/App.tsx
+import { ThemeInit } from "../.flowbite-react/init";
export default function App() {
return (
<>
-
- {/* ... */}
+
+ {/* Your application */}
>
);
}
diff --git a/apps/web/content/docs/getting-started/cli.mdx b/apps/web/content/docs/getting-started/cli.mdx
index a9b5050c21..1f7d912114 100644
--- a/apps/web/content/docs/getting-started/cli.mdx
+++ b/apps/web/content/docs/getting-started/cli.mdx
@@ -12,7 +12,6 @@ The Flowbite React CLI provides a comprehensive set of tools for:
- Managing development workflows
- Handling class generation
- Configuring your development environment
-- Patching Tailwind CSS configurations
- Providing help and documentation
## Installation & Setup
@@ -149,15 +148,6 @@ import { AccordionPanel } from "flowbite-react";
...
```
-### `flowbite-react patch`
-
-Patches Tailwind CSS to expose its version number for compatibility detection:
-
-- Creates a JavaScript file that exports the Tailwind CSS version
-- Necessary because package.json cannot be reliably read by all bundlers
-- Makes the version accessible via `import version from "tailwindcss/version"`
-- Enables Flowbite React to adapt its behavior based on the installed Tailwind version
-
### `flowbite-react register`
Registers the Flowbite React process for development:
@@ -232,7 +222,8 @@ The CLI creates a `.flowbite-react/config.json` file with several configuration
"path": "src/components",
"prefix": "",
"rsc": true,
- "tsx": true
+ "tsx": true,
+ "version": 4
}
```
@@ -260,6 +251,10 @@ Whether to include the `"use client"` directive for React Server Components. Def
Whether to use TypeScript (.tsx) or JavaScript (.jsx) for component creation. Default is `true`.
+#### `version`
+
+The version of Tailwind CSS to use. Default is `4`.
+
### VSCode Integration
The CLI sets up VSCode for optimal development:
diff --git a/package.json b/package.json
index 244e12cd13..2d18c5d42c 100644
--- a/package.json
+++ b/package.json
@@ -45,5 +45,5 @@
"prettier-plugin-tailwindcss": "0.6.11",
"turbo": "2.4.4"
},
- "packageManager": "bun@1.2.4"
+ "packageManager": "bun@1.2.18"
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 571a1c13b4..fc13eb96b4 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -162,6 +162,46 @@
"default": "./dist/plugin/tailwindcss/*.cjs"
}
},
+ "./store": {
+ "import": {
+ "types": "./dist/store/index.d.ts",
+ "default": "./dist/store/index.js"
+ },
+ "require": {
+ "types": "./dist/store/index.d.cts",
+ "default": "./dist/store/index.cjs"
+ }
+ },
+ "./store/*": {
+ "import": {
+ "types": "./dist/store/*.d.ts",
+ "default": "./dist/store/*.js"
+ },
+ "require": {
+ "types": "./dist/store/*.d.cts",
+ "default": "./dist/store/*.cjs"
+ }
+ },
+ "./store/init": {
+ "import": {
+ "types": "./dist/store/init/index.d.ts",
+ "default": "./dist/store/init/index.js"
+ },
+ "require": {
+ "types": "./dist/store/init/index.d.cts",
+ "default": "./dist/store/init/index.cjs"
+ }
+ },
+ "./store/init/*": {
+ "import": {
+ "types": "./dist/store/init/*.d.ts",
+ "default": "./dist/store/init/*.js"
+ },
+ "require": {
+ "types": "./dist/store/init/*.d.cts",
+ "default": "./dist/store/init/*.cjs"
+ }
+ },
"./theme": {
"import": {
"types": "./dist/theme/index.d.ts",
@@ -212,7 +252,6 @@
"format": "prettier . --write",
"format:check": "prettier . --check",
"generate-metadata": "bun scripts/generate-metadata.ts",
- "postinstall": "bun run src/cli/bin.ts patch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"prepack": "clean-package",
diff --git a/packages/ui/rollup.config.js b/packages/ui/rollup.config.js
index 65d77fcc19..55e7c49041 100644
--- a/packages/ui/rollup.config.js
+++ b/packages/ui/rollup.config.js
@@ -18,7 +18,6 @@ const external = [
"react/jsx-runtime",
"readline",
"tailwindcss/plugin",
- "tailwindcss/version.js",
...Object.keys({
...packageJson.dependencies,
...packageJson.devDependencies,
diff --git a/packages/ui/schema.json b/packages/ui/schema.json
index f645317884..0d43ff9666 100644
--- a/packages/ui/schema.json
+++ b/packages/ui/schema.json
@@ -128,7 +128,8 @@
},
"prefix": {
"description": "Optional prefix to apply to all Tailwind CSS classes. \nSee https://flowbite-react.com/docs/customize/config#prefix for more details.",
- "type": "string"
+ "type": "string",
+ "default": ""
},
"rsc": {
"description": "Whether to include the 'use client' directive for React Server Components. \nSee https://flowbite-react.com/docs/customize/config#rsc for more details.",
@@ -139,7 +140,13 @@
"description": "Whether to use TypeScript (.tsx) or JavaScript (.jsx) for component creation. \nSee https://flowbite-react.com/docs/customize/config#tsx for more details.",
"type": "boolean",
"default": true
+ },
+ "version": {
+ "description": "The version of Tailwind CSS to use. \nSee https://flowbite-react.com/docs/customize/config#version for more details.",
+ "type": "number",
+ "enum": [3, 4],
+ "default": 4
}
},
- "required": ["components", "dark", "path", "prefix", "rsc", "tsx"]
+ "required": ["components", "dark", "path", "prefix", "rsc", "tsx", "version"]
}
diff --git a/packages/ui/scripts/generate-metadata.ts b/packages/ui/scripts/generate-metadata.ts
index c49c432813..7bee096917 100644
--- a/packages/ui/scripts/generate-metadata.ts
+++ b/packages/ui/scripts/generate-metadata.ts
@@ -235,6 +235,7 @@ async function generateSchema(components: string[]): Promise {
description:
"Optional prefix to apply to all Tailwind CSS classes. \nSee https://flowbite-react.com/docs/customize/config#prefix for more details.",
type: "string",
+ default: "",
},
rsc: {
description:
@@ -248,8 +249,15 @@ async function generateSchema(components: string[]): Promise {
type: "boolean",
default: true,
},
+ version: {
+ description:
+ "The version of Tailwind CSS to use. \nSee https://flowbite-react.com/docs/customize/config#version for more details.",
+ type: "number",
+ enum: [3, 4],
+ default: 4,
+ },
},
- required: ["components", "dark", "path", "prefix", "rsc", "tsx"],
+ required: ["components", "dark", "path", "prefix", "rsc", "tsx", "version"],
};
defaultSchema.properties.components.items.enum.push(...components);
diff --git a/packages/ui/src/cli/commands/build.ts b/packages/ui/src/cli/commands/build.ts
index 35916958a0..e15a88be52 100644
--- a/packages/ui/src/cli/commands/build.ts
+++ b/packages/ui/src/cli/commands/build.ts
@@ -1,7 +1,49 @@
-import { generateClassList } from "./generate-class-list";
+import fs from "fs/promises";
+import { allowedExtensions, automaticClassGenerationMessage, classListFilePath, excludeDirs } from "../consts";
+import { buildClassList } from "../utils/build-class-list";
+import { extractComponentImports } from "../utils/extract-component-imports";
+import { findFiles } from "../utils/find-files";
+import { getConfig } from "../utils/get-config";
+import { setupInit } from "./setup-init";
import { setupOutputDirectory } from "./setup-output-directory";
export async function build() {
await setupOutputDirectory();
- await generateClassList();
+
+ try {
+ const config = await getConfig();
+ await setupInit(config);
+
+ const importedComponents: string[] = [];
+
+ if (config.components.length) {
+ console.warn(automaticClassGenerationMessage);
+ } else {
+ const files = await findFiles({
+ patterns: allowedExtensions.map((ext) => `**/*${ext}`),
+ excludeDirs,
+ });
+
+ for (const file of files) {
+ const content = await fs.readFile(file, "utf-8");
+ const components = extractComponentImports(content);
+
+ if (components.length) {
+ importedComponents.push(...components);
+ }
+ }
+ }
+
+ const classList = buildClassList({
+ components: config.components.length ? config.components : [...new Set(importedComponents)],
+ dark: config.dark,
+ prefix: config.prefix,
+ version: config.version,
+ });
+
+ console.log(`Generating ${classListFilePath} file...`);
+ await fs.writeFile(classListFilePath, JSON.stringify(classList, null, 2));
+ } catch (error) {
+ console.error(`Failed to generate ${classListFilePath}:`, error);
+ }
}
diff --git a/packages/ui/src/cli/commands/create.ts b/packages/ui/src/cli/commands/create.ts
index 482ed2c833..fffaa2b52a 100644
--- a/packages/ui/src/cli/commands/create.ts
+++ b/packages/ui/src/cli/commands/create.ts
@@ -2,10 +2,12 @@ import fs from "fs/promises";
import path from "path";
import readline from "readline";
import { getConfig } from "../utils/get-config";
+import { setupInit } from "./setup-init";
export async function create(componentName?: string) {
try {
const config = await getConfig();
+ await setupInit(config);
let finalComponentName = componentName;
@@ -163,7 +165,7 @@ ${formattedName}.displayName = "${formattedName}";`;
// Write the component file
console.log(`Creating component file at ${componentFilePath}...`);
- await fs.writeFile(componentFilePath, componentContent, { flag: "w" });
+ await fs.writeFile(componentFilePath, componentContent);
console.log(`\n✅ Component ${formattedName} created successfully!`);
} catch (error) {
diff --git a/packages/ui/src/cli/commands/dev.ts b/packages/ui/src/cli/commands/dev.ts
index 714ff8e274..d93763ad87 100644
--- a/packages/ui/src/cli/commands/dev.ts
+++ b/packages/ui/src/cli/commands/dev.ts
@@ -8,14 +8,23 @@ import {
classListFilePath,
configFilePath,
excludeDirs,
+ gitIgnoreFilePath,
+ initFilePath,
+ initJsxFilePath,
} from "../consts";
import { buildClassList } from "../utils/build-class-list";
import { extractComponentImports } from "../utils/extract-component-imports";
+import { findFiles } from "../utils/find-files";
import { getClassList } from "../utils/get-class-list";
import { getConfig } from "../utils/get-config";
+import { setupGitIgnore } from "./setup-gitignore";
+import { setupInit } from "./setup-init";
+import { setupOutputDirectory } from "./setup-output-directory";
export async function dev() {
- const config = await getConfig();
+ await setupOutputDirectory();
+ let config = await getConfig();
+ await setupInit(config);
if (config.components.length) {
console.warn(automaticClassGenerationMessage);
@@ -24,6 +33,35 @@ export async function dev() {
const importedComponentsMap: Record = {};
let classList = await getClassList();
+ // initial run
+ const files = await findFiles({
+ patterns: allowedExtensions.map((ext) => `**/*${ext}`),
+ excludeDirs,
+ });
+
+ for (const file of files) {
+ const content = await fs.readFile(file, "utf-8");
+ const componentImports = extractComponentImports(content);
+
+ if (componentImports.length) {
+ importedComponentsMap[file] = componentImports;
+ }
+ }
+
+ const newImportedComponents = [...new Set(Object.values(importedComponentsMap).flat())];
+ const newClassList = buildClassList({
+ components: config.components.length ? config.components : newImportedComponents,
+ dark: config.dark,
+ prefix: config.prefix,
+ version: config.version,
+ });
+
+ if (!isEqual(classList, newClassList)) {
+ classList = newClassList;
+ await fs.writeFile(classListFilePath, JSON.stringify(classList, null, 2));
+ }
+
+ // watch for changes
async function handleChange(path: string, eventName: "change" | "unlink") {
if (eventName === "change") {
const content = await fs.readFile(path, "utf-8");
@@ -41,16 +79,24 @@ export async function dev() {
const newImportedComponents = [...new Set(Object.values(importedComponentsMap).flat())];
- const config = await getConfig();
+ if ([configFilePath, initFilePath, initJsxFilePath].includes(path)) {
+ config = await getConfig();
+ await setupInit(config);
+ }
+ if (path === gitIgnoreFilePath) {
+ await setupGitIgnore();
+ }
+
const newClassList = buildClassList({
components: config.components.length ? config.components : newImportedComponents,
dark: config.dark,
prefix: config.prefix,
+ version: config.version,
});
if (!isEqual(classList, newClassList)) {
classList = newClassList;
- await fs.writeFile(classListFilePath, JSON.stringify(classList, null, 2), { flag: "w" });
+ await fs.writeFile(classListFilePath, JSON.stringify(classList, null, 2));
}
}
@@ -60,10 +106,13 @@ export async function dev() {
return excludeDirs.includes(basename(path));
}
if (stats?.isFile()) {
- return !allowedExtensions.concat(configFilePath).some((ext) => path.endsWith(ext));
+ return !allowedExtensions
+ .concat(configFilePath, gitIgnoreFilePath, initFilePath, initJsxFilePath)
+ .some((ext) => path.endsWith(ext));
}
return false;
},
+ ignoreInitial: true,
});
watcher.on("add", (path) => handleChange(path, "change"));
diff --git a/packages/ui/src/cli/commands/ensure-tailwind.ts b/packages/ui/src/cli/commands/ensure-tailwind.ts
index 28537837b0..f5267e6996 100644
--- a/packages/ui/src/cli/commands/ensure-tailwind.ts
+++ b/packages/ui/src/cli/commands/ensure-tailwind.ts
@@ -1,10 +1,13 @@
import { getPackageJson } from "../utils/get-package-json";
+/**
+ * Requires Tailwind CSS to be installed in the project.
+ */
export async function ensureTailwind() {
const packageJson = await getPackageJson();
- const tailwindVersion = packageJson?.dependencies?.["tailwindcss"] || packageJson?.devDependencies?.["tailwindcss"];
+ const packageName = "tailwindcss";
- if (!tailwindVersion) {
+ if (!(packageJson?.dependencies?.[packageName] || packageJson?.devDependencies?.[packageName])) {
console.error("Install Tailwind CSS first.\n\nSee: https://tailwindcss.com/docs/installation");
process.exit(1);
}
diff --git a/packages/ui/src/cli/commands/generate-class-list.ts b/packages/ui/src/cli/commands/generate-class-list.ts
deleted file mode 100644
index 8318f8a4c9..0000000000
--- a/packages/ui/src/cli/commands/generate-class-list.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import fs from "fs/promises";
-import { allowedExtensions, classListFilePath, excludeDirs } from "../consts";
-import { buildClassList } from "../utils/build-class-list";
-import { extractComponentImports } from "../utils/extract-component-imports";
-import { findFiles } from "../utils/find-files";
-import { getConfig } from "../utils/get-config";
-
-export async function generateClassList() {
- try {
- const config = await getConfig();
-
- if (!config.components.length) {
- const files = await findFiles({
- patterns: allowedExtensions.map((ext) => `**/*${ext}`),
- excludeDirs,
- });
- const importedComponents = new Set();
-
- for (const file of files) {
- const content = await fs.readFile(file, "utf-8");
-
- for (const component of extractComponentImports(content)) {
- importedComponents.add(component);
- }
- }
-
- if (importedComponents.size > 0) {
- config.components = [...importedComponents];
- }
- }
-
- const classList = buildClassList(config);
-
- console.log(`Generating ${classListFilePath} file...`);
- await fs.writeFile(classListFilePath, JSON.stringify(classList, null, 2), { flag: "w" });
- } catch (error) {
- console.error(`Failed to generate ${classListFilePath}:`, error);
- }
-}
diff --git a/packages/ui/src/cli/commands/help.ts b/packages/ui/src/cli/commands/help.ts
index 538b4d8a1d..f370b3c024 100644
--- a/packages/ui/src/cli/commands/help.ts
+++ b/packages/ui/src/cli/commands/help.ts
@@ -10,7 +10,6 @@ Commands:
init Initialize Flowbite React with config files and necessary setup
install Install Flowbite React using your detected package manager
migrate Run code transformations to update to latest patterns and APIs
- patch Patch Tailwind CSS to expose version number for compatibility
register Register Flowbite React process for development with automatic class generation
setup Setup additional features and configurations
diff --git a/packages/ui/src/cli/commands/init.ts b/packages/ui/src/cli/commands/init.ts
index 02d5ee5486..2227c1a29e 100644
--- a/packages/ui/src/cli/commands/init.ts
+++ b/packages/ui/src/cli/commands/init.ts
@@ -1,11 +1,10 @@
import { ensureTailwind } from "./ensure-tailwind";
-import { installFlowbiteReact } from "./install";
-import { patchTailwind } from "./patch";
+import { installPackage } from "./install";
import { setupClassList } from "./setup-class-list";
import { setupConfig } from "./setup-config";
import { setupGitIgnore } from "./setup-gitignore";
+import { setupInit } from "./setup-init";
import { setupOutputDirectory } from "./setup-output-directory";
-import { setupPatch } from "./setup-patch";
import { setupPlugin } from "./setup-plugin";
import { setupRegister } from "./setup-register";
import { setupTailwind } from "./setup-tailwind";
@@ -13,44 +12,19 @@ import { setupVSCode } from "./setup-vscode";
export async function init() {
try {
- // require `tailwindcss`
await ensureTailwind();
-
- // patch `tailwindcss`
- await patchTailwind();
-
- // install `flowbite-react`
- await installFlowbiteReact();
-
- // setup patch script in `package.json`
- await setupPatch();
-
- // setup `tailwindcss`
- await setupTailwind();
-
- // setup `.flowbite-react` directory
+ await installPackage();
await setupOutputDirectory();
-
- // setup `.flowbite-react/class-list.json` file
- await setupClassList();
-
- // setup `.flowbite-react/config.json` file
- await setupConfig();
-
- // setup `.flowbite-react/.gitignore` file
await setupGitIgnore();
-
- // setup VSCode intellisense
+ await setupClassList();
+ const config = await setupConfig();
+ await setupInit(config);
await setupVSCode();
-
- // setup plugin based on bundler
+ await setupTailwind();
const hasBundler = await setupPlugin();
-
if (!hasBundler) {
- // setup register script in `package.json`
await setupRegister();
}
-
console.log("\n✅ Flowbite React has been successfully initialized!");
} catch (error) {
console.error("An error occurred during initialization:", error);
diff --git a/packages/ui/src/cli/commands/install.ts b/packages/ui/src/cli/commands/install.ts
index d8a39574c8..61539bca8c 100644
--- a/packages/ui/src/cli/commands/install.ts
+++ b/packages/ui/src/cli/commands/install.ts
@@ -1,16 +1,16 @@
import { resolveCommand } from "package-manager-detector/commands";
import { detect } from "package-manager-detector/detect";
import { execCommand } from "../utils/exec-command";
+import { getModulePackageJson } from "../utils/get-module-package-json";
import { getPackageJson } from "../utils/get-package-json";
-export async function installFlowbiteReact() {
- try {
- const packageJson = await getPackageJson();
-
- if (packageJson.dependencies?.["flowbite-react"] || packageJson.devDependencies?.["flowbite-react"]) {
- return;
- }
+/**
+ * Installs `flowbite-react` package using the detected package manager.
+ */
+export async function installPackage() {
+ const packageName = "flowbite-react";
+ try {
let pm = await detect();
if (!pm) {
@@ -19,12 +19,26 @@ export async function installFlowbiteReact() {
pm ??= { agent: "npm", name: "npm" };
- const packageName = "flowbite-react";
+ const packageJson = await getPackageJson();
+ const currentPackage = await getModulePackageJson(packageName);
+
+ if (currentPackage && (packageJson?.dependencies?.[packageName] || packageJson?.devDependencies?.[packageName])) {
+ if (currentPackage.version.localeCompare("0.11", undefined, { numeric: true }) < 0) {
+ console.log(
+ "The current version of flowbite-react is below 0.11.x, which is the version with the new engine and CLI.",
+ );
+ const { command = "", args } = resolveCommand(pm.agent, "add", [`${packageName}@latest`]) ?? {};
+ console.log(`Updating ${packageName} to latest version using ${pm.name}...`);
+ await execCommand(command, args);
+ }
+ return;
+ }
+
const { command = "", args } = resolveCommand(pm.agent, "add", [packageName]) ?? {};
console.log(`Installing ${packageName} using ${pm.name}...`);
- execCommand(command, args);
+ await execCommand(command, args);
} catch (error) {
- console.error("Failed to install flowbite-react:", error);
+ console.error(`Failed to install ${packageName}:`, error);
}
}
diff --git a/packages/ui/src/cli/commands/patch.ts b/packages/ui/src/cli/commands/patch.ts
deleted file mode 100644
index e2945048b4..0000000000
--- a/packages/ui/src/cli/commands/patch.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import fs from "fs/promises";
-import path from "path";
-
-/**
- * Patches Tailwind CSS installation to ensure version files exist and are correctly configured.
- *
- * This function:
- * - Resolves the Tailwind CSS module path
- * - Reads the Tailwind package.json to get the actual version
- * - Creates or updates version files (version.js, version.mjs, version.d.ts, version.d.mts)
- * - Updates package.json exports if necessary
- *
- * @returns {Promise} A promise that resolves when patching is complete
- */
-export async function patchTailwind(): Promise {
- try {
- let tailwindPath: string | undefined;
-
- try {
- let tailwindModulePath;
- if (typeof require !== "undefined") {
- tailwindModulePath = require.resolve("tailwindcss/package.json", {
- paths: [process.cwd()],
- });
- tailwindPath = path.resolve(path.dirname(tailwindModulePath));
- } else {
- const { createRequire } = await import("module");
- const require = createRequire(import.meta.url);
- tailwindModulePath = require.resolve("tailwindcss/package.json", {
- paths: [process.cwd()],
- });
- tailwindPath = path.resolve(path.dirname(tailwindModulePath));
- }
- } catch {
- console.warn("Could not resolve Tailwind CSS module path. Skipping version patch.");
- return;
- }
-
- const tailwindPackageJsonPath = path.join(tailwindPath, "package.json");
- let tailwindPackageJson: {
- version: string;
- exports?: Record;
- };
-
- try {
- const packageJsonContent = await fs.readFile(tailwindPackageJsonPath, "utf-8");
- tailwindPackageJson = JSON.parse(packageJsonContent);
- } catch {
- console.warn("Could not read Tailwind CSS `package.json`. Skipping version patch.");
- return;
- }
-
- const actualVersion = tailwindPackageJson.version;
-
- // Check if version files exist and have the correct version
- const versionFilePath = path.join(tailwindPath, "version.js");
- const versionMjsFilePath = path.join(tailwindPath, "version.mjs");
- const versionDtsFilePath = path.join(tailwindPath, "version.d.ts");
- const versionDmtsFilePath = path.join(tailwindPath, "version.d.mts");
-
- let patchesApplied = false;
-
- // create `version.js`, `version.mjs`, `version.d.ts` and `version.d.mts` files if needed
- try {
- let filesCreated = false;
-
- // Check and create `version.js` file (CJS)
- if (await shouldUpdateFile(versionFilePath, actualVersion)) {
- const versionContent = `"use strict";\n\nconst version = "${actualVersion}";\nmodule.exports = version;\n`;
- await fs.writeFile(versionFilePath, versionContent, "utf-8");
- filesCreated = true;
- }
-
- // Check and create `version.mjs` file (ESM)
- if (await shouldUpdateFile(versionMjsFilePath, actualVersion)) {
- const versionMjsContent = `const version = "${actualVersion}";\nexport default version;\n`;
- await fs.writeFile(versionMjsFilePath, versionMjsContent, "utf-8");
- filesCreated = true;
- }
-
- // Check and create `version.d.ts` file
- if (await shouldUpdateFile(versionDtsFilePath)) {
- const versionDtsContent = `declare const version: string;\nexport = version;\n`;
- await fs.writeFile(versionDtsFilePath, versionDtsContent, "utf-8");
- filesCreated = true;
- }
-
- // Check and create `version.d.mts` file
- if (await shouldUpdateFile(versionDmtsFilePath)) {
- const versionDmtsContent = `declare const version: string;\nexport default version;\n`;
- await fs.writeFile(versionDmtsFilePath, versionDmtsContent, "utf-8");
- filesCreated = true;
- }
-
- if (filesCreated) {
- patchesApplied = true;
- }
- } catch {
- console.warn("Could not create Tailwind CSS version files. Skipping version patch.");
- }
-
- // patch package.json.exports
- try {
- if (tailwindPackageJson.exports) {
- if (!tailwindPackageJson.exports["./version"] || !tailwindPackageJson.exports["./version.js"]) {
- tailwindPackageJson.exports = {
- ...tailwindPackageJson.exports,
- "./version": {
- require: "./version.js",
- import: "./version.mjs",
- },
- "./version.js": {
- require: "./version.js",
- import: "./version.mjs",
- },
- };
- await fs.writeFile(tailwindPackageJsonPath, JSON.stringify(tailwindPackageJson, null, 2), "utf-8");
- patchesApplied = true;
- }
- }
- } catch {
- console.warn("Could not patch Tailwind CSS `package.json.exports`. Skipping version patch.");
- }
-
- if (patchesApplied) {
- console.log("Patched Tailwind CSS");
- }
- } catch (error) {
- console.error("Failed to patch Tailwind CSS:", error);
- }
-}
-
-/**
- * Determines whether a file should be updated based on its existence and content.
- *
- * @param filePath - The path to the file to check
- * @param actualVersion - Optional version string to check for in the file content
- * @returns {Promise} True if the file doesn't exist or doesn't contain the actual version
- */
-async function shouldUpdateFile(filePath: string, actualVersion?: string): Promise {
- try {
- const content = await fs.readFile(filePath, "utf-8");
- if (actualVersion !== undefined) {
- return !content.includes(actualVersion);
- }
- return false;
- } catch {
- // File doesn't exist
- return true;
- }
-}
diff --git a/packages/ui/src/cli/commands/register.ts b/packages/ui/src/cli/commands/register.ts
index 67ca0b4965..db9bc72269 100644
--- a/packages/ui/src/cli/commands/register.ts
+++ b/packages/ui/src/cli/commands/register.ts
@@ -3,10 +3,13 @@ import fs from "fs/promises";
import path from "path";
import { automaticClassGenerationMessage, outputDir, processIdFile } from "../consts";
import { getConfig } from "../utils/get-config";
+import { setupInit } from "./setup-init";
import { setupOutputDirectory } from "./setup-output-directory";
export async function register() {
+ await setupOutputDirectory();
const config = await getConfig();
+ await setupInit(config);
if (config.components.length) {
console.warn(automaticClassGenerationMessage);
@@ -31,10 +34,8 @@ export async function register() {
shell: true,
});
- await setupOutputDirectory();
-
if (devProcess.pid) {
- await fs.writeFile(processIdFilePath, devProcess.pid.toString(), { flag: "w" });
+ await fs.writeFile(processIdFilePath, devProcess.pid.toString());
}
} catch (error) {
console.error("Failed to register flowbite-react", error);
diff --git a/packages/ui/src/cli/commands/setup-class-list.ts b/packages/ui/src/cli/commands/setup-class-list.ts
index b77448320d..a2ac66b76d 100644
--- a/packages/ui/src/cli/commands/setup-class-list.ts
+++ b/packages/ui/src/cli/commands/setup-class-list.ts
@@ -1,11 +1,16 @@
import fs from "fs/promises";
import { classListFilePath } from "../consts";
+/**
+ * Sets up the `.flowbite-react/class-list.json` file in the project.
+ *
+ * This function checks if the `.flowbite-react/class-list.json` file exists and creates it if it does not.
+ */
export async function setupClassList() {
try {
await fs.access(classListFilePath);
} catch {
console.log(`Creating ${classListFilePath} file...`);
- await fs.writeFile(classListFilePath, JSON.stringify([], null, 2), { flag: "w" });
+ await fs.writeFile(classListFilePath, JSON.stringify([], null, 2));
}
}
diff --git a/packages/ui/src/cli/commands/setup-config.ts b/packages/ui/src/cli/commands/setup-config.ts
index 7e40e18d0f..b4572e89d3 100644
--- a/packages/ui/src/cli/commands/setup-config.ts
+++ b/packages/ui/src/cli/commands/setup-config.ts
@@ -1,21 +1,129 @@
import fs from "fs/promises";
+import { klona } from "klona/json";
+import { isEqual } from "../../helpers/is-equal";
+import { COMPONENT_TO_CLASS_LIST_MAP } from "../../metadata/class-list";
import { configFilePath } from "../consts";
-import type { Config } from "../utils/get-config";
+import { getTailwindVersion } from "../utils/get-tailwind-version";
+
+export interface Config {
+ $schema: string;
+ components: string[];
+ dark: boolean;
+ path: string;
+ prefix: string;
+ rsc: boolean;
+ tsx: boolean;
+ version: 3 | 4;
+}
+
+/**
+ * Sets up the `.flowbite-react/config.json` file in the project.
+ *
+ * This function creates or updates the configuration file with default values and validates existing configurations.
+ */
+export async function setupConfig(): Promise {
+ const defaultConfig: Config = {
+ $schema: "https://unpkg.com/flowbite-react/schema.json",
+ components: [],
+ dark: true,
+ path: "src/components",
+ // TODO: infer from project
+ prefix: "",
+ rsc: true,
+ tsx: true,
+ version: await getTailwindVersion(),
+ };
+ const writeTimeout = 10;
-export async function setupConfig() {
try {
- await fs.access(configFilePath);
- } catch {
- const defaultConfig: Config = {
- $schema: "https://unpkg.com/flowbite-react/schema.json",
- components: [],
- dark: true,
- prefix: "",
- path: "src/components",
- tsx: true,
- rsc: true,
- };
- console.log(`Creating ${configFilePath} file...`);
- await fs.writeFile(configFilePath, JSON.stringify(defaultConfig, null, 2), { flag: "w" });
+ const raw = await fs.readFile(configFilePath, "utf-8");
+ const config: Config = JSON.parse(raw);
+ let newConfig = klona(config);
+
+ if (newConfig.$schema !== defaultConfig.$schema) {
+ console.warn(`Invalid $schema in ${configFilePath}: ${newConfig.$schema}`);
+ newConfig.$schema = defaultConfig.$schema;
+ }
+ if (!Array.isArray(newConfig.components)) {
+ console.warn(`Invalid components in ${configFilePath}: ${newConfig.components}`);
+ newConfig.components = [...defaultConfig.components];
+ } else {
+ const isValidComponent = (component: unknown) =>
+ typeof component === "string" &&
+ component.trim() !== "" &&
+ (component === "*" ||
+ COMPONENT_TO_CLASS_LIST_MAP[component as keyof typeof COMPONENT_TO_CLASS_LIST_MAP] !== undefined);
+
+ if (newConfig.components.some((component) => !isValidComponent(component))) {
+ console.warn(`Invalid components in ${configFilePath}: ${newConfig.components}`);
+ newConfig.components = newConfig.components.filter(isValidComponent);
+ }
+ }
+ if (typeof newConfig.dark !== "boolean") {
+ console.warn(`Invalid dark in ${configFilePath}: ${newConfig.dark}`);
+ newConfig.dark = defaultConfig.dark;
+ }
+ if (typeof newConfig.path !== "string") {
+ console.warn(`Invalid path in ${configFilePath}: ${newConfig.path}`);
+ newConfig.path = defaultConfig.path;
+ }
+ if (typeof newConfig.prefix !== "string") {
+ console.warn(`Invalid prefix in ${configFilePath}: ${newConfig.prefix}`);
+ newConfig.prefix = defaultConfig.prefix;
+ }
+ if (typeof newConfig.rsc !== "boolean") {
+ console.warn(`Invalid rsc in ${configFilePath}: ${newConfig.rsc}`);
+ newConfig.rsc = defaultConfig.rsc;
+ }
+ if (typeof newConfig.tsx !== "boolean") {
+ console.warn(`Invalid tsx in ${configFilePath}: ${newConfig.tsx}`);
+ newConfig.tsx = defaultConfig.tsx;
+ }
+ if (newConfig.version !== defaultConfig.version) {
+ console.warn(`Invalid version in ${configFilePath}: ${newConfig.version} (detected: ${defaultConfig.version})`);
+ newConfig.version = defaultConfig.version;
+ }
+
+ for (const key in newConfig) {
+ if (!(key in defaultConfig)) {
+ console.warn(`Invalid property in ${configFilePath}: ${key}`);
+ delete newConfig[key as keyof Config];
+ }
+ }
+
+ const isSorted = isEqual(Object.keys(newConfig).sort(), Object.keys(newConfig));
+ if (!isSorted) {
+ console.warn(`Invalid keys order in ${configFilePath}`);
+ newConfig = Object.fromEntries(Object.entries(newConfig).sort()) as Config;
+ }
+
+ if (!isEqual(config, newConfig) || !isSorted) {
+ console.log(`Updating ${configFilePath} file...`);
+ setTimeout(() => fs.writeFile(configFilePath, JSON.stringify(newConfig, null, 2)), writeTimeout);
+ }
+
+ if (
+ newConfig.dark !== defaultConfig.dark ||
+ newConfig.prefix !== defaultConfig.prefix ||
+ newConfig.version !== defaultConfig.version
+ ) {
+ // TODO: search for in the project and warn if it's not found
+ console.info(
+ `\n[!] Custom values detected in ${configFilePath}, render at root level of your app to sync runtime with node config values.`,
+ `\n[!] Otherwise, your app will use the default values instead of your custom configuration.`,
+ `\n[!] Example: In case of custom 'prefix' or 'version', the app will not display the correct class names.`,
+ );
+ }
+
+ return newConfig;
+ } catch (error) {
+ if (error instanceof Error && error.message.includes("ENOENT")) {
+ console.log(`Creating ${configFilePath} file...`);
+ } else {
+ console.error(`Invalid ${configFilePath} file, regenerating...`);
+ }
+
+ setTimeout(() => fs.writeFile(configFilePath, JSON.stringify(defaultConfig, null, 2)), writeTimeout);
+ return defaultConfig;
}
}
diff --git a/packages/ui/src/cli/commands/setup-gitignore.ts b/packages/ui/src/cli/commands/setup-gitignore.ts
index 237b1f8fe9..9dd0ba092f 100644
--- a/packages/ui/src/cli/commands/setup-gitignore.ts
+++ b/packages/ui/src/cli/commands/setup-gitignore.ts
@@ -1,19 +1,26 @@
import fs from "fs/promises";
-import path from "path";
-import { classListFile, outputDir, processIdFile } from "../consts";
+import { classListFile, gitIgnoreFilePath, processIdFile } from "../consts";
+/**
+ * Sets up the `.flowbite-react/.gitignore` file in the project.
+ *
+ * This function ensures the `.gitignore` file exists in the `.flowbite-react` directory.
+ * It will create or update the file if needed.
+ */
export async function setupGitIgnore() {
- const gitIgnoreFilePath = path.join(outputDir, ".gitignore");
+ const content = `${classListFile}\n${processIdFile}`;
try {
- const gitignore = await fs.readFile(gitIgnoreFilePath, "utf-8").catch(() => {
- console.log(`Creating ${gitIgnoreFilePath} file...`);
- return "";
- });
+ let currentContent: string;
+ try {
+ currentContent = await fs.readFile(gitIgnoreFilePath, "utf-8");
+ } catch {
+ currentContent = "";
+ }
- if (![classListFile, processIdFile].some((file) => gitignore.includes(file))) {
- console.log(`Adding ${classListFile}, ${processIdFile} to ${gitIgnoreFilePath}...`);
- await fs.writeFile(gitIgnoreFilePath, `${classListFile}\n${processIdFile}`, { flag: "w" });
+ if (currentContent.trimEnd() !== content) {
+ console.log(`${currentContent ? "Updating" : "Creating"} ${gitIgnoreFilePath} file...`);
+ setTimeout(() => fs.writeFile(gitIgnoreFilePath, content), 10);
}
} catch (error) {
console.error(`Failed to update ${gitIgnoreFilePath}:`, error);
diff --git a/packages/ui/src/cli/commands/setup-init.ts b/packages/ui/src/cli/commands/setup-init.ts
new file mode 100644
index 0000000000..f42370babe
--- /dev/null
+++ b/packages/ui/src/cli/commands/setup-init.ts
@@ -0,0 +1,104 @@
+import fs from "fs/promises";
+import { parse } from "recast";
+import { initFilePath, initJsxFilePath } from "../consts";
+import type { Config } from "./setup-config";
+
+/**
+ * Sets up the `.flowbite-react/init.tsx` file in the project.
+ *
+ * This function ensures the init.tsx file exists and is up-to-date with the current configuration.
+ * It will create or update the file if needed.
+ */
+export async function setupInit(config: Config) {
+ const content = `
+/* eslint-disable */
+// @ts-nocheck
+// biome-ignore-all lint: auto-generated file
+
+// This file is auto-generated by the flowbite-react CLI.
+// Do not edit this file directly.
+// Instead, edit the .flowbite-react/config.json file.
+
+import { StoreInit } from "flowbite-react/store/init";
+import React from "react";
+
+export const config = {
+ dark: ${config.dark},
+ prefix: "${config.prefix}",
+ version: ${config.version},
+};
+
+export function ThemeInit() {
+ return ;
+}
+
+ThemeInit.displayName = "ThemeInit";
+`.trim();
+
+ const targetPath = config.tsx ? initFilePath : initJsxFilePath;
+ const oldPath = config.tsx ? initJsxFilePath : initFilePath;
+
+ try {
+ let currentContent = "";
+ try {
+ currentContent = await fs.readFile(targetPath, "utf-8");
+ } catch {
+ console.log(`Creating ${targetPath} file...`);
+ setTimeout(() => fs.writeFile(targetPath, content), 10);
+ }
+
+ if (currentContent) {
+ const currentAst = parse(currentContent);
+ const newAst = parse(content);
+
+ if (!compareNodes(currentAst.program, newAst.program)) {
+ console.log(`Updating ${targetPath} file...`);
+ setTimeout(() => fs.writeFile(targetPath, content), 10);
+ }
+ }
+
+ try {
+ await fs.access(oldPath);
+ console.log(`Removing ${oldPath} file...`);
+ await fs.unlink(oldPath);
+ } catch {
+ // noop
+ }
+ } catch (error) {
+ console.error(`Failed to update ${targetPath}:`, error);
+ }
+}
+
+/**
+ * Compare two AST nodes ignoring location info and comments
+ */
+function compareNodes(a: unknown, b: unknown): boolean {
+ if (a === b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ if (Array.isArray(a)) {
+ if (!Array.isArray(b) || a.length !== b.length) {
+ return false;
+ }
+ return a.every((item, i) => compareNodes(item, b[i]));
+ }
+ if (typeof a !== "object" || typeof b !== "object") {
+ return a === b;
+ }
+
+ // Skip location and comment-related properties
+ const keysA = Object.keys(a).filter(
+ (k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
+ );
+ const keysB = Object.keys(b).filter(
+ (k) => !["start", "end", "loc", "range", "tokens", "comments", "leadingComments", "trailingComments"].includes(k),
+ );
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+ return keysA.every((key) => compareNodes(a[key as keyof typeof a], b[key as keyof typeof b]));
+}
diff --git a/packages/ui/src/cli/commands/setup-output-directory.ts b/packages/ui/src/cli/commands/setup-output-directory.ts
index 6136d11eb2..1cb0803462 100644
--- a/packages/ui/src/cli/commands/setup-output-directory.ts
+++ b/packages/ui/src/cli/commands/setup-output-directory.ts
@@ -1,6 +1,11 @@
import fs from "fs/promises";
import { outputDir } from "../consts";
+/**
+ * Sets up the `.flowbite-react` directory in the project.
+ *
+ * This function checks if the `.flowbite-react` directory exists and creates it if it does not.
+ */
export async function setupOutputDirectory() {
try {
await fs.access(outputDir);
diff --git a/packages/ui/src/cli/commands/setup-patch.ts b/packages/ui/src/cli/commands/setup-patch.ts
deleted file mode 100644
index d13f08c39b..0000000000
--- a/packages/ui/src/cli/commands/setup-patch.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import fs from "fs/promises";
-import cjson from "comment-json";
-import { packageJsonFile } from "../consts";
-import { getPackageJson } from "../utils/get-package-json";
-
-export async function setupPatch() {
- try {
- const patchCommand = "flowbite-react patch";
- const packageJson = await getPackageJson();
-
- if (!packageJson.scripts) {
- packageJson.scripts = {};
- }
-
- if (!packageJson.scripts.postinstall?.includes(patchCommand)) {
- console.log(`Adding postinstall patch script to ${packageJsonFile}...`);
- if (packageJson.scripts.postinstall) {
- packageJson.scripts.postinstall += ` && ${patchCommand}`;
- } else {
- packageJson.scripts.postinstall = patchCommand;
- }
- await fs.writeFile(packageJsonFile, cjson.stringify(packageJson, null, 2), { flag: "w" });
- }
- } catch (error) {
- console.error(`Failed to setup ${packageJsonFile}:`, error);
- }
-}
diff --git a/packages/ui/src/cli/commands/setup-plugin.ts b/packages/ui/src/cli/commands/setup-plugin.ts
index 2772777103..73cd8573b8 100644
--- a/packages/ui/src/cli/commands/setup-plugin.ts
+++ b/packages/ui/src/cli/commands/setup-plugin.ts
@@ -1,5 +1,11 @@
import fs from "fs/promises";
+/**
+ * Sets up the plugin for the project based on the bundler.
+ *
+ * This function checks for the existence of configuration files for various bundlers and frameworks
+ * and sets up the appropriate plugin for each.
+ */
export async function setupPlugin() {
const configFileMap = {
astro: ["astro.config.cjs", "astro.config.mjs", "astro.config.ts", "astro.config.js"],
diff --git a/packages/ui/src/cli/commands/setup-register.ts b/packages/ui/src/cli/commands/setup-register.ts
index 7afa569b8e..39782745cf 100644
--- a/packages/ui/src/cli/commands/setup-register.ts
+++ b/packages/ui/src/cli/commands/setup-register.ts
@@ -3,6 +3,13 @@ import cjson from "comment-json";
import { packageJsonFile } from "../consts";
import { getPackageJson } from "../utils/get-package-json";
+/**
+ * Sets up the register script in the project's package.json file.
+ *
+ * This function checks if the postinstall script already exists in the package.json file.
+ * If it does not exist, it adds the register command to the postinstall script.
+ * If it does exist, it appends the register command to the existing postinstall script.
+ */
export async function setupRegister() {
try {
const registerCommand = "flowbite-react register";
@@ -19,7 +26,7 @@ export async function setupRegister() {
} else {
packageJson.scripts.postinstall = registerCommand;
}
- await fs.writeFile(packageJsonFile, cjson.stringify(packageJson, null, 2), { flag: "w" });
+ await fs.writeFile(packageJsonFile, cjson.stringify(packageJson, null, 2));
}
} catch (error) {
console.error(`Failed to setup ${packageJsonFile}:`, error);
diff --git a/packages/ui/src/cli/commands/setup-tailwind.ts b/packages/ui/src/cli/commands/setup-tailwind.ts
index 09acd52c50..05dca47b85 100644
--- a/packages/ui/src/cli/commands/setup-tailwind.ts
+++ b/packages/ui/src/cli/commands/setup-tailwind.ts
@@ -6,6 +6,12 @@ import { addToConfig } from "../utils/add-to-config";
import { findFiles } from "../utils/find-files";
import { joinNormalizedPath } from "../utils/normalize-path";
+/**
+ * Sets up Tailwind CSS configuration for the project.
+ *
+ * This function checks if Tailwind CSS is installed in the project and then
+ * attempts to add the necessary configuration for Tailwind CSS v4 or v3.
+ */
export async function setupTailwind() {
try {
const found = !!((await setupTailwindV4()) || (await setupTailwindV3()));
@@ -18,6 +24,12 @@ export async function setupTailwind() {
}
}
+/**
+ * Sets up Tailwind CSS v4 configuration for the project.
+ *
+ * This function searches for Tailwind CSS files in the project and attempts to
+ * add the necessary configuration for Tailwind CSS v4.
+ */
async function setupTailwindV4() {
try {
const cssFiles = await findFiles({
diff --git a/packages/ui/src/cli/commands/setup-vscode.ts b/packages/ui/src/cli/commands/setup-vscode.ts
index d8fe2d514f..fd7f7783c6 100644
--- a/packages/ui/src/cli/commands/setup-vscode.ts
+++ b/packages/ui/src/cli/commands/setup-vscode.ts
@@ -3,6 +3,12 @@ import path from "path";
import cjson from "comment-json";
import { vscodeDir } from "../consts";
+/**
+ * Sets up the VSCode configuration for the project.
+ *
+ * This function checks if the `.vscode` directory exists and creates it if it does not.
+ * It then sets up the `settings.json` and `extensions.json` files with the necessary configuration for Flowbite React.
+ */
export async function setupVSCode() {
try {
await fs.access(vscodeDir);
@@ -15,6 +21,12 @@ export async function setupVSCode() {
await setupVSCodeExtensions();
}
+/**
+ * Sets up the VSCode settings for the project.
+ *
+ * This function checks if the `settings.json` file exists and creates it if it does not.
+ * It then sets up the `files.associations`, `tailwindCSS.classAttributes`, and `tailwindCSS.experimental.classRegex` settings.
+ */
async function setupVSCodeSettings() {
try {
const vscodeSettingsFilePath = path.join(vscodeDir, "settings.json");
@@ -86,7 +98,7 @@ async function setupVSCodeSettings() {
}
console.log(`${exists ? "Updating" : "Creating"} ${vscodeSettingsFilePath} with flowbite-react configuration...`);
- await fs.writeFile(vscodeSettingsFilePath, cjson.stringify(settings, null, 2), { flag: "w" });
+ await fs.writeFile(vscodeSettingsFilePath, cjson.stringify(settings, null, 2));
} catch (error) {
console.error("Failed to setup VSCode settings:", error);
}
@@ -134,7 +146,7 @@ async function setupVSCodeExtensions() {
}
console.log(`${exists ? "Updating" : "Creating"} ${vscodeExtensionsFilePath} with flowbite-react configuration...`);
- await fs.writeFile(vscodeExtensionsFilePath, cjson.stringify(extensions, null, 2), { flag: "w" });
+ await fs.writeFile(vscodeExtensionsFilePath, cjson.stringify(extensions, null, 2));
} catch (error) {
console.error("Failed to setup VSCode extensions:", error);
}
diff --git a/packages/ui/src/cli/consts.ts b/packages/ui/src/cli/consts.ts
index 78f09af906..fd5e29900d 100644
--- a/packages/ui/src/cli/consts.ts
+++ b/packages/ui/src/cli/consts.ts
@@ -1,16 +1,21 @@
import path from "path";
-export const pluginName = "flowbiteReact";
-export const pluginPath = "flowbite-react/plugin";
export const classListFile = "class-list.json";
export const configFile = "config.json";
+export const gitIgnoreFile = ".gitignore";
+export const initFile = "init";
export const outputDir = ".flowbite-react";
export const packageJsonFile = "package.json";
+export const pluginName = "flowbiteReact";
+export const pluginPath = "flowbite-react/plugin";
export const processIdFile = "pid";
export const vscodeDir = ".vscode";
export const classListFilePath = path.join(outputDir, classListFile);
export const configFilePath = path.join(outputDir, configFile);
+export const gitIgnoreFilePath = path.join(outputDir, gitIgnoreFile);
+export const initFilePath = path.join(outputDir, `${initFile}.tsx`);
+export const initJsxFilePath = path.join(outputDir, `${initFile}.jsx`);
export const excludeDirs = [
".astro",
diff --git a/packages/ui/src/cli/main.ts b/packages/ui/src/cli/main.ts
index c30e361073..d4de29320a 100644
--- a/packages/ui/src/cli/main.ts
+++ b/packages/ui/src/cli/main.ts
@@ -23,17 +23,13 @@ export async function main(argv: string[]) {
await init();
}
if (command === "install") {
- const { installFlowbiteReact } = await import("./commands/install");
- await installFlowbiteReact();
+ const { installPackage } = await import("./commands/install");
+ await installPackage();
}
if (command === "migrate") {
const { migrate } = await import("./commands/migrate");
await migrate();
}
- if (command === "patch") {
- const { patchTailwind } = await import("./commands/patch");
- await patchTailwind();
- }
if (command === "register") {
const { register } = await import("./commands/register");
await register();
@@ -54,19 +50,7 @@ export async function main(argv: string[]) {
}
if (
- ![
- "build",
- "create",
- "dev",
- "help",
- "--help",
- "init",
- "install",
- "migrate",
- "patch",
- "register",
- "setup",
- ].includes(command)
+ !["build", "create", "dev", "help", "--help", "init", "install", "migrate", "register", "setup"].includes(command)
) {
console.error(`Unknown command: ${command}`);
const { help } = await import("./commands/help");
diff --git a/packages/ui/src/cli/utils/build-class-list.ts b/packages/ui/src/cli/utils/build-class-list.ts
index fd597ccf37..52b001253b 100644
--- a/packages/ui/src/cli/utils/build-class-list.ts
+++ b/packages/ui/src/cli/utils/build-class-list.ts
@@ -1,7 +1,6 @@
import { applyPrefix } from "../../helpers/apply-prefix";
import { applyPrefixV3 } from "../../helpers/apply-prefix-v3";
import { convertUtilitiesToV4 } from "../../helpers/convert-utilities-to-v4";
-import { getTailwindVersion } from "../../helpers/get-tailwind-version";
import { stripDark } from "../../helpers/strip-dark";
import { CLASS_LIST_MAP, COMPONENT_TO_CLASS_LIST_MAP } from "../../metadata/class-list";
import { DEPENDENCY_LIST_MAP } from "../../metadata/dependency-list";
@@ -13,19 +12,20 @@ import { DEPENDENCY_LIST_MAP } from "../../metadata/dependency-list";
* @param {string[]} options.components - The components to include in the class list.
* @param {boolean} options.dark - Whether to include dark mode classes.
* @param {string} options.prefix - The prefix to apply to the class names.
+ * @param {3 | 4} options.version - The version of Tailwind CSS to use.
* @returns {string[]} The sorted list of CSS classes.
*/
export function buildClassList({
components,
dark,
prefix,
+ version,
}: {
components: string[];
dark: boolean;
prefix: string;
+ version: 3 | 4;
}): string[] {
- const version = getTailwindVersion();
-
let classList: string[] = [];
if (components.includes("*")) {
diff --git a/packages/ui/src/cli/utils/get-config.ts b/packages/ui/src/cli/utils/get-config.ts
index 9959e26e86..3cff520517 100644
--- a/packages/ui/src/cli/utils/get-config.ts
+++ b/packages/ui/src/cli/utils/get-config.ts
@@ -1,63 +1,15 @@
-import fs from "fs/promises";
-import { configFilePath } from "../consts";
-
-export interface Config {
- $schema: string;
- components: string[];
- dark: boolean;
- path: string;
- prefix: string;
- rsc: boolean;
- tsx: boolean;
-}
+import { setupConfig, type Config } from "../commands/setup-config";
/**
- * Reads the configuration file and returns its content as a Config object.
+ * Gets the current configuration by reading and validating the config file.
*
- * This function attempts to read the file specified by `configFilePath`, parse its content as JSON,
- * and return the parsed configuration. If the file cannot be read or parsed, a default Config object is returned.
+ * This function calls `setupConfig()` which will:
+ * - Create the config file if it doesn't exist
+ * - Validate and fix any invalid config values
+ * - Return the parsed configuration
*
- * @returns {Promise} A promise that resolves to a Config object representing the configuration.
+ * @returns {Promise} A promise that resolves to the validated configuration object
*/
export async function getConfig(): Promise {
- const config: Config = {
- $schema: "",
- components: [],
- dark: true,
- path: "src/components",
- prefix: "",
- rsc: true,
- tsx: true,
- };
-
- try {
- const raw = await fs.readFile(configFilePath, "utf-8");
- const parsed: Config = JSON.parse(raw);
-
- if (parsed.$schema !== undefined && typeof parsed.$schema === "string") {
- config.$schema = parsed.$schema;
- }
- if (parsed.components !== undefined && Array.isArray(parsed.components)) {
- config.components = parsed.components.map((component) => component.trim()).filter(Boolean);
- }
- if (parsed.dark !== undefined && typeof parsed.dark === "boolean") {
- config.dark = parsed.dark;
- }
- if (parsed.path !== undefined && typeof parsed.path === "string") {
- config.path = parsed.path;
- }
- if (parsed.prefix !== undefined && typeof parsed.prefix === "string") {
- config.prefix = parsed.prefix;
- }
- if (parsed.rsc !== undefined && typeof parsed.rsc === "boolean") {
- config.rsc = parsed.rsc;
- }
- if (parsed.tsx !== undefined && typeof parsed.tsx === "boolean") {
- config.tsx = parsed.tsx;
- }
-
- return config;
- } catch {
- return config;
- }
+ return await setupConfig();
}
diff --git a/packages/ui/src/cli/utils/get-module-package-json.ts b/packages/ui/src/cli/utils/get-module-package-json.ts
new file mode 100644
index 0000000000..45a28e913a
--- /dev/null
+++ b/packages/ui/src/cli/utils/get-module-package-json.ts
@@ -0,0 +1,22 @@
+import { createRequire } from "module";
+
+/**
+ * Gets the package.json contents for a given package.
+ *
+ * @param packageName - The name of the package to get the package.json from
+ * @returns The contents of the package.json file or null if the package is not installed
+ */
+export async function getModulePackageJson>(
+ packageName: string,
+): Promise {
+ try {
+ const require = createRequire(import.meta.url);
+ return require(`${packageName}/package.json`);
+ } catch {
+ try {
+ return (await import(`${packageName}/package.json`)).default;
+ } catch {
+ return null;
+ }
+ }
+}
diff --git a/packages/ui/src/cli/utils/get-package-json.ts b/packages/ui/src/cli/utils/get-package-json.ts
index 901dcc8720..b65087b194 100644
--- a/packages/ui/src/cli/utils/get-package-json.ts
+++ b/packages/ui/src/cli/utils/get-package-json.ts
@@ -5,9 +5,9 @@ import { packageJsonFile } from "../consts";
export interface PackageJson {
name: string;
version: string;
+ scripts: Record;
dependencies: Record;
devDependencies: Record;
- scripts: Record;
}
/**
* Reads and parses the package.json file.
diff --git a/packages/ui/src/cli/utils/get-tailwind-version.ts b/packages/ui/src/cli/utils/get-tailwind-version.ts
new file mode 100644
index 0000000000..f71075138a
--- /dev/null
+++ b/packages/ui/src/cli/utils/get-tailwind-version.ts
@@ -0,0 +1,24 @@
+import { getModulePackageJson } from "./get-module-package-json";
+
+/**
+ * Gets the version of Tailwind CSS used in the project.
+ *
+ * This function attempts to read the version from the `tailwindcss/package.json` file.
+ *
+ * @throws {Error} If the detected Tailwind CSS major version is not 3 or 4
+ * @returns {Promise<3 | 4>} `3` if the version is 3.x, `4` if the version is 4.x
+ */
+export async function getTailwindVersion(): Promise<3 | 4> {
+ const tailwindcssPackageJson = await getModulePackageJson("tailwindcss");
+
+ if (!tailwindcssPackageJson) {
+ throw new Error("Tailwind CSS is not installed");
+ }
+
+ const major = parseInt(tailwindcssPackageJson.version.split(".")[0], 10);
+ if (major === 3 || major === 4) {
+ return major;
+ }
+
+ throw new Error(`Unsupported Tailwind CSS major version: ${major}`);
+}
diff --git a/packages/ui/src/helpers/get-tailwind-version.ts b/packages/ui/src/helpers/get-tailwind-version.ts
deleted file mode 100644
index 8b9a9a0074..0000000000
--- a/packages/ui/src/helpers/get-tailwind-version.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import version from "tailwindcss/version.js";
-
-/**
- * Gets the major version number of the installed Tailwind CSS
- *
- * @returns The major version number (3 or 4) or undefined if not found
- */
-export function getTailwindVersion(): 3 | 4 | undefined {
- try {
- return parseInt(version.split(".")[0], 10) as 3 | 4;
- } catch (_) {
- return;
- }
-}
diff --git a/packages/ui/src/helpers/resolve-theme.test.ts b/packages/ui/src/helpers/resolve-theme.test.ts
index cc292cfb0e..262450aa61 100644
--- a/packages/ui/src/helpers/resolve-theme.test.ts
+++ b/packages/ui/src/helpers/resolve-theme.test.ts
@@ -18,7 +18,7 @@ describe("resolveTheme", () => {
});
it("should apply prefix with version 3 format", () => {
- setStore({ prefix: "tw-" });
+ setStore({ prefix: "tw-", version: 3 });
const base = { color: "text-red-400" };
diff --git a/packages/ui/src/helpers/resolve-theme.ts b/packages/ui/src/helpers/resolve-theme.ts
index e465a8c000..e8c06c4ab5 100644
--- a/packages/ui/src/helpers/resolve-theme.ts
+++ b/packages/ui/src/helpers/resolve-theme.ts
@@ -1,13 +1,12 @@
import { deepmerge } from "deepmerge-ts";
import { klona } from "klona/json";
import { useRef } from "react";
-import { getDark, getPrefix } from "../store";
+import { getDark, getPrefix, getVersion } from "../store";
import type { ApplyTheme, DeepPartialApplyTheme, DeepPartialBoolean } from "../types";
import { applyPrefix } from "./apply-prefix";
import { applyPrefixV3 } from "./apply-prefix-v3";
import { convertUtilitiesToV4 } from "./convert-utilities-to-v4";
import { deepMergeStrings } from "./deep-merge";
-import { getTailwindVersion } from "./get-tailwind-version";
import { isEqual } from "./is-equal";
import { stripDark } from "./strip-dark";
import { twMerge } from "./tailwind-merge";
@@ -67,7 +66,7 @@ export function resolveTheme(
): T {
const dark = getDark();
const prefix = getPrefix();
- const version = getTailwindVersion();
+ const version = getVersion();
const _custom = custom?.length ? custom?.filter((value) => value !== undefined) : undefined;
const _clearThemeList = clearThemeList?.length ? clearThemeList?.filter((value) => value !== undefined) : undefined;
diff --git a/packages/ui/src/helpers/tailwind-merge.ts b/packages/ui/src/helpers/tailwind-merge.ts
index cbbe55fb5c..b45ca96a78 100644
--- a/packages/ui/src/helpers/tailwind-merge.ts
+++ b/packages/ui/src/helpers/tailwind-merge.ts
@@ -1,13 +1,12 @@
import { extendTailwindMerge as extendTailwindMerge_v2 } from "tailwind-merge-v2";
import { extendTailwindMerge as extendTailwindMerge_v3, type ClassNameValue } from "tailwind-merge-v3";
-import { getPrefix } from "../store";
-import { getTailwindVersion } from "./get-tailwind-version";
+import { getPrefix, getVersion } from "../store";
const cache = new Map>();
export function twMerge(...classLists: ClassNameValue[]): string {
const prefix = getPrefix();
- const version = getTailwindVersion();
+ const version = getVersion();
const cacheKey = `${prefix}.${version}`;
const cacheValue = cache.get(cacheKey);
diff --git a/packages/ui/src/hooks/use-theme-mode.ts b/packages/ui/src/hooks/use-theme-mode.ts
index b1ba6035bd..40e9ab1caf 100644
--- a/packages/ui/src/hooks/use-theme-mode.ts
+++ b/packages/ui/src/hooks/use-theme-mode.ts
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { isClient } from "../helpers/is-client";
import { useWatchLocalStorageValue } from "../hooks/use-watch-localstorage-value";
-import { getMode, getPrefix } from "../store";
+import { getMode, getPrefix, getVersion } from "../store";
const DEFAULT_MODE: ThemeMode = "auto";
const LS_THEME_MODE = "flowbite-theme-mode";
@@ -98,13 +98,15 @@ function setModeInLS(mode: ThemeMode) {
* Add or remove class `dark` on `html` element
*/
function setModeInDOM(mode: ThemeMode) {
- const prefix = getPrefix() ?? "";
const computedMode = computeModeValue(mode);
+ const prefix = getPrefix() ?? "";
+ const version = getVersion();
+ const className = version === 3 ? `${prefix}dark` : "dark";
if (computedMode === "dark") {
- document.documentElement.classList.add(`${prefix}dark`);
+ document.documentElement.classList.add(className);
} else {
- document.documentElement.classList.remove(`${prefix}dark`);
+ document.documentElement.classList.remove(className);
}
}
diff --git a/packages/ui/src/store/index.ts b/packages/ui/src/store/index.ts
index a78235c7f2..948c26fa02 100644
--- a/packages/ui/src/store/index.ts
+++ b/packages/ui/src/store/index.ts
@@ -22,12 +22,19 @@ export type StoreProps = DeepPartial<{
* @default undefined
*/
prefix: string;
+ /**
+ * The version of Tailwind CSS to use
+ *
+ * @default 4
+ */
+ version: 3 | 4;
}>;
const store: StoreProps = {
dark: undefined,
mode: undefined,
prefix: undefined,
+ version: undefined,
};
export function setStore(data: StoreProps) {
@@ -44,6 +51,13 @@ export function setStore(data: StoreProps) {
if ("prefix" in data) {
store.prefix = data.prefix;
}
+ if ("version" in data) {
+ if (data.version === 3 || data.version === 4) {
+ store.version = data.version;
+ } else {
+ console.warn(`Invalid version value: ${data.version}.\nAvailable values: 3, 4`);
+ }
+ }
}
export function getDark(): StoreProps["dark"] {
@@ -57,3 +71,7 @@ export function getMode(): StoreProps["mode"] {
export function getPrefix(): StoreProps["prefix"] {
return store.prefix;
}
+
+export function getVersion(): StoreProps["version"] {
+ return store.version;
+}
diff --git a/packages/ui/src/theme/config.tsx b/packages/ui/src/theme/config.tsx
index ce126fb0b3..5001a565c9 100644
--- a/packages/ui/src/theme/config.tsx
+++ b/packages/ui/src/theme/config.tsx
@@ -1,10 +1,10 @@
import type { StoreProps } from "../store";
import { StoreInit } from "../store/init";
-export type ThemeConfigProps = StoreProps;
+export type ThemeConfigProps = Pick;
-export function ThemeConfig(props: ThemeConfigProps) {
- return ;
+export function ThemeConfig({ mode }: ThemeConfigProps) {
+ return ;
}
ThemeConfig.displayName = "ThemeConfig";
diff --git a/packages/ui/src/theme/mode-script.test.tsx b/packages/ui/src/theme/mode-script.test.tsx
index 0114f5e123..fbd3f3fa6a 100644
--- a/packages/ui/src/theme/mode-script.test.tsx
+++ b/packages/ui/src/theme/mode-script.test.tsx
@@ -1,7 +1,6 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
-import { getPrefix } from "../store";
-import { ThemeModeScript } from "./mode-script";
+import { getThemeModeScript, ThemeModeScript } from "./mode-script";
describe("ThemeModeScript", () => {
it("should render with default props", () => {
@@ -34,11 +33,39 @@ describe("ThemeModeScript", () => {
expect(script?.innerHTML).toContain("custom-key");
});
- it("should include prefix from getPrefix()", () => {
- const prefix = getPrefix() ?? "";
- const { container } = render();
+ it("should include prefix when version is 3", () => {
+ const { container } = render();
const script = container.querySelector("script");
- expect(script?.innerHTML).toContain(`"${prefix}dark"`);
+ expect(script?.innerHTML).toContain('const className = "custom-dark"');
+ });
+
+ it("should not include prefix when version is 4", () => {
+ const { container } = render();
+ const script = container.querySelector("script");
+
+ expect(script?.innerHTML).toContain('const className = "dark"');
+ });
+});
+
+describe("getThemeModeScript", () => {
+ it("should return the correct script for prefix and version 3", () => {
+ const script = getThemeModeScript({ prefix: "custom-", version: 3 });
+ expect(script).toContain('const className = "custom-dark"');
+ });
+
+ it("should return the correct script for prefix and version 4", () => {
+ const script = getThemeModeScript({ prefix: "custom-", version: 4 });
+ expect(script).toContain('const className = "dark"');
+ });
+
+ it("should return the correct script for version 3", () => {
+ const script = getThemeModeScript({ version: 3 });
+ expect(script).toContain('const className = "dark"');
+ });
+
+ it("should return the correct script for version 4", () => {
+ const script = getThemeModeScript({ version: 4 });
+ expect(script).toContain('const className = "dark"');
});
});
diff --git a/packages/ui/src/theme/mode-script.tsx b/packages/ui/src/theme/mode-script.tsx
index ce13b89034..95ee95b5fa 100644
--- a/packages/ui/src/theme/mode-script.tsx
+++ b/packages/ui/src/theme/mode-script.tsx
@@ -1,12 +1,12 @@
import type React from "react";
import type { ThemeMode } from "../hooks/use-theme-mode";
-import { getPrefix } from "../store";
const defaultOptions = {
mode: "auto" as ThemeMode,
defaultMode: "auto" as ThemeMode,
localStorageKey: "flowbite-theme-mode",
prefix: "",
+ version: 4 as 3 | 4,
};
export interface ThemeModeScriptProps extends React.ComponentPropsWithoutRef<"script"> {
@@ -30,6 +30,20 @@ export interface ThemeModeScriptProps extends React.ComponentPropsWithoutRef<"sc
* @default "flowbite-theme-mode"
*/
localStorageKey?: string;
+ /**
+ * The prefix to use for the theme mode class
+ *
+ * @type {string}
+ * @default ""
+ */
+ prefix?: string;
+ /**
+ * The version of Tailwind CSS to use
+ *
+ * @type {3 | 4}
+ * @default 4
+ */
+ version?: 3 | 4;
}
/**
@@ -46,6 +60,8 @@ export function ThemeModeScript({
mode,
defaultMode = defaultOptions.defaultMode,
localStorageKey = defaultOptions.localStorageKey,
+ prefix = defaultOptions.prefix,
+ version = defaultOptions.version,
...others
}: ThemeModeScriptProps): JSX.Element {
return (
@@ -53,7 +69,13 @@ export function ThemeModeScript({
{...others}
data-flowbite-theme-mode-script
dangerouslySetInnerHTML={{
- __html: getThemeModeScript({ mode, defaultMode, localStorageKey, prefix: getPrefix() ?? "" }),
+ __html: getThemeModeScript({
+ mode,
+ defaultMode,
+ localStorageKey,
+ prefix,
+ version,
+ }),
}}
/>
);
@@ -77,6 +99,7 @@ export function getThemeModeScript(
defaultMode?: ThemeMode;
localStorageKey?: string;
prefix?: string;
+ version?: 3 | 4;
} = {},
): string {
const {
@@ -84,6 +107,7 @@ export function getThemeModeScript(
defaultMode = defaultOptions.defaultMode,
localStorageKey = defaultOptions.localStorageKey,
prefix = defaultOptions.prefix,
+ version = defaultOptions.version,
} = props;
return `
@@ -93,11 +117,12 @@ export function getThemeModeScript(
const resolvedMode = (isStorageModeValid ? storageMode : null) ?? ${mode ? `"${mode}"` : undefined} ?? "${defaultMode}";
const computedMode =
resolvedMode === "auto" ? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : resolvedMode;
+ const className = ${version === 3 ? `"${prefix}dark"` : `"dark"`};
if (computedMode === "dark") {
- document.documentElement.classList.add("${prefix}dark");
+ document.documentElement.classList.add(className);
} else {
- document.documentElement.classList.remove("${prefix}dark");
+ document.documentElement.classList.remove(className);
}
localStorage.setItem("${localStorageKey}", resolvedMode);
@@ -110,9 +135,9 @@ export function getThemeModeScript(
if (resolvedMode === "auto") {
if (e.matches) {
- document.documentElement.classList.add("${prefix}dark");
+ document.documentElement.classList.add(className);
} else {
- document.documentElement.classList.remove("${prefix}dark");
+ document.documentElement.classList.remove(className);
}
}
});
@@ -125,9 +150,9 @@ export function getThemeModeScript(
const resolvedMode = isStorageModeValid ? newMode : "${defaultMode}";
if (resolvedMode === "dark" || (resolvedMode === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
- document.documentElement.classList.add("${prefix}dark");
+ document.documentElement.classList.add(className);
} else {
- document.documentElement.classList.remove("${prefix}dark");
+ document.documentElement.classList.remove(className);
}
}
});
@@ -144,6 +169,7 @@ export function getThemeModeScript(
* @param {ThemeMode} [options.defaultMode="auto"] - The default theme mode if none is set
* @param {string} [options.localStorageKey="flowbite-theme-mode"] - Key used to store theme mode in localStorage
* @param {string} [options.prefix=""] - The prefix to use for the theme mode class
+ * @param {3 | 4} [options.version=4] - The version of Tailwind CSS to use
* @returns {void}
*/
export function initThemeMode(
@@ -152,6 +178,7 @@ export function initThemeMode(
defaultMode?: ThemeMode;
localStorageKey?: string;
prefix?: string;
+ version?: 3 | 4;
} = {},
): void {
const {
@@ -159,6 +186,7 @@ export function initThemeMode(
defaultMode = defaultOptions.defaultMode,
localStorageKey = defaultOptions.localStorageKey,
prefix = defaultOptions.prefix,
+ version = defaultOptions.version,
} = props;
try {
@@ -171,11 +199,12 @@ export function initThemeMode(
? "dark"
: "light"
: resolvedMode;
+ const className = version === 3 ? `${prefix}dark` : "dark";
if (computedMode === "dark") {
- document.documentElement.classList.add(`${prefix}dark`);
+ document.documentElement.classList.add(className);
} else {
- document.documentElement.classList.remove(`${prefix}dark`);
+ document.documentElement.classList.remove(className);
}
localStorage.setItem(localStorageKey, resolvedMode);
@@ -188,9 +217,9 @@ export function initThemeMode(
if (resolvedMode === "auto") {
if (e.matches) {
- document.documentElement.classList.add(`${prefix}dark`);
+ document.documentElement.classList.add(className);
} else {
- document.documentElement.classList.remove(`${prefix}dark`);
+ document.documentElement.classList.remove(className);
}
}
});
@@ -206,9 +235,9 @@ export function initThemeMode(
resolvedMode === "dark" ||
(resolvedMode === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
- document.documentElement.classList.add(`${prefix}dark`);
+ document.documentElement.classList.add(className);
} else {
- document.documentElement.classList.remove(`${prefix}dark`);
+ document.documentElement.classList.remove(className);
}
}
});