@fakhrirafiki/theme-engine 0.4.8 → 0.4.9
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 +100 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Theme system for **Next.js (App Router)**: mode (`light | dark | system`) + them
|
|
|
25
25
|
- [Built-in presets](#-built-in-presets)
|
|
26
26
|
- [Tailwind tokens](#-tailwind-tokens-you-get)
|
|
27
27
|
- [Components](#-components)
|
|
28
|
+
- [Recipe: ThemePresetSelect](#-recipe-themepresetselect-simple-list)
|
|
28
29
|
- [API reference](#-api-reference)
|
|
29
30
|
- [Troubleshooting](#-troubleshooting)
|
|
30
31
|
|
|
@@ -166,8 +167,8 @@ export function TypedPresetButtons() {
|
|
|
166
167
|
|
|
167
168
|
### SSR & flashes
|
|
168
169
|
|
|
169
|
-
- `ThemeProvider` injects
|
|
170
|
-
-
|
|
170
|
+
- `ThemeProvider` injects a small pre-hydration script to restore **preset colors** before hydration (reduces flashes).
|
|
171
|
+
- The pre-hydration script restores **preset colors only** (it does not set the `.dark` / `.light` class).
|
|
171
172
|
- `defaultPreset="..."` pre-hydration only works for **built-in presets**. Custom `defaultPreset` still works after hydration.
|
|
172
173
|
|
|
173
174
|
### Persistence
|
|
@@ -325,6 +326,101 @@ export function PresetPicker() {
|
|
|
325
326
|
}
|
|
326
327
|
```
|
|
327
328
|
|
|
329
|
+
### 🧾 Recipe: `ThemePresetSelect` (simple list)
|
|
330
|
+
|
|
331
|
+
Want a simple, scrollable preset list (e.g. for a settings modal)? Copy-paste this component and style it however you like.
|
|
332
|
+
|
|
333
|
+
> Note: this snippet uses Tailwind utility classes. If you don’t use Tailwind, replace the classes with your own styles/UI components.
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
'use client';
|
|
337
|
+
|
|
338
|
+
import { formatColor, useThemeEngine } from '@fakhrirafiki/theme-engine';
|
|
339
|
+
|
|
340
|
+
type ThemePresetSelectProps = {
|
|
341
|
+
allowedPresetIds?: string[];
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export function ThemePresetSelect({
|
|
345
|
+
allowedPresetIds = [
|
|
346
|
+
'modern-minimal',
|
|
347
|
+
'violet-bloom',
|
|
348
|
+
'vercel',
|
|
349
|
+
'mono',
|
|
350
|
+
],
|
|
351
|
+
}: ThemePresetSelectProps) {
|
|
352
|
+
const { currentTheme, applyThemeById, availablePresets, resolvedMode } = useThemeEngine();
|
|
353
|
+
|
|
354
|
+
const presets = allowedPresetIds
|
|
355
|
+
.map((id) => {
|
|
356
|
+
const preset = availablePresets[id];
|
|
357
|
+
if (!preset) return null;
|
|
358
|
+
return { id, label: preset.label };
|
|
359
|
+
})
|
|
360
|
+
.filter((preset): preset is { id: string; label: string } => preset !== null);
|
|
361
|
+
|
|
362
|
+
const getPreviewColors = (presetId: string): string[] => {
|
|
363
|
+
const preset = availablePresets[presetId];
|
|
364
|
+
if (!preset) return [];
|
|
365
|
+
|
|
366
|
+
const scheme = resolvedMode === 'dark' ? preset.styles.dark : preset.styles.light;
|
|
367
|
+
const primary = (scheme as any).primary as string | undefined;
|
|
368
|
+
const secondary = (scheme as any).secondary as string | undefined;
|
|
369
|
+
const accent = (scheme as any).accent as string | undefined;
|
|
370
|
+
|
|
371
|
+
return [primary, secondary, accent].filter(Boolean) as string[];
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<div className="mt-4 max-h-[70vh] space-y-2 overflow-y-auto pr-1">
|
|
376
|
+
{presets.map((preset) => {
|
|
377
|
+
const isActive = currentTheme?.presetId === preset.id;
|
|
378
|
+
const previewColors = getPreviewColors(preset.id).slice(0, 3);
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<button
|
|
382
|
+
key={preset.id}
|
|
383
|
+
type="button"
|
|
384
|
+
className={`w-full rounded-full border px-3 py-2 text-xs transition-colors ${
|
|
385
|
+
isActive
|
|
386
|
+
? 'border-primary/70 bg-primary/10 text-foreground'
|
|
387
|
+
: 'border-border bg-muted/40 text-muted-foreground hover:border-muted-foreground/40 hover:bg-muted/60'
|
|
388
|
+
}`}
|
|
389
|
+
onClick={() => applyThemeById(preset.id)}
|
|
390
|
+
>
|
|
391
|
+
<span className="flex items-center justify-between gap-3">
|
|
392
|
+
<span className="flex items-center gap-2">
|
|
393
|
+
{previewColors.length > 0 && (
|
|
394
|
+
<span className="flex gap-1">
|
|
395
|
+
{previewColors.map((color, index) => (
|
|
396
|
+
<span
|
|
397
|
+
key={index}
|
|
398
|
+
className="inline-block h-2.5 w-2.5 rounded-full border border-foreground/10 shadow-sm"
|
|
399
|
+
style={{ backgroundColor: formatColor(color, 'hex') }}
|
|
400
|
+
/>
|
|
401
|
+
))}
|
|
402
|
+
</span>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
<span className="text-xs font-medium text-foreground">{preset.label}</span>
|
|
406
|
+
</span>
|
|
407
|
+
|
|
408
|
+
{isActive && (
|
|
409
|
+
<span className="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-foreground">
|
|
410
|
+
Aktif
|
|
411
|
+
</span>
|
|
412
|
+
)}
|
|
413
|
+
</span>
|
|
414
|
+
</button>
|
|
415
|
+
);
|
|
416
|
+
})}
|
|
417
|
+
|
|
418
|
+
{presets.length === 0 && <p className="text-xs text-muted-foreground">Belum ada tema yang tersedia.</p>}
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
328
424
|
---
|
|
329
425
|
|
|
330
426
|
## 🧾 API Reference
|
|
@@ -349,7 +445,7 @@ export function PresetPicker() {
|
|
|
349
445
|
| `modeStorageKey` | `string` | `'theme-engine-theme'` | `localStorage` key for mode |
|
|
350
446
|
| `presetStorageKey` | `string` | `'theme-preset'` | `localStorage` key for preset |
|
|
351
447
|
| `customPresets` | `Record<string, TweakCNThemePreset>` | `undefined` | Add your own presets (can override built-ins by ID) |
|
|
352
|
-
| `
|
|
448
|
+
| `Pre-hydration script` | n/a | always on | `ThemeProvider` always injects a pre-hydration script for preset restoration |
|
|
353
449
|
|
|
354
450
|
### `useThemeEngine()`
|
|
355
451
|
|
|
@@ -382,15 +478,6 @@ Return fields:
|
|
|
382
478
|
| `builtInPresets` | `Record<string, TweakCNThemePreset>` | Built-in only |
|
|
383
479
|
| `customPresets` | `Record<string, TweakCNThemePreset>` | Custom only |
|
|
384
480
|
|
|
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
481
|
### Utilities
|
|
395
482
|
|
|
396
483
|
| Export | Description |
|
|
@@ -410,7 +497,7 @@ Note: the thrown error string might mention `useTheme` because `useThemeEngine()
|
|
|
410
497
|
|
|
411
498
|
### Preset doesn’t apply on refresh
|
|
412
499
|
|
|
413
|
-
`ThemeProvider` injects
|
|
500
|
+
`ThemeProvider` injects a pre-hydration script automatically. Avoid injecting another preset-restoration script manually (you may end up with duplicates).
|
|
414
501
|
|
|
415
502
|
### Styles don’t load / components look unstyled
|
|
416
503
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fakhrirafiki/theme-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Elegant theming system with smooth transitions, custom presets, semantic accent colors, and complete shadcn/ui support for modern React applications",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|