@fakhrirafiki/theme-engine 0.4.5 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,32 @@
1
- # Theme Engine
1
+ # 🎨 Theme Engine
2
2
 
3
- Theme system for React: **mode** (`light | dark | system`) + **color presets** (CSS variables) with optional View Transition ripple.
3
+ Theme system for **Next.js (App Router)**: mode (`light | dark | system`) + theme presets (semantic tokens via CSS variables).
4
+
5
+ > ✅ Opinionated defaults, minimal setup, and TypeScript autocomplete that “just works”.
6
+
7
+ ![npm version](https://img.shields.io/npm/v/@fakhrirafiki/theme-engine)
8
+ ![npm downloads](https://img.shields.io/npm/dm/@fakhrirafiki/theme-engine)
9
+ ![license](https://img.shields.io/npm/l/@fakhrirafiki/theme-engine)
10
+
11
+ ## ✨ Why use this?
12
+
13
+ - ⚡ **Fast setup**: 1 CSS import + 1 provider
14
+ - 🌓 **Mode support**: `light | dark | system` (with View Transition ripple when supported)
15
+ - 🎨 **Theme presets**: built-in presets + your own presets
16
+ - 🧠 **DX-first**: `useThemeEngine()` for everything
17
+ - 🧩 **Tailwind v4 friendly**: `@theme inline` tokens included (works with shadcn-style semantic tokens)
18
+
19
+ ## 📚 Table of contents
20
+
21
+ - [Install](#-install)
22
+ - [Quick Start (Nextjs App Router)](#-quick-start-nextjs-app-router)
23
+ - [Usage](#-usage)
24
+ - [Custom presets](#-custom-presets-recommended)
25
+ - [Built-in presets](#-built-in-presets)
26
+ - [Tailwind tokens](#-tailwind-tokens-you-get)
27
+ - [Components](#-components)
28
+ - [API reference](#-api-reference)
29
+ - [Troubleshooting](#-troubleshooting)
4
30
 
5
31
  ## Install
6
32
 
@@ -8,45 +34,49 @@ Theme system for React: **mode** (`light | dark | system`) + **color presets** (
8
34
  pnpm add @fakhrirafiki/theme-engine
9
35
  ```
10
36
 
11
- ## Setup (Next.js App Router)
37
+ > Using npm/yarn?
38
+ >
39
+ > - `npm i @fakhrirafiki/theme-engine`
40
+ > - `yarn add @fakhrirafiki/theme-engine`
41
+
42
+ ## 🚀 Quick Start (Next.js App Router)
12
43
 
13
44
  ### 1) Import CSS once
14
45
 
15
- In `app/globals.css`:
46
+ In `src/app/globals.css`:
16
47
 
17
48
  ```css
18
- @import "@fakhrirafiki/theme-engine/styles";
49
+ @import '@fakhrirafiki/theme-engine/styles';
19
50
  ```
20
51
 
21
- Notes:
22
-
23
- - If you use Tailwind v4, you will typically also want:
52
+ ✅ Tailwind v4 (recommended order):
24
53
 
25
54
  ```css
26
- @import "tailwindcss";
27
- @import "@fakhrirafiki/theme-engine/styles";
55
+ @import 'tailwindcss';
56
+ @import '@fakhrirafiki/theme-engine/styles';
28
57
 
29
58
  @custom-variant dark (&:is(.dark *));
30
59
  ```
31
60
 
32
- - `@fakhrirafiki/theme-engine/styles` includes Tailwind v4 directives (`@theme inline`). If you are not using Tailwind v4, import only the non-tailwind CSS modules instead:
61
+ ℹ️ Not using Tailwind v4?
33
62
 
34
63
  ```css
35
- @import "@fakhrirafiki/theme-engine/styles/base.css";
36
- @import "@fakhrirafiki/theme-engine/styles/animations.css";
37
- @import "@fakhrirafiki/theme-engine/styles/components.css";
38
- @import "@fakhrirafiki/theme-engine/styles/utilities.css";
64
+ @import '@fakhrirafiki/theme-engine/styles/base.css';
65
+ @import '@fakhrirafiki/theme-engine/styles/animations.css';
66
+ @import '@fakhrirafiki/theme-engine/styles/components.css';
67
+ @import '@fakhrirafiki/theme-engine/styles/utilities.css';
39
68
  ```
40
69
 
41
- ### 2) Wrap with `ThemeProvider`
70
+ ### 2) Wrap your app with `ThemeProvider`
42
71
 
43
- In `app/layout.tsx`:
72
+ In `src/app/layout.tsx`:
44
73
 
45
74
  ```tsx
46
- import { ThemeProvider } from "@fakhrirafiki/theme-engine";
47
- import "./globals.css";
75
+ import type { ReactNode } from 'react';
76
+ import { ThemeProvider } from '@fakhrirafiki/theme-engine';
77
+ import './globals.css';
48
78
 
49
- export default function RootLayout({ children }: { children: React.ReactNode }) {
79
+ export default function RootLayout({ children }: { children: ReactNode }) {
50
80
  return (
51
81
  <html lang="en" suppressHydrationWarning>
52
82
  <body>
@@ -59,133 +89,158 @@ export default function RootLayout({ children }: { children: React.ReactNode })
59
89
  }
60
90
  ```
61
91
 
62
- Notes:
63
-
64
- - By default, `ThemeProvider` injects `ThemeScript` for preset restoration (to reduce flashes).
65
- - `ThemeScript` restores **preset colors** only (it does not set the `dark`/`light` class).
66
- - `defaultPreset="..."` pre-hydration only works for **built-in presets** (because `ThemeScript` uses `getPresetById()` internally). Custom `defaultPreset` still works after hydration.
92
+ ### 3) Use it
67
93
 
68
- ## Day-to-day usage
94
+ ## 🧑‍💻 Usage
69
95
 
70
- ### Toggle dark/light mode
71
-
72
- Use the ready-made button:
96
+ Toggle mode:
73
97
 
74
98
  ```tsx
75
- "use client";
99
+ 'use client';
76
100
 
77
- import { ThemeToggle } from "@fakhrirafiki/theme-engine";
101
+ import { useThemeEngine } from '@fakhrirafiki/theme-engine';
78
102
 
79
- export function HeaderThemeToggle() {
80
- return <ThemeToggle size="md" variant="ghost" />;
103
+ export function ModeButtons() {
104
+ const { mode, setDarkMode, toggleDarkMode } = useThemeEngine();
105
+
106
+ return (
107
+ <div>
108
+ <button onClick={() => setDarkMode('system')}>System</button>
109
+ <button onClick={() => setDarkMode('light')}>Light</button>
110
+ <button onClick={() => setDarkMode('dark')}>Dark</button>
111
+ <button onClick={() => toggleDarkMode()}>Toggle</button>
112
+ <div>Current: {mode}</div>
113
+ </div>
114
+ );
81
115
  }
82
116
  ```
83
117
 
84
- Or control it yourself:
118
+ Pick a theme preset by ID:
85
119
 
86
120
  ```tsx
87
- "use client";
121
+ 'use client';
88
122
 
89
- import { useTheme } from "@fakhrirafiki/theme-engine";
123
+ import { useThemeEngine } from '@fakhrirafiki/theme-engine';
124
+
125
+ export function PresetButtons() {
126
+ const { applyThemeById, clearTheme, currentTheme } = useThemeEngine();
90
127
 
91
- export function ModeButtons() {
92
- const { mode, setMode, toggleMode } = useTheme();
93
128
  return (
94
129
  <div>
95
- <button onClick={() => setMode("system")}>System</button>
96
- <button onClick={() => setMode("light")}>Light</button>
97
- <button onClick={() => setMode("dark")}>Dark</button>
98
- <button onClick={() => toggleMode()}>Toggle</button>
99
- <div>Current: {mode}</div>
130
+ <button onClick={() => applyThemeById('modern-minimal')}>Modern Minimal</button>
131
+ <button onClick={() => clearTheme()}>Reset</button>
132
+ <div>Active: {currentTheme?.presetName ?? 'Default'}</div>
100
133
  </div>
101
134
  );
102
135
  }
103
136
  ```
104
137
 
105
- ### Pick a preset (by ID)
138
+ 💡 Want typed autocomplete (built-in IDs + your custom IDs)? Use a generic:
106
139
 
107
140
  ```tsx
108
- "use client";
141
+ 'use client';
109
142
 
110
- import { useTypedTheme } from "@fakhrirafiki/theme-engine";
111
- import { customPresets } from "./custom-theme-presets";
143
+ import { ThemePresets, useThemeEngine } from '@fakhrirafiki/theme-engine';
144
+ import { customPresets } from './custom-theme-presets';
112
145
 
113
- export function PresetButtons() {
114
- const { setThemePresetById, clearPreset, currentPreset } = useTypedTheme(customPresets);
146
+ export function TypedPresetButtons() {
147
+ const { applyThemeById } = useThemeEngine<ThemePresets<typeof customPresets>>();
115
148
 
116
149
  return (
117
150
  <div>
118
- <button onClick={() => setThemePresetById("my-brand")}>My Brand</button>
119
- <button onClick={() => setThemePresetById("modern-minimal")}>Modern Minimal</button>
120
- <button onClick={() => clearPreset()}>Reset</button>
121
- <div>Active: {currentPreset?.presetName ?? "Default"}</div>
151
+ <button onClick={() => applyThemeById('my-brand')}>My Brand</button>
152
+ <button onClick={() => applyThemeById('modern-minimal')}>Modern Minimal</button>
122
153
  </div>
123
154
  );
124
155
  }
125
156
  ```
126
157
 
127
- ### Use the animated preset picker
158
+ ---
128
159
 
129
- ```tsx
130
- "use client";
160
+ ## Concepts
131
161
 
132
- import { ThemePresetButtons } from "@fakhrirafiki/theme-engine";
162
+ ### Mode vs preset
133
163
 
134
- export function PresetPicker() {
135
- return <ThemePresetButtons />;
136
- }
137
- ```
164
+ - 🌓 **Mode** controls the `<html>` class (`.light` / `.dark`) and `color-scheme`.
165
+ - 🎨 **Preset** controls semantic design tokens (CSS variables like `--background`, `--primary`, etc).
138
166
 
139
- ## Presets
167
+ ### SSR & flashes
140
168
 
141
- ### Built-in presets
169
+ - `ThemeProvider` injects `ThemeScript` to restore **preset colors** before hydration (reduces flashes).
170
+ - `ThemeScript` restores **preset colors only** (it does not set the `.dark` / `.light` class).
171
+ - `defaultPreset="..."` pre-hydration only works for **built-in presets**. Custom `defaultPreset` still works after hydration.
142
172
 
143
- The package ships with a built-in preset collection:
173
+ ### Persistence
144
174
 
145
- ```tsx
146
- import { getPresetIds, getPresetById } from "@fakhrirafiki/theme-engine";
175
+ By default:
147
176
 
148
- const ids = getPresetIds();
149
- const modernMinimal = getPresetById("modern-minimal");
177
+ - Mode is stored in `localStorage['theme-engine-theme']`
178
+ - Preset is stored in `localStorage['theme-preset']`
179
+
180
+ If you run multiple apps on the same domain, override the keys:
181
+
182
+ ```tsx
183
+ <ThemeProvider modeStorageKey="my-app:mode" presetStorageKey="my-app:preset">
184
+ {children}
185
+ </ThemeProvider>
150
186
  ```
151
187
 
152
- ### Custom presets (recommended pattern)
188
+ ---
153
189
 
154
- Create your presets in TweakCN-compatible format and pass them into `ThemeProvider`:
190
+ ## 🧩 Custom presets (recommended)
155
191
 
156
- ```tsx
157
- import { ThemeProvider, type TweakCNThemePreset } from "@fakhrirafiki/theme-engine";
192
+ Create presets in TweakCN-compatible format and pass them into `ThemeProvider`.
193
+
194
+ ✅ Tip: use `satisfies` to preserve literal keys for TS autocomplete:
195
+
196
+ ### 🎛️ Get a brand theme from TweakCN (recommended)
197
+
198
+ The fastest way to create a great-looking preset is to use the TweakCN editor:
199
+
200
+ - https://tweakcn.com/editor/theme
201
+
202
+ Pick a theme, tweak the colors, then copy the preset output and paste it into your `customPresets` object (it matches the `TweakCNThemePreset` shape).
203
+
204
+ ```ts
205
+ import { type TweakCNThemePreset } from '@fakhrirafiki/theme-engine';
158
206
 
159
- // Tip: use `satisfies` (instead of `Record<string, ...>`) to keep literal keys for TS autocomplete
160
- const customPresets = {
161
- "my-brand": {
162
- label: "My Brand",
207
+ export const customPresets = {
208
+ 'my-brand': {
209
+ label: 'My Brand',
163
210
  styles: {
164
211
  light: {
165
- background: "#ffffff",
166
- foreground: "#111827",
167
- card: "#ffffff",
168
- "card-foreground": "#111827",
169
- primary: "#2563eb",
170
- "primary-foreground": "#ffffff",
171
- secondary: "#e5e7eb",
172
- "secondary-foreground": "#111827",
212
+ background: '#ffffff',
213
+ foreground: '#111827',
214
+ primary: '#2563eb',
215
+ 'primary-foreground': '#ffffff',
216
+ secondary: '#e5e7eb',
217
+ 'secondary-foreground': '#111827',
218
+ card: '#ffffff',
219
+ 'card-foreground': '#111827',
173
220
  },
174
221
  dark: {
175
- background: "#0b1020",
176
- foreground: "#f9fafb",
177
- card: "#111827",
178
- "card-foreground": "#f9fafb",
179
- primary: "#60a5fa",
180
- "primary-foreground": "#0b1020",
181
- secondary: "#1f2937",
182
- "secondary-foreground": "#f9fafb",
222
+ background: '#0b1020',
223
+ foreground: '#f9fafb',
224
+ primary: '#60a5fa',
225
+ 'primary-foreground': '#0b1020',
226
+ secondary: '#1f2937',
227
+ 'secondary-foreground': '#f9fafb',
228
+ card: '#111827',
229
+ 'card-foreground': '#f9fafb',
183
230
  },
184
231
  },
185
232
  },
186
233
  } satisfies Record<string, TweakCNThemePreset>;
234
+ ```
235
+
236
+ Then in your providers/layout:
237
+
238
+ ```tsx
239
+ import type { ReactNode } from 'react';
240
+ import { ThemeProvider } from '@fakhrirafiki/theme-engine';
241
+ import { customPresets } from './custom-theme-presets';
187
242
 
188
- export function AppRoot({ children }: { children: React.ReactNode }) {
243
+ export function AppProviders({ children }: { children: ReactNode }) {
189
244
  return (
190
245
  <ThemeProvider customPresets={customPresets} defaultPreset="my-brand">
191
246
  {children}
@@ -194,28 +249,28 @@ export function AppRoot({ children }: { children: React.ReactNode }) {
194
249
  }
195
250
  ```
196
251
 
197
- Validation behavior:
252
+ Notes:
198
253
 
199
254
  - Custom presets are validated in `ThemeProvider`.
200
- - Invalid custom presets are skipped; warnings are allowed.
201
- - You can validate manually via `validateCustomPresets()` / `logValidationResult()`.
255
+ - Invalid custom presets are skipped (warnings/errors are logged on `localhost`).
256
+ - Preset values can be `H S% L%`, `hsl(...)`, `#hex`, `rgb(...)`, and modern CSS colors like `oklch(...)` (they are normalized internally).
202
257
 
203
- ## Persistence keys
258
+ ---
204
259
 
205
- By default:
260
+ ## 🎁 Built-in presets
206
261
 
207
- - Mode is stored in `localStorage['theme-engine-theme']`.
208
- - Preset is stored in `localStorage['theme-preset']`.
262
+ The package ships with a built-in preset collection:
209
263
 
210
- If you run multiple apps on the same domain, override the keys:
264
+ ```ts
265
+ import { getPresetIds, getPresetById } from '@fakhrirafiki/theme-engine';
211
266
 
212
- ```tsx
213
- <ThemeProvider modeStorageKey="my-app:mode" presetStorageKey="my-app:preset">
214
- {children}
215
- </ThemeProvider>
267
+ const ids = getPresetIds();
268
+ const modernMinimal = getPresetById('modern-minimal');
216
269
  ```
217
270
 
218
- ## Tailwind tokens you get
271
+ ---
272
+
273
+ ## 🎨 Tailwind tokens you get
219
274
 
220
275
  After importing `@fakhrirafiki/theme-engine/styles`, you can use semantic tokens like:
221
276
 
@@ -229,28 +284,139 @@ After importing `@fakhrirafiki/theme-engine/styles`, you can use semantic tokens
229
284
  | Muted | `bg-muted`, `text-muted-foreground` | `--muted`, `--muted-foreground` | Subtle backgrounds / helper text |
230
285
  | Accent | `bg-accent`, `text-accent-foreground` | `--accent`, `--accent-foreground` | Emphasis (not status colors) |
231
286
  | Destructive | `bg-destructive`, `text-destructive-foreground` | `--destructive`, `--destructive-foreground` | Danger actions |
232
- | Borders / focus | `border-border`, `border-input`, `ring-ring` | `--border`, `--input`, `--ring` | Also used by `outline-ring/50` in `tailwind.css` |
287
+ | Borders / focus | `border-border`, `border-input`, `ring-ring` | `--border`, `--input`, `--ring` | Used by `outline-ring/50` too |
233
288
  | Charts | `bg-chart-1`, `text-chart-2` | `--chart-1` ... `--chart-5` | Data viz palettes |
234
289
  | Sidebar | `bg-sidebar`, `text-sidebar-foreground`, `bg-sidebar-primary`, `border-sidebar-border` | `--sidebar-*` | Handy for dashboard layouts |
235
- | Status accents | `bg-accent-success`, `text-accent-danger-foreground` | `--accent-<name>`, `--accent-<name>-foreground` | Optional: only if your preset defines `accent-*` variables |
290
+ | Status accents | `bg-accent-success`, `text-accent-danger-foreground` | `--accent-<name>`, `--accent-<name>-foreground` | Optional: only if preset defines `accent-*` |
236
291
  | Radius scale | `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl` | `--radius-sm`, `--radius-md`, `--radius-lg`, `--radius-xl` | Derived from `--radius` |
237
292
  | Tracking scale | `tracking-tighter`, `tracking-wide` | `--tracking-*` | Derived from `--letter-spacing` |
238
- | Fonts | `font-sans`, `font-serif`, `font-mono` | `--font-sans`, `--font-serif`, `--font-mono` | Set in `base.css`, overridable by presets |
239
- | Shadows | `shadow-sm`, `shadow-md`, `shadow-xl` | `--shadow-*` | Shadow scale derived from `--shadow-*` knobs |
293
+ | Fonts | `font-sans`, `font-serif`, `font-mono` | `--font-sans`, `--font-serif`, `--font-mono` | Defaults in `base.css` |
294
+ | Shadows | `shadow-sm`, `shadow-md`, `shadow-xl` | `--shadow-*` | Derived from `--shadow-*` knobs |
295
+
296
+ ---
297
+
298
+ ## 🧱 Components
299
+
300
+ ### `ThemeToggle`
301
+
302
+ Ready-made mode toggle button (with View Transition ripple when supported).
303
+
304
+ ```tsx
305
+ 'use client';
240
306
 
241
- ## Troubleshooting
307
+ import { ThemeToggle } from '@fakhrirafiki/theme-engine';
242
308
 
243
- ### `useTheme must be used within a ThemeProvider`
309
+ export function HeaderThemeToggle() {
310
+ return <ThemeToggle size="md" variant="ghost" />;
311
+ }
312
+ ```
313
+
314
+ ### `ThemePresetButtons`
315
+
316
+ Animated preset picker (shows custom presets first, then built-ins):
317
+
318
+ ```tsx
319
+ 'use client';
320
+
321
+ import { ThemePresetButtons } from '@fakhrirafiki/theme-engine';
322
+
323
+ export function PresetPicker() {
324
+ return <ThemePresetButtons />;
325
+ }
326
+ ```
327
+
328
+ ---
329
+
330
+ ## 🧾 API Reference
331
+
332
+ ### `ThemeProvider`
333
+
334
+ ```ts
335
+ <ThemeProvider
336
+ defaultMode="system"
337
+ defaultPreset="modern-minimal"
338
+ modeStorageKey="theme-engine-theme"
339
+ presetStorageKey="theme-preset"
340
+ customPresets={customPresets}
341
+ />
342
+ ```
343
+
344
+ | Prop | Type | Default | Description |
345
+ | --- | --- | --- | --- |
346
+ | `children` | `ReactNode` | required | React subtree |
347
+ | `defaultMode` | `Mode` | `'system'` | Used when no persisted value |
348
+ | `defaultPreset` | `BuiltInPresetId \| keyof customPresets` | `undefined` | Default preset (see SSR note) |
349
+ | `modeStorageKey` | `string` | `'theme-engine-theme'` | `localStorage` key for mode |
350
+ | `presetStorageKey` | `string` | `'theme-preset'` | `localStorage` key for preset |
351
+ | `customPresets` | `Record<string, TweakCNThemePreset>` | `undefined` | Add your own presets (can override built-ins by ID) |
352
+ | `ThemeScript` | n/a | always on | `ThemeProvider` always injects `ThemeScript` for pre-hydration preset restoration |
353
+
354
+ ### `useThemeEngine()`
355
+
356
+ Signature:
357
+
358
+ ```ts
359
+ useThemeEngine<TCustomPresets = undefined>()
360
+ ```
361
+
362
+ To get typed custom preset IDs:
363
+
364
+ ```ts
365
+ useThemeEngine<ThemePresets<typeof customPresets>>()
366
+ ```
367
+
368
+ Return fields:
369
+
370
+ | Field | Type | Description |
371
+ | --- | --- | --- |
372
+ | `darkMode` | `boolean` | `resolvedMode === 'dark'` |
373
+ | `mode` | `'light' \| 'dark' \| 'system'` | Current user preference |
374
+ | `resolvedMode` | `'light' \| 'dark'` | Resolved mode (never `system`) |
375
+ | `setDarkMode` | `(mode: Mode) => void` | Set `light \| dark \| system` |
376
+ | `toggleDarkMode` | `(coords?: { x: number; y: number }) => void` | Toggles light/dark (and exits `system`) |
377
+ | `applyThemeById` | `(id: ThemeId) => void` | Apply a preset by ID (alias: `applyPresetById`) |
378
+ | `clearTheme` | `() => void` | Clear preset and fall back to `defaultPreset` if provided (alias: `clearPreset`) |
379
+ | `currentTheme` | `{ presetId; presetName; colors; appliedAt } \| null` | Current preset (alias: `currentPreset`) |
380
+ | `isUsingDefaultPreset` | `boolean` | Whether current preset equals `defaultPreset` |
381
+ | `availablePresets` | `Record<string, TweakCNThemePreset>` | Built-in + custom |
382
+ | `builtInPresets` | `Record<string, TweakCNThemePreset>` | Built-in only |
383
+ | `customPresets` | `Record<string, TweakCNThemePreset>` | Custom only |
384
+
385
+ ### `ThemeScript`
386
+
387
+ Normally you don’t need this (it’s injected by `ThemeProvider` by default).
388
+
389
+ | Prop | Type | Default | Description |
390
+ | --- | --- | --- | --- |
391
+ | `presetStorageKey` | `string` | `'theme-preset'` | Must match `ThemeProvider` |
392
+ | `defaultPreset` | `string` | `undefined` | Built-in preset ID for pre-hydration default |
393
+
394
+ ### Utilities
395
+
396
+ | Export | Description |
397
+ | --- | --- |
398
+ | `formatColor(color, format)` | Converts a color string into `hsl`/`rgb`/`hex` |
399
+ | `withAlpha(hslTriplet, alpha)` | Adds alpha to an HSL triplet |
400
+
401
+ ---
402
+
403
+ ## 🩹 Troubleshooting
404
+
405
+ ### `useThemeEngine must be used within a ThemeProvider`
244
406
 
245
407
  Wrap your component tree with `ThemeProvider` (and ensure the component is a client component).
246
408
 
409
+ Note: the thrown error string might mention `useTheme` because `useThemeEngine()` uses it internally.
410
+
247
411
  ### Preset doesn’t apply on refresh
248
412
 
249
- If you render `ThemeScript` manually (using `disableScript`), make sure both use the same `presetStorageKey`.
413
+ `ThemeProvider` injects `ThemeScript` automatically. Avoid rendering `ThemeScript` manually (you may end up with duplicates).
414
+
415
+ ### Styles don’t load / components look unstyled
250
416
 
251
- ### `ThemePresetButtons` breaks
417
+ Ensure your `globals.css` imports `@fakhrirafiki/theme-engine/styles` (and Tailwind v4 is configured if you rely on Tailwind utilities).
252
418
 
253
- Ensure you imported `@fakhrirafiki/theme-engine/styles` (or at least `animations.css` + `components.css`).
419
+ ---
254
420
 
255
421
  ## License
256
422