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 (
To use custom fonts, embed them in your project. See{" "} Tailwind docs {" "} @@ -338,7 +321,7 @@ const ThemeControlPanel = ({ - + Sans-Serif Font - + Serif Font - + Monospace Font - updateStyle("letter-spacing", `${value}em`) - } + value={parseFloat(currentStyles["letter-spacing"]?.replace("em", ""))} + onChange={(value) => updateStyle("letter-spacing", `${value}em`)} min={-0.5} max={0.5} step={0.025} @@ -395,6 +374,10 @@ const ThemeControlPanel = ({ + + + + { if (key === "shadow-color") { updateStyle(key, value as string); diff --git a/config/theme.ts b/config/theme.ts index cfe91849..ab2fe852 100644 --- a/config/theme.ts +++ b/config/theme.ts @@ -1,4 +1,4 @@ -import { ThemeEditorState } from "../types/theme"; +import { ThemeEditorState } from "../types/editor"; // these are common between light and dark modes // we can assume that light mode's value will be used for dark mode as well @@ -19,8 +19,7 @@ export const COMMON_STYLES = [ export const DEFAULT_FONT_SANS = "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'"; -export const DEFAULT_FONT_SERIF = - 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif'; +export const DEFAULT_FONT_SERIF = 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif'; export const DEFAULT_FONT_MONO = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'; @@ -127,8 +126,12 @@ export const defaultThemeState: ThemeEditorState = { dark: defaultDarkThemeStyles, }, currentMode: - typeof window !== "undefined" && - window.matchMedia("(prefers-color-scheme: dark)").matches + typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light", + hslAdjustments: { + hueShift: 0, + saturationScale: 1, + lightnessScale: 1, + }, }; diff --git a/store/editor-store.ts b/store/editor-store.ts index e73719e8..d4c251f8 100644 --- a/store/editor-store.ts +++ b/store/editor-store.ts @@ -12,6 +12,7 @@ interface EditorStore { applyThemePreset: (preset: string) => void; saveThemeCheckpoint: () => void; restoreThemeCheckpoint: () => void; + resetToCurrentPreset: () => void; hasThemeChangedFromCheckpoint: () => boolean; hasUnsavedChanges: () => boolean; } @@ -31,12 +32,12 @@ export const useEditorStore = create()( ...themeState, preset, styles: newStyles, + hslAdjustments: defaultThemeState.hslAdjustments, + }; + const updates: Partial & { themeState: ThemeEditorState } = { + themeState: newThemeState, + themeCheckpoint: newThemeState, }; - const updates: Partial & { themeState: ThemeEditorState } = - { - themeState: newThemeState, - themeCheckpoint: newThemeState, - }; set(updates); }, saveThemeCheckpoint: () => { @@ -61,10 +62,27 @@ export const useEditorStore = create()( }, hasUnsavedChanges: () => { const themeState = get().themeState; - const presetThemeStyles = getPresetThemeStyles( - themeState.preset ?? "default" + const presetThemeStyles = getPresetThemeStyles(themeState.preset ?? "default"); + const stylesChanged = !isDeepEqual(themeState.styles, presetThemeStyles); + const hslChanged = !isDeepEqual( + themeState.hslAdjustments, + defaultThemeState.hslAdjustments ); - return !isDeepEqual(themeState.styles, presetThemeStyles); + return stylesChanged || hslChanged; + }, + resetToCurrentPreset: () => { + const themeState = get().themeState; + const presetThemeStyles = getPresetThemeStyles(themeState.preset ?? "default"); + const newThemeState: ThemeEditorState = { + ...themeState, + styles: presetThemeStyles, + preset: themeState.preset, + hslAdjustments: defaultThemeState.hslAdjustments, + }; + set({ + themeState: newThemeState, + themeCheckpoint: newThemeState, + }); }, }), { diff --git a/types/editor.ts b/types/editor.ts index 4a8a5ec1..0ed09699 100644 --- a/types/editor.ts +++ b/types/editor.ts @@ -19,6 +19,11 @@ export interface ThemeEditorState extends BaseEditorState { preset?: string; styles: ThemeStyles; currentMode: "light" | "dark"; + hslAdjustments?: { + hueShift: number; + saturationScale: number; + lightnessScale: number; + }; } // Type for available editors diff --git a/types/theme.ts b/types/theme.ts index 29024e8e..b18ebf18 100644 --- a/types/theme.ts +++ b/types/theme.ts @@ -57,11 +57,6 @@ export const themeStylesSchema = z.object({ export type ThemeStyleProps = z.infer; export type ThemeStyles = z.infer; -export interface ThemeEditorState { - styles: ThemeStyles; - currentMode: "light" | "dark"; -} - export interface ThemeEditorPreviewProps { styles: ThemeStyles; currentMode: "light" | "dark"; diff --git a/utils/color-converter.ts b/utils/color-converter.ts index 5274e829..f84381ba 100644 --- a/utils/color-converter.ts +++ b/utils/color-converter.ts @@ -1,11 +1,16 @@ import * as culori from "culori"; import { ColorFormat } from "../types"; +import { Hsl } from "culori"; -const formatNumber = (num?: number) => { +export const formatNumber = (num?: number) => { if (!num) return "0"; return num % 1 === 0 ? num : num.toFixed(2); }; +export const formatHsl = (hsl: Hsl) => { + return `hsl(${formatNumber(hsl.h)} ${formatNumber(hsl.s * 100)}% ${formatNumber(hsl.l * 100)}%)`; +}; + export const colorFormatter = ( colorValue: string, format: ColorFormat = "hsl", @@ -19,7 +24,7 @@ export const colorFormatter = ( case "hsl": { const hsl = culori.converter("hsl")(color); if (tailwindVersion === "4") { - return `hsl(${formatNumber(hsl.h)} ${formatNumber(hsl.s * 100)}% ${formatNumber(hsl.l * 100)}%)`; + return formatHsl(hsl); } return `${formatNumber(hsl.h)} ${formatNumber(hsl.s * 100)}% ${formatNumber(hsl.l * 100)}%`; } @@ -40,5 +45,4 @@ export const colorFormatter = ( } }; -export const convertToHSL = (colorValue: string): string => - colorFormatter(colorValue, "hsl"); +export const convertToHSL = (colorValue: string): string => colorFormatter(colorValue, "hsl"); diff --git a/utils/shadows.ts b/utils/shadows.ts index 7500f1f6..f1f596b7 100644 --- a/utils/shadows.ts +++ b/utils/shadows.ts @@ -1,6 +1,6 @@ import { colorFormatter } from "./color-converter"; import { applyStyleToElement } from "./apply-style-to-element"; -import { ThemeEditorState } from "../types/theme"; +import { ThemeEditorState } from "../types/editor"; import { defaultThemeState } from "../config/theme"; export const getShadowMap = (themeEditorState: ThemeEditorState) => { @@ -28,8 +28,7 @@ export const getShadowMap = (themeEditorState: ThemeEditorState) => { // Use the fixed blur specific to the shadow size const blur2 = fixedBlur; // Calculate spread relative to the first layer's spread variable - const spread2 = - (parseFloat(spread?.replace("px", "") ?? "0") - 1).toString() + "px"; + const spread2 = (parseFloat(spread?.replace("px", "") ?? "0") - 1).toString() + "px"; // Use the same color function (opacity can still be overridden by --shadow-opacity) const color2 = color(1.0); // Default opacity for second layer is 0.1 in examples @@ -47,9 +46,7 @@ export const getShadowMap = (themeEditorState: ThemeEditorState) => { "shadow-sm": `${offsetX} ${offsetY} ${blur} ${spread} ${color( 1.0 )}, ${secondLayer("1px", "2px")}`, - shadow: `${offsetX} ${offsetY} ${blur} ${spread} ${color( - 1.0 - )}, ${secondLayer("1px", "2px")}`, // Alias for the 'shadow:' example line + shadow: `${offsetX} ${offsetY} ${blur} ${spread} ${color(1.0)}, ${secondLayer("1px", "2px")}`, // Alias for the 'shadow:' example line "shadow-md": `${offsetX} ${offsetY} ${blur} ${spread} ${color( 1.0 diff --git a/utils/theme-fonts.ts b/utils/theme-fonts.ts index 07785280..7f8b5ebf 100644 --- a/utils/theme-fonts.ts +++ b/utils/theme-fonts.ts @@ -1,4 +1,4 @@ -import { ThemeEditorState, ThemeStyleProps } from "../types/theme"; +import { ThemeEditorState } from "../types/editor"; const sansSerifFontNames = [ "Inter", diff --git a/utils/theme-style-generator.ts b/utils/theme-style-generator.ts index 7b71677e..7e4c20bb 100644 --- a/utils/theme-style-generator.ts +++ b/utils/theme-style-generator.ts @@ -1,8 +1,9 @@ -import { ThemeEditorState, ThemeStyles } from "@/types/theme"; +import { ThemeEditorState } from "@/types/editor"; import { colorFormatter } from "./color-converter"; import { ColorFormat } from "../types"; import { getShadowMap } from "./shadows"; import { defaultLightThemeStyles } from "@/config/theme"; +import { ThemeStyles } from "@/types/theme"; type ThemeMode = "light" | "dark"; @@ -47,10 +48,7 @@ const generateColorVariables = ( --sidebar-ring: ${formatColor(styles["sidebar-ring"])};`; }; -const generateFontVariables = ( - themeStyles: ThemeStyles, - mode: ThemeMode -): string => { +const generateFontVariables = (themeStyles: ThemeStyles, mode: ThemeMode): string => { const styles = themeStyles[mode]; return ` --font-sans: ${styles["font-sans"]}; @@ -98,15 +96,13 @@ const generateThemeVariables = ( getShadowMap({ styles: themeStyles, currentMode: mode }) ); const spacingVar = - mode === "light" && - themeStyles["light"].spacing !== defaultLightThemeStyles.spacing + mode === "light" && themeStyles["light"].spacing !== defaultLightThemeStyles.spacing ? `\n --spacing: ${themeStyles["light"].spacing};` : ""; const trackingVars = mode === "light" && - themeStyles["light"]["letter-spacing"] !== - defaultLightThemeStyles["letter-spacing"] + themeStyles["light"]["letter-spacing"] !== defaultLightThemeStyles["letter-spacing"] ? `\n --tracking-normal: ${themeStyles["light"]["letter-spacing"]};` : ""; @@ -192,15 +188,12 @@ export const generateThemeCode = ( } const themeStyles = themeEditorState.styles as ThemeStyles; - const formatColor = (color: string) => - colorFormatter(color, colorFormat, tailwindVersion); + const formatColor = (color: string) => colorFormatter(color, colorFormat, tailwindVersion); const lightTheme = generateThemeVariables(themeStyles, "light", formatColor); const darkTheme = generateThemeVariables(themeStyles, "dark", formatColor); const tailwindV4Theme = - tailwindVersion === "4" - ? `\n\n${generateTailwindV4ThemeInline(themeStyles)}` - : ""; + tailwindVersion === "4" ? `\n\n${generateTailwindV4ThemeInline(themeStyles)}` : ""; const bodyLetterSpacing = themeStyles["light"]["letter-spacing"] !== "0em"