diff --git a/components/editor/action-bar/components/action-bar-buttons.tsx b/components/editor/action-bar/components/action-bar-buttons.tsx index e6d4b1ea..4b75c123 100644 --- a/components/editor/action-bar/components/action-bar-buttons.tsx +++ b/components/editor/action-bar/components/action-bar-buttons.tsx @@ -28,28 +28,22 @@ export function ActionBarButtons({ onShareClick, isSaving, }: ActionBarButtonsProps) { - const { themeState, restoreThemeCheckpoint, hasThemeChangedFromCheckpoint } = - useEditorStore(); + const { themeState, resetToCurrentPreset, hasThemeChangedFromCheckpoint } = useEditorStore(); const { getPreset } = useThemePresetStore(); - const currentPreset = themeState?.preset - ? getPreset(themeState?.preset) - : undefined; + const currentPreset = themeState?.preset ? getPreset(themeState?.preset) : undefined; const isSavedPreset = !!currentPreset && currentPreset.source === "SAVED"; return (
- + - - + + - + {isSavedPreset && } onShareClick(themeState.preset)} /> diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index 8aa93c42..78fe0c90 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -1,11 +1,7 @@ "use client"; import React, { useEffect, use } from "react"; -import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, -} from "@/components/ui/resizable"; +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { EditorConfig } from "@/types/editor"; import { Theme, ThemeStyles } from "@/types/theme"; @@ -33,15 +29,10 @@ const isThemeStyles = (styles: unknown): styles is ThemeStyles => { const Editor: React.FC = ({ config, themePromise }) => { const themeState = useEditorStore((state) => state.themeState); const setThemeState = useEditorStore((state) => state.setThemeState); - const saveThemeCheckpoint = useEditorStore( - (state) => state.saveThemeCheckpoint - ); const Controls = config.controls; const Preview = config.preview; - const loadSavedPresets = useThemePresetStore( - (state) => state.loadSavedPresets - ); + const loadSavedPresets = useThemePresetStore((state) => state.loadSavedPresets); const { data: session } = authClient.useSession(); @@ -64,14 +55,17 @@ const Editor: React.FC = ({ config, themePromise }) => { useEffect(() => { if (initialTheme && isThemeStyles(initialTheme.styles)) { const prev = useEditorStore.getState().themeState; - setThemeState({ ...prev, styles: initialTheme.styles }); - saveThemeCheckpoint(); + setThemeState({ + ...prev, + styles: initialTheme.styles, + preset: initialTheme.id, + }); } - }, [initialTheme, setThemeState, saveThemeCheckpoint]); + }, [initialTheme, setThemeState]); if (initialTheme && !isThemeStyles(initialTheme.styles)) { return ( -
+
Fetched theme data is invalid.
); @@ -82,10 +76,10 @@ const Editor: React.FC = ({ config, themePromise }) => { return (
{/* Desktop Layout */} -
+
-
+
= ({ config, themePromise }) => { -
-
+
+
@@ -111,15 +105,15 @@ const Editor: React.FC = ({ config, themePromise }) => { - + Controls Preview - -
+ +
= ({ config, themePromise }) => { />
- -
+ +
diff --git a/components/editor/hsl-adjustment-controls.tsx b/components/editor/hsl-adjustment-controls.tsx new file mode 100644 index 00000000..4b91e81d --- /dev/null +++ b/components/editor/hsl-adjustment-controls.tsx @@ -0,0 +1,158 @@ +"use client"; + +import React, { useCallback, useMemo, useRef, useEffect } from "react"; +import { SliderWithInput } from "./slider-with-input"; +import { useEditorStore } from "../../store/editor-store"; +import { COMMON_STYLES, defaultThemeState } from "../../config/theme"; +import { ThemeEditorState } from "@/types/editor"; +import { converter, formatHex, Hsl } from "culori"; +import { debounce } from "@/utils/debounce"; +import { isDeepEqual } from "@/lib/utils"; + +// Adjusts a color by modifying HSL values +function adjustColorByHsl( + color: string, + hueShift: number, + saturationScale: number, + lightnessScale: number +): string { + const hsl = converter("hsl")(color); + const h = hsl?.h; + const s = hsl?.s; + const l = hsl?.l; + + if (h === undefined || s === undefined || l === undefined) { + return color; + } + + const adjustedHsl = { + h: (((h + hueShift) % 360) + 360) % 360, + s: Math.min(1, Math.max(0, s * saturationScale)), + l: Math.min(1, Math.max(0.1, l * lightnessScale)), + }; + + const out = converter("hsl")(adjustedHsl as Hsl); + return formatHex(out); +} + +const HslAdjustmentControls = () => { + const { themeState, setThemeState, saveThemeCheckpoint, themeCheckpoint } = useEditorStore(); + const debouncedUpdateRef = useRef | null>(null); + + // Get current HSL adjustments with fallback to defaults + const currentHslAdjustments = useMemo( + () => themeState.hslAdjustments ?? defaultThemeState.hslAdjustments!, + [themeState.hslAdjustments] + ); + + // Save checkpoint if HSL adjustments are at default values + useEffect(() => { + if (isDeepEqual(themeState.hslAdjustments, defaultThemeState.hslAdjustments)) { + saveThemeCheckpoint(); + } + }, [themeState.hslAdjustments, saveThemeCheckpoint]); + + // Setup debounced update function + useEffect(() => { + debouncedUpdateRef.current = debounce((hslAdjustments: ThemeEditorState["hslAdjustments"]) => { + const { + hueShift = defaultThemeState.hslAdjustments!.hueShift, + saturationScale = defaultThemeState.hslAdjustments!.saturationScale, + lightnessScale = defaultThemeState.hslAdjustments!.lightnessScale, + } = hslAdjustments ?? {}; + + const adjustments = { hueShift, saturationScale, lightnessScale }; + const state = themeCheckpoint ?? themeState; + const { light: lightStyles, dark: darkStyles } = state.styles; + + const updatedLightStyles = Object.keys(lightStyles) + .filter((key) => !COMMON_STYLES.includes(key)) + .reduce>((acc, key) => { + const colorKey = key as keyof typeof lightStyles; + return { + ...acc, + [key]: adjustColorByHsl( + lightStyles[colorKey], + adjustments.hueShift, + adjustments.saturationScale, + adjustments.lightnessScale + ), + }; + }, {}); + + const updatedDarkStyles = Object.keys(darkStyles) + .filter((key) => !COMMON_STYLES.includes(key)) + .reduce>((acc, key) => { + const colorKey = key as keyof typeof darkStyles; + return { + ...acc, + [key]: adjustColorByHsl( + darkStyles[colorKey], + adjustments.hueShift, + adjustments.saturationScale, + adjustments.lightnessScale + ), + }; + }, {}); + + // Update theme state with all changes + setThemeState({ + ...state, + hslAdjustments, + styles: { + light: { ...lightStyles, ...updatedLightStyles }, + dark: { ...darkStyles, ...updatedDarkStyles }, + }, + }); + }, 10); + + return () => debouncedUpdateRef.current?.cancel(); + }, [themeState, setThemeState, themeCheckpoint]); + + // Handle HSL value changes + const handleHslChange = useCallback( + (property: keyof typeof currentHslAdjustments, value: number) => { + if (debouncedUpdateRef.current) { + debouncedUpdateRef.current({ + ...currentHslAdjustments, + [property]: value, + }); + } + }, + [currentHslAdjustments] + ); + + return ( + <> + handleHslChange("hueShift", value)} + unit="deg" + min={-180} + max={180} + step={1} + label="Hue Shift" + /> + handleHslChange("saturationScale", value)} + unit="x" + min={0} + max={2} + step={0.01} + label="Saturation Multiplier" + /> + handleHslChange("lightnessScale", value)} + unit="x" + min={0.2} + max={2} + step={0.01} + label="Lightness Multiplier" + /> + + ); +}; + +export default HslAdjustmentControls; diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index c06472a9..72b1e45f 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -30,6 +30,7 @@ import ShadowControl from "./shadow-control"; import TabsTriggerPill from "./theme-preview/tabs-trigger-pill"; import ThemeEditActions from "./theme-edit-actions"; import { useThemePresetStore } from "@/store/theme-preset-store"; +import HslAdjustmentControls from "./hsl-adjustment-controls"; const ThemeControlPanel = ({ styles, @@ -49,10 +50,7 @@ const ThemeControlPanel = ({ ); const updateStyle = React.useCallback( - ( - key: K, - value: (typeof currentStyles)[K] - ) => { + (key: K, value: (typeof currentStyles)[K]) => { // apply common styles to both light and dark modes if (COMMON_STYLES.includes(key)) { onChange({ @@ -96,26 +94,19 @@ const ThemeControlPanel = ({ )}
-
- -
- +
+ +
+ Colors Typography Other
- + - + updateStyle("primary", color)} @@ -136,9 +127,7 @@ const ThemeControlPanel = ({ /> - updateStyle("secondary-foreground", color) - } + onChange={(color) => updateStyle("secondary-foreground", color)} label="Secondary Foreground" /> @@ -216,9 +205,7 @@ const ThemeControlPanel = ({ /> - updateStyle("destructive-foreground", color) - } + onChange={(color) => updateStyle("destructive-foreground", color)} label="Destructive Foreground" /> @@ -287,9 +274,7 @@ const ThemeControlPanel = ({ /> - updateStyle("sidebar-primary-foreground", color) - } + onChange={(color) => updateStyle("sidebar-primary-foreground", color)} label="Sidebar Primary Foreground" /> - updateStyle("sidebar-accent-foreground", color) - } + onChange={(color) => updateStyle("sidebar-accent-foreground", color)} label="Sidebar Accent Foreground" /> -
- -
+
+ +

To use custom fonts, embed them in your project.
See{" "} Tailwind docs {" "} @@ -338,7 +321,7 @@ const ThemeControlPanel = ({

-