From a911e2643a3a81139b806c17fe44f5fe968b2b35 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Thu, 17 Apr 2025 20:16:53 +0530 Subject: [PATCH 01/11] action bar intro --- app/editor/theme/page.tsx | 1 + components/editor/action-bar.tsx | 94 +++ components/editor/code-panel-dialog.tsx | 28 + components/editor/code-panel.tsx | 21 +- components/editor/editor.tsx | 37 +- components/editor/theme-control-panel.tsx | 719 +++++++++++----------- components/editor/theme-preset-select.tsx | 76 ++- components/editor/theme-preview-panel.tsx | 267 ++++---- 8 files changed, 662 insertions(+), 581 deletions(-) create mode 100644 components/editor/action-bar.tsx create mode 100644 components/editor/code-panel-dialog.tsx diff --git a/app/editor/theme/page.tsx b/app/editor/theme/page.tsx index 48721c70..9b748548 100644 --- a/app/editor/theme/page.tsx +++ b/app/editor/theme/page.tsx @@ -3,6 +3,7 @@ import { cn } from "@/lib/utils"; import Editor from "@/components/editor/editor"; import { Metadata } from "next"; import { Header } from "../../../components/editor/header"; +import { ActionBar } from "@/components/editor/action-bar"; export const metadata: Metadata = { title: "tweakcn — Theme Generator for shadcn/ui", diff --git a/components/editor/action-bar.tsx b/components/editor/action-bar.tsx new file mode 100644 index 00000000..87a7cbba --- /dev/null +++ b/components/editor/action-bar.tsx @@ -0,0 +1,94 @@ +"use client"; + +import ThemePresetSelect from "./theme-preset-select"; +import { useEditorStore } from "@/store/editor-store"; +import { getPresetThemeStyles, presets } from "@/utils/theme-presets"; +import { Button } from "../ui/button"; +import { FileCode, RefreshCw, Code } from "lucide-react"; +import CssImportDialog from "./css-import-dialog"; +import { useState } from "react"; +import { parseCssInput } from "@/utils/parse-css-input"; +import { toast } from "../ui/use-toast"; +import { CodePanelDialog } from "./code-panel-dialog"; +import { Separator } from "../ui/separator"; + +export function ActionBar() { + const { themeState, applyThemePreset, resetToCurrentPreset, setThemeState } = + useEditorStore(); + const [cssImportOpen, setCssImportOpen] = useState(false); + const [codePanelOpen, setCodePanelOpen] = useState(false); + + const handlePresetChange = (preset: string) => { + applyThemePreset(preset); + }; + + const handleCssImport = (css: string) => { + // This just shows a success toast for now + const { lightColors, darkColors } = parseCssInput(css); + const styles = { + ...themeState.styles, + light: { ...themeState.styles.light, ...lightColors }, + dark: { ...themeState.styles.dark, ...darkColors }, + }; + + setThemeState({ + ...themeState, + styles, + }); + + // The actual CSS parsing and theme application logic would be implemented later + toast({ + title: "CSS imported", + description: "Your custom CSS has been imported successfully", + }); + }; + + return ( +
+
+
+ + + + + +
+
+ + + +
+ ); +} diff --git a/components/editor/code-panel-dialog.tsx b/components/editor/code-panel-dialog.tsx new file mode 100644 index 00000000..ebddea13 --- /dev/null +++ b/components/editor/code-panel-dialog.tsx @@ -0,0 +1,28 @@ +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import CodePanel from "./code-panel"; +import { ThemeEditorState } from "@/types/editor"; + +interface CodePanelDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + themeEditorState: ThemeEditorState; +} + +export function CodePanelDialog({ + open, + onOpenChange, + themeEditorState, +}: CodePanelDialogProps) { + return ( + + +
+ onOpenChange(false)} + /> +
+
+
+ ); +} diff --git a/components/editor/code-panel.tsx b/components/editor/code-panel.tsx index 63fc5850..8ad38f44 100644 --- a/components/editor/code-panel.tsx +++ b/components/editor/code-panel.tsx @@ -22,13 +22,9 @@ import { import { generateThemeCode } from "@/utils/theme-style-generator"; interface CodePanelProps { themeEditorState: ThemeEditorState; - onCodePanelToggle: () => void; } -const CodePanel: React.FC = ({ - themeEditorState, - onCodePanelToggle, -}) => { +const CodePanel: React.FC = ({ themeEditorState }) => { const [registryCopied, setRegistryCopied] = useState(false); const [copied, setCopied] = useState(false); const posthog = usePostHog(); @@ -102,20 +98,7 @@ const CodePanel: React.FC = ({
-

Code

- - - - - Collapse Code Panel - +

Theme Code

{preset && preset !== "default" && (
diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index a014af82..13ab847f 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -13,7 +13,6 @@ import { ThemeEditorState, } from "@/types/editor"; import { ThemeStyles } from "@/types/theme"; -import CodePanel from "./code-panel"; import { Sliders } from "lucide-react"; import { useEditorStore } from "@/store/editor-store"; @@ -36,7 +35,6 @@ const Editor: React.FC = ({ config }) => { const { themeState, setThemeState } = useEditorStore(); const Controls = config.controls; const Preview = config.preview; - const [isCodePanelOpen, setIsCodePanelOpen] = useState(true); const handleStyleChange = (newStyles: ThemeStyles) => { setThemeState({ ...themeState, styles: newStyles }); @@ -53,7 +51,7 @@ const Editor: React.FC = ({ config }) => {
-
+
= ({ config }) => {
-
- setIsCodePanelOpen(!isCodePanelOpen)} - /> +
+
- {isCodePanelOpen && ( - <> - - - setIsCodePanelOpen(!isCodePanelOpen)} - /> - - - )}
@@ -114,20 +96,9 @@ const Editor: React.FC = ({ config }) => {
- setIsCodePanelOpen(!isCodePanelOpen)} - /> +
- - setIsCodePanelOpen(!isCodePanelOpen)} - /> -
diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index 3340f558..b3bc2fc6 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -32,6 +32,7 @@ import { toast } from "../ui/use-toast"; import { parseCssInput } from "../../utils/parse-css-input"; import ShadowControl from "./shadow-control"; import ThemeControlActions from "./theme-control-actions"; +import TabsTriggerPill from "./theme-preview/tabs-trigger-pill"; const ThemeControlPanel = ({ styles, @@ -82,22 +83,6 @@ const ThemeControlPanel = ({ [onChange, styles, currentMode, currentStyles] ); - const handleCssImport = (css: string) => { - // This just shows a success toast for now - const { lightColors, darkColors } = parseCssInput(css); - onChange({ - ...styles, - light: { ...styles.light, ...lightColors }, - dark: { ...styles.dark, ...darkColors }, - }); - - // The actual CSS parsing and theme application logic would be implemented later - toast({ - title: "CSS imported", - description: "Your custom CSS has been imported successfully", - }); - }; - // Ensure we have valid styles for the current mode if (!currentStyles) { return null; // Or some fallback UI @@ -106,375 +91,365 @@ const ThemeControlPanel = ({ const radius = parseFloat(currentStyles.radius.replace("rem", "")); return ( -
-
-
-

Theme Editor

-
- setCssImportOpen(true)} - /> -
- -
+ <> +
+
+ +
+ + Colors + Typography + Other + +
- - - Colors - Typography - Other - - - - - - updateStyle("primary", color)} - label="Primary" - /> - updateStyle("primary-foreground", color)} - label="Primary Foreground" - /> - - - - updateStyle("secondary", color)} - label="Secondary" - /> - updateStyle("secondary-foreground", color)} - label="Secondary Foreground" - /> - - - - updateStyle("accent", color)} - label="Accent" - /> - updateStyle("accent-foreground", color)} - label="Accent Foreground" - /> - - - - updateStyle("background", color)} - label="Background" - /> - updateStyle("foreground", color)} - label="Foreground" - /> - - - - updateStyle("card", color)} - label="Card Background" - /> - updateStyle("card-foreground", color)} - label="Card Foreground" - /> - - - - updateStyle("popover", color)} - label="Popover Background" - /> - updateStyle("popover-foreground", color)} - label="Popover Foreground" - /> - - - - updateStyle("muted", color)} - label="Muted" - /> - updateStyle("muted-foreground", color)} - label="Muted Foreground" - /> - - - - updateStyle("destructive", color)} - label="Destructive" - /> - - updateStyle("destructive-foreground", color) - } - label="Destructive Foreground" - /> - - - - updateStyle("border", color)} - label="Border" - /> - updateStyle("input", color)} - label="Input" - /> - updateStyle("ring", color)} - label="Ring" - /> - - - - updateStyle("chart-1", color)} - label="Chart 1" - /> - updateStyle("chart-2", color)} - label="Chart 2" - /> - updateStyle("chart-3", color)} - label="Chart 3" - /> - updateStyle("chart-4", color)} - label="Chart 4" - /> - updateStyle("chart-5", color)} - label="Chart 5" - /> - - - - updateStyle("sidebar", color)} - label="Sidebar Background" - /> - updateStyle("sidebar-foreground", color)} - label="Sidebar Foreground" - /> - updateStyle("sidebar-primary", color)} - label="Sidebar Primary" - /> - - updateStyle("sidebar-primary-foreground", color) - } - label="Sidebar Primary Foreground" - /> - updateStyle("sidebar-accent", color)} - label="Sidebar Accent" - /> - - updateStyle("sidebar-accent-foreground", color) - } - label="Sidebar Accent Foreground" - /> - updateStyle("sidebar-border", color)} - label="Sidebar Border" - /> - updateStyle("sidebar-ring", color)} - label="Sidebar Ring" - /> - - - - -
- -
-

- To use custom fonts, embed them in your project.
- See{" "} - - Tailwind docs - {" "} - for details. -

-
-
- - -
- - updateStyle("font-sans", value)} + + + + updateStyle("primary", color)} + label="Primary" /> -
- - - -
- - updateStyle("font-serif", value)} + updateStyle("primary-foreground", color)} + label="Primary Foreground" /> -
+
- -
- - updateStyle("font-mono", value)} + + updateStyle("secondary", color)} + label="Secondary" /> -
- - - - - updateStyle("letter-spacing", `${value}em`) - } - min={-0.5} - max={0.5} - step={0.025} - unit="em" - label="Letter Spacing" - /> - -
- - - - updateStyle("radius", `${value}rem`)} - min={0} - max={5} - step={0.025} - unit="rem" - label="Radius" - /> - - - - updateStyle("spacing", `${value}rem`)} - min={0.15} - max={0.35} - step={0.01} - unit="rem" - label="Spacing" - /> - -
- { - if (key === "shadow-color") { - updateStyle(key, value as string); - } else if (key === "shadow-opacity") { - updateStyle(key, value.toString()); - } else { - updateStyle(key as keyof ThemeStyleProps, `${value}px`); + + updateStyle("secondary-foreground", color) } - }} - /> -
-
-
-
+ label="Secondary Foreground" + /> + - -
+ + updateStyle("accent", color)} + label="Accent" + /> + updateStyle("accent-foreground", color)} + label="Accent Foreground" + /> + + + + updateStyle("background", color)} + label="Background" + /> + updateStyle("foreground", color)} + label="Foreground" + /> + + + + updateStyle("card", color)} + label="Card Background" + /> + updateStyle("card-foreground", color)} + label="Card Foreground" + /> + + + + updateStyle("popover", color)} + label="Popover Background" + /> + updateStyle("popover-foreground", color)} + label="Popover Foreground" + /> + + + + updateStyle("muted", color)} + label="Muted" + /> + updateStyle("muted-foreground", color)} + label="Muted Foreground" + /> + + + + updateStyle("destructive", color)} + label="Destructive" + /> + + updateStyle("destructive-foreground", color) + } + label="Destructive Foreground" + /> + + + + updateStyle("border", color)} + label="Border" + /> + updateStyle("input", color)} + label="Input" + /> + updateStyle("ring", color)} + label="Ring" + /> + + + + updateStyle("chart-1", color)} + label="Chart 1" + /> + updateStyle("chart-2", color)} + label="Chart 2" + /> + updateStyle("chart-3", color)} + label="Chart 3" + /> + updateStyle("chart-4", color)} + label="Chart 4" + /> + updateStyle("chart-5", color)} + label="Chart 5" + /> + + + + updateStyle("sidebar", color)} + label="Sidebar Background" + /> + updateStyle("sidebar-foreground", color)} + label="Sidebar Foreground" + /> + updateStyle("sidebar-primary", color)} + label="Sidebar Primary" + /> + + updateStyle("sidebar-primary-foreground", color) + } + label="Sidebar Primary Foreground" + /> + updateStyle("sidebar-accent", color)} + label="Sidebar Accent" + /> + + updateStyle("sidebar-accent-foreground", color) + } + label="Sidebar Accent Foreground" + /> + updateStyle("sidebar-border", color)} + label="Sidebar Border" + /> + updateStyle("sidebar-ring", color)} + label="Sidebar Ring" + /> + + + + +
+ +
+

+ To use custom fonts, embed them in your project.
+ See{" "} + + Tailwind docs + {" "} + for details. +

+
+
+ + +
+ + updateStyle("font-sans", value)} + /> +
+ + + +
+ + updateStyle("font-serif", value)} + /> +
+ + +
+ + updateStyle("font-mono", value)} + /> +
+
+ + + + updateStyle("letter-spacing", `${value}em`) + } + min={-0.5} + max={0.5} + step={0.025} + unit="em" + label="Letter Spacing" + /> + +
+ + + + updateStyle("radius", `${value}rem`)} + min={0} + max={5} + step={0.025} + unit="rem" + label="Radius" + /> + + + + updateStyle("spacing", `${value}rem`)} + min={0.15} + max={0.35} + step={0.01} + unit="rem" + label="Spacing" + /> + +
+ { + if (key === "shadow-color") { + updateStyle(key, value as string); + } else if (key === "shadow-opacity") { + updateStyle(key, value.toString()); + } else { + updateStyle(key as keyof ThemeStyleProps, `${value}px`); + } + }} + /> +
+
+ + +
+ ); }; diff --git a/components/editor/theme-preset-select.tsx b/components/editor/theme-preset-select.tsx index 7476f6bc..ce0d2b8e 100644 --- a/components/editor/theme-preset-select.tsx +++ b/components/editor/theme-preset-select.tsx @@ -1,5 +1,9 @@ import React, { useCallback, useMemo, useState } from "react"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { ThemePreset } from "../../types/theme"; import { useEditorStore } from "../../store/editor-store"; import { getPresetThemeStyles } from "../../utils/theme-presets"; @@ -17,7 +21,12 @@ import { import { useTheme } from "@/components/theme-provider"; import { Separator } from "../ui/separator"; import { ScrollArea } from "../ui/scroll-area"; -import { Command, CommandEmpty, CommandGroup, CommandItem } from "../ui/command"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandItem, +} from "../ui/command"; import { Tooltip, TooltipContent, @@ -62,7 +71,10 @@ const ThemePresetSelect: React.FC = ({ const mode = themeState.currentMode; const [search, setSearch] = useState(""); - const presetNames = useMemo(() => ["default", ...Object.keys(presets)], [presets]); + const presetNames = useMemo( + () => ["default", ...Object.keys(presets)], + [presets] + ); const value = presetNames?.find((name) => name === currentPreset); const currentIndex = useMemo( () => presetNames.indexOf(value || "default"), @@ -107,14 +119,17 @@ const ThemePresetSelect: React.FC = ({ }; return ( -
+
- + - +
@@ -219,7 +242,9 @@ const ThemePresetSelect: React.FC = ({ color={getPresetThemeStyles(presetName)[mode].accent} /> = ({ {presets[presetName]?.label || presetName} - {presets[presetName] && isThemeNew(presets[presetName]) && ( - - New - - )} + {presets[presetName] && + isThemeNew(presets[presetName]) && ( + + New + + )}
{presetName === value && ( @@ -251,9 +277,9 @@ const ThemePresetSelect: React.FC = ({ - - Toggle Theme - - )} - - - + + Toggle Theme + )} - - - - {isFullscreen ? "Exit full screen" : "Full screen"} - - - {!isCodePanelOpen && !isFullscreen && ( - - - + + + {isFullscreen ? "Exit full screen" : "Full screen"} + + + {!isCodePanelOpen && !isFullscreen && ( + + + + + Hide Code Panel + + )} +
+
+ + +
+ - - - - Hide Code Panel - - )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
- -
- - - Cards -
- Mail - Tasks - Music - Dashboard -
- Color Palette -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
+ ); }; From 8499dc6ec496c10ed71c804f5d769ae9df9bb904 Mon Sep 17 00:00:00 2001 From: lorenzopalaia Date: Thu, 17 Apr 2025 17:16:29 +0200 Subject: [PATCH 02/11] feat: Implement contrast checker and color picker refactor --- README.md | 2 +- components/editor/color-picker.tsx | 25 ++- components/editor/contrast-checker.tsx | 217 ++++++++++++++++++++++ components/editor/theme-control-panel.tsx | 30 +-- hooks/use-contrast-checker.ts | 56 ++++++ store/editor-store.ts | 37 +++- types/index.ts | 1 + utils/contrast-checker.ts | 45 +++++ 8 files changed, 383 insertions(+), 30 deletions(-) create mode 100644 components/editor/contrast-checker.tsx create mode 100644 hooks/use-contrast-checker.ts create mode 100644 utils/contrast-checker.ts diff --git a/README.md b/README.md index 6ed59958..5011d645 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ npm install npm run dev ``` -4. Open [http://localhost:8080](http://localhost:8080) in your browser. +4. Open [http://localhost:3000](http://localhost:3000) in your browser. ## Contributors diff --git a/components/editor/color-picker.tsx b/components/editor/color-picker.tsx index a64875bc..479da3cb 100644 --- a/components/editor/color-picker.tsx +++ b/components/editor/color-picker.tsx @@ -3,7 +3,12 @@ import { Label } from "@/components/ui/label"; import { ColorPickerProps } from "@/types"; import { debounce } from "@/utils/debounce"; -const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => { +const ColorPicker = ({ + color, + onChange, + label, + onlyShowPicker, +}: ColorPickerProps) => { const [isOpen, setIsOpen] = useState(false); const [localColor, setLocalColor] = useState(color); @@ -31,6 +36,24 @@ const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => { }; }, [debouncedOnChange]); + if (onlyShowPicker) { + return ( +
setIsOpen(!isOpen)} + > + +
+ ); + } + return (
diff --git a/components/editor/contrast-checker.tsx b/components/editor/contrast-checker.tsx new file mode 100644 index 00000000..8898973e --- /dev/null +++ b/components/editor/contrast-checker.tsx @@ -0,0 +1,217 @@ +import React from "react"; +import { useContrastChecker } from "../../hooks/use-contrast-checker"; +import { ThemeStyleProps } from "@/types/theme"; +import { Button } from "../ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogDescription, +} from "../ui/dialog"; +import ColorPicker from "./color-picker"; +import { useEditorStore } from "@/store/editor-store"; +import { Contrast, Check, AlertTriangle } from "lucide-react"; + +type ContrastCheckerProps = { + currentStyles: ThemeStyleProps | Partial; +}; + +const MIN_CONTRAST_RATIO = 4.5; + +type ColorPair = { + id: string; + foregroundId: keyof ThemeStyleProps; + backgroundId: keyof ThemeStyleProps; + foreground: string | undefined; + background: string | undefined; + label: string; +}; + +const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { + const colorPairsToCheck: ColorPair[] = [ + { + id: "primary", + foregroundId: "primary-foreground", + backgroundId: "primary", + foreground: currentStyles?.["primary-foreground"], + background: currentStyles?.["primary"], + label: "Primary", + }, + { + id: "secondary", + foregroundId: "secondary-foreground", + backgroundId: "secondary", + foreground: currentStyles?.["secondary-foreground"], + background: currentStyles?.["secondary"], + label: "Secondary", + }, + { + id: "accent", + foregroundId: "accent-foreground", + backgroundId: "accent", + foreground: currentStyles?.["accent-foreground"], + background: currentStyles?.["accent"], + label: "Accent", + }, + { + id: "base", + foregroundId: "foreground", + backgroundId: "background", + foreground: currentStyles?.["foreground"], + background: currentStyles?.["background"], + label: "Base", + }, + { + id: "card", + foregroundId: "card-foreground", + backgroundId: "card", + foreground: currentStyles?.["card-foreground"], + background: currentStyles?.["card"], + label: "Card", + }, + { + id: "popover", + foregroundId: "popover-foreground", + backgroundId: "popover", + foreground: currentStyles?.["popover-foreground"], + background: currentStyles?.["popover"], + label: "Popover", + }, + { + id: "muted", + foregroundId: "muted-foreground", + backgroundId: "muted", + foreground: currentStyles?.["muted-foreground"], + background: currentStyles?.["muted"], + label: "Muted", + }, + { + id: "destructive", + foregroundId: "destructive-foreground", + backgroundId: "destructive", + foreground: currentStyles?.["destructive-foreground"], + background: currentStyles?.["destructive"], + label: "Destructive", + }, + { + id: "sidebar", + foregroundId: "sidebar-foreground", + backgroundId: "sidebar", + foreground: currentStyles?.["sidebar-foreground"], + background: currentStyles?.["sidebar"], + label: "Sidebar Base", + }, + { + id: "sidebar-primary", + foregroundId: "sidebar-primary-foreground", + backgroundId: "sidebar-primary", + foreground: currentStyles?.["sidebar-primary-foreground"], + background: currentStyles?.["sidebar-primary"], + label: "Sidebar Primary", + }, + { + id: "sidebar-accent", + foregroundId: "sidebar-accent-foreground", + backgroundId: "sidebar-accent", + foreground: currentStyles?.["sidebar-accent-foreground"], + background: currentStyles?.["sidebar-accent"], + label: "Sidebar Accent", + }, + ]; + + const { updateStyle } = useEditorStore(); + + const validColorPairsToCheck = colorPairsToCheck.filter( + (pair): pair is ColorPair & { foreground: string; background: string } => + !!pair.foreground && !!pair.background + ); + const contrastResults = useContrastChecker(validColorPairsToCheck); + + return ( + + + + + + + Contrast Checker + + Check the contrast ratios of your theme colors to ensure they meet + accessibility standards. You can also adjust the colors using the + color pickers below. The recommended minimum contrast ratio is{" "} + {MIN_CONTRAST_RATIO}. + +
+ {colorPairsToCheck.map((pair) => { + const result = contrastResults?.find((res) => res.id === pair.id); + + return ( +
+

+ {pair.label} +

+
+ + updateStyle(pair.backgroundId, color) + } + label={pair.label} + onlyShowPicker + /> +
+ {pair.foreground && pair.background && ( +

+ Aa +

+ )} + {(!pair.foreground || !pair.background) && ( +

N/A

+ )} +
+ + updateStyle(pair.foregroundId, color) + } + label={pair.label} + onlyShowPicker + /> +
+

+ {result ? ( + <> + Contrast Ratio: {result.contrastRatio.toFixed(2)}{" "} + {result.contrastRatio >= MIN_CONTRAST_RATIO ? ( + + ) : ( + + )} + + ) : ( + "N/A" + )} +

+
+ ); + })} +
+
+
+
+ ); +}; + +export default ContrastChecker; diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index 3340f558..bd3d5214 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -22,7 +22,6 @@ import { DEFAULT_FONT_MONO, DEFAULT_FONT_SANS, DEFAULT_FONT_SERIF, - COMMON_STYLES, defaultThemeState, } from "../../config/theme"; import { Separator } from "../ui/separator"; @@ -32,6 +31,7 @@ import { toast } from "../ui/use-toast"; import { parseCssInput } from "../../utils/parse-css-input"; import ShadowControl from "./shadow-control"; import ThemeControlActions from "./theme-control-actions"; +import ContrastChecker from "./contrast-checker"; const ThemeControlPanel = ({ styles, @@ -45,6 +45,7 @@ const ThemeControlPanel = ({ resetToDefault, hasDefaultThemeChanged, hasCurrentPresetChanged, + updateStyle, } = useEditorStore(); const [cssImportOpen, setCssImportOpen] = useState(false); @@ -56,32 +57,6 @@ const ThemeControlPanel = ({ [currentMode, styles] ); - const updateStyle = React.useCallback( - ( - key: K, - value: (typeof currentStyles)[K] - ) => { - // apply common styles to both light and dark modes - if (COMMON_STYLES.includes(key)) { - onChange({ - ...styles, - light: { ...styles.light, [key]: value }, - dark: { ...styles.dark, [key]: value }, - }); - return; - } - - onChange({ - ...styles, - [currentMode]: { - ...currentStyles, - [key]: value, - }, - }); - }, - [onChange, styles, currentMode, currentStyles] - ); - const handleCssImport = (css: string) => { // This just shows a success toast for now const { lightColors, darkColors } = parseCssInput(css); @@ -110,6 +85,7 @@ const ThemeControlPanel = ({

Theme Editor

+
([]); + + const debouncedCalculation = useCallback( + debounce((pairs: ColorPair[]) => { + if (!pairs.length) { + setContrastResults([]); + return; + } + + try { + const results = pairs.map((pair) => { + const ratio = parseFloat( + getContrastRatio(pair.foreground, pair.background) + ); + return { + id: pair.id, + contrastRatio: ratio, + }; + }); + + setContrastResults(results); + } catch (error) { + console.error("Error checking contrast:", error); + setContrastResults([]); + } + }, 750), + [] + ); + + useEffect(() => { + debouncedCalculation(colorPairs); + }, [colorPairs, debouncedCalculation]); + + return contrastResults; +} diff --git a/store/editor-store.ts b/store/editor-store.ts index e1942f20..8485b567 100644 --- a/store/editor-store.ts +++ b/store/editor-store.ts @@ -1,9 +1,10 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { ThemeEditorState } from "@/types/editor"; +import { ThemeStyleProps } from "@/types/theme"; // @ts-expect-error: owned by ngard import { isEqual } from "@ngard/tiny-isequal"; -import { defaultThemeState } from "@/config/theme"; +import { defaultThemeState, COMMON_STYLES } from "@/config/theme"; import { getPresetThemeStyles } from "@/utils/theme-presets"; interface EditorStore { @@ -15,6 +16,10 @@ interface EditorStore { hasDefaultThemeChanged: () => boolean; hasCurrentPresetChanged: () => boolean; hasChangedThemeFromDefault: boolean; + updateStyle: ( + key: K, + value: ThemeStyleProps[K] + ) => void; } export const useEditorStore = create()( @@ -63,6 +68,36 @@ export const useEditorStore = create()( ); return !isEqual(state.themeState.styles, presetStyles); }, + updateStyle: (key, value) => + set((state) => { + const currentMode = state.themeState.currentMode; + const currentStyles = state.themeState.styles; + + let newStyles = { ...currentStyles }; + + if (COMMON_STYLES.includes(key as string)) { + newStyles = { + ...newStyles, + light: { ...newStyles.light, [key]: value }, + dark: { ...newStyles.dark, [key]: value }, + }; + } else { + newStyles = { + ...newStyles, + [currentMode]: { + ...(newStyles[currentMode] || {}), + [key]: value, + }, + }; + } + + return { + themeState: { + ...state.themeState, + styles: newStyles, + }, + }; + }), }), { name: "editor-storage", // unique name for localStorage diff --git a/types/index.ts b/types/index.ts index 763eddf4..74e0940c 100644 --- a/types/index.ts +++ b/types/index.ts @@ -10,6 +10,7 @@ export type ColorPickerProps = { color: string; onChange: (color: string) => void; label: string; + onlyShowPicker?: boolean; }; export type SliderInputProps = { diff --git a/utils/contrast-checker.ts b/utils/contrast-checker.ts new file mode 100644 index 00000000..7dcbb5f1 --- /dev/null +++ b/utils/contrast-checker.ts @@ -0,0 +1,45 @@ +import * as culori from "culori"; + +/** + * Calculates the luminance of a color according to WCAG standard + * @param colorValue - The color in any supported format + * @returns The relative luminance of the color (0-1) + */ +function getLuminance(colorValue: string): number { + try { + const color = culori.parse(colorValue); + if (!color) { + console.warn(`Invalid color: ${colorValue}`); + return 0; + } + + // Culori directly provides the luminance according to WCAG standard + return culori.wcagLuminance(color); + } catch (error) { + console.error(`Error calculating luminance: ${colorValue}`, error); + return 0; + } +} + +/** + * Calculates the contrast ratio between two colors according to WCAG guidelines + * @param color1 - First color (in any format) + * @param color2 - Second color (in any format) + * @returns The contrast ratio as a string (e.g. "4.50") + */ +export function getContrastRatio(color1: string, color2: string): string { + try { + const lum1 = getLuminance(color1); + const lum2 = getLuminance(color2); + + // WCAG contrast ratio formula + const ratio = (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05); + return ratio.toFixed(2); + } catch (error) { + console.error( + `Error calculating contrast between ${color1} and ${color2}:`, + error + ); + return "1.00"; // Fallback value indicating low contrast + } +} From 1a97a06be456500e70fbfa8b804453f6a90278a6 Mon Sep 17 00:00:00 2001 From: lorenzopalaia Date: Thu, 17 Apr 2025 17:21:36 +0200 Subject: [PATCH 03/11] feat: Add tooltip to contrast checker button for improved accessibility --- components/editor/contrast-checker.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/components/editor/contrast-checker.tsx b/components/editor/contrast-checker.tsx index 8898973e..6b873379 100644 --- a/components/editor/contrast-checker.tsx +++ b/components/editor/contrast-checker.tsx @@ -10,6 +10,7 @@ import { DialogTrigger, DialogDescription, } from "../ui/dialog"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import ColorPicker from "./color-picker"; import { useEditorStore } from "@/store/editor-store"; import { Contrast, Check, AlertTriangle } from "lucide-react"; @@ -132,9 +133,14 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { return ( - + + + + + Check Contrast + From eae9a3a1bb05770d75e64ae049c7d118207f05e4 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Thu, 17 Apr 2025 22:34:03 +0530 Subject: [PATCH 04/11] desktop ui final --- components/editor/action-bar.tsx | 31 +++- components/editor/color-picker.tsx | 7 +- components/editor/editor.tsx | 2 +- components/editor/header.tsx | 23 +-- components/editor/theme-control-panel.tsx | 4 +- components/editor/theme-preset-select.tsx | 21 +-- components/editor/theme-preview-panel.tsx | 199 ++++++++++------------ types/theme.ts | 2 - 8 files changed, 132 insertions(+), 157 deletions(-) diff --git a/components/editor/action-bar.tsx b/components/editor/action-bar.tsx index 87a7cbba..bad5f20b 100644 --- a/components/editor/action-bar.tsx +++ b/components/editor/action-bar.tsx @@ -4,13 +4,15 @@ import ThemePresetSelect from "./theme-preset-select"; import { useEditorStore } from "@/store/editor-store"; import { getPresetThemeStyles, presets } from "@/utils/theme-presets"; import { Button } from "../ui/button"; -import { FileCode, RefreshCw, Code } from "lucide-react"; +import { FileCode, RefreshCw, Code, Moon, Sun } from "lucide-react"; import CssImportDialog from "./css-import-dialog"; import { useState } from "react"; import { parseCssInput } from "@/utils/parse-css-input"; import { toast } from "../ui/use-toast"; import { CodePanelDialog } from "./code-panel-dialog"; import { Separator } from "../ui/separator"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; +import { useTheme } from "../theme-provider"; export function ActionBar() { const { themeState, applyThemePreset, resetToCurrentPreset, setThemeState } = @@ -43,10 +45,33 @@ export function ActionBar() { }); }; + const { theme, toggleTheme } = useTheme(); + + const handleThemeToggle = (event: React.MouseEvent) => { + const { clientX: x, clientY: y } = event; + toggleTheme({ x, y }); + }; + return ( -
+
+
+ + + {theme === "dark" ? ( + + ) : ( + + )} + + +
+ - +
-
+
setIsOpen(!isOpen)} > @@ -60,7 +59,7 @@ const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => { type="text" value={localColor} onChange={handleColorChange} - className="flex-1 h-8 px-2 text-sm rounded-md border bg-background" + className="flex-1 h-8 px-2 text-sm rounded bg-input/20" />
diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index 13ab847f..a202a380 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -62,7 +62,7 @@ const Editor: React.FC = ({ config }) => {
-
+
diff --git a/components/editor/header.tsx b/components/editor/header.tsx index ee365ead..c9ac759f 100644 --- a/components/editor/header.tsx +++ b/components/editor/header.tsx @@ -10,17 +10,10 @@ import Logo from "@/assets/logo.svg"; import { useGithubStars } from "@/hooks/use-github-stars"; import { SocialLink } from "@/components/social-link"; import { Separator } from "@/components/ui/separator"; -import * as SwitchPrimitives from "@radix-ui/react-switch"; export function Header() { - const { theme, toggleTheme } = useTheme(); const { stargazersCount } = useGithubStars("jnsahaj", "tweakcn"); - const handleThemeToggle = (event: React.MouseEvent) => { - const { clientX: x, clientY: y } = event; - toggleTheme({ x, y }); - }; - return (
@@ -38,7 +31,7 @@ export function Header() { {stargazersCount > 0 && stargazersCount.toLocaleString()} - +
- - - - {theme === "dark" ? ( - - ) : ( - - )} - -
diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index b3bc2fc6..8987a6b5 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -101,7 +101,7 @@ const ThemeControlPanel = ({
-
+
Colors Typography @@ -109,7 +109,7 @@ const ThemeControlPanel = ({
- + = ({
- +
- + @@ -279,21 +276,21 @@ const ThemePresetSelect: React.FC = ({
); diff --git a/components/editor/theme-preview-panel.tsx b/components/editor/theme-preview-panel.tsx index b5398757..bdc8f82e 100644 --- a/components/editor/theme-preview-panel.tsx +++ b/components/editor/theme-preview-panel.tsx @@ -8,7 +8,7 @@ import TabsTriggerPill from "./theme-preview/tabs-trigger-pill"; import ExamplesPreviewContainer from "./theme-preview/examples-preview-container"; import { lazy } from "react"; import { Button } from "@/components/ui/button"; -import { Maximize, Minimize, PanelRight, Moon, Sun } from "lucide-react"; +import { Maximize, Minimize, Moon, Sun } from "lucide-react"; import { useFullscreen } from "@/hooks/use-fullscreen"; import { cn } from "@/lib/utils"; import { useTheme } from "@/components/theme-provider"; @@ -28,8 +28,6 @@ const DemoDashboard = lazy(() => import("@/components/examples/dashboard")); const ThemePreviewPanel = ({ styles, currentMode, - isCodePanelOpen, - onCodePanelToggle, }: ThemeEditorPreviewProps) => { const { isFullscreen, toggleFullscreen } = useFullscreen(); const { theme, toggleTheme } = useTheme(); @@ -48,138 +46,117 @@ const ThemePreviewPanel = ({
-
- -
- - Cards -
- Mail - Tasks - Music - Dashboard -
- Color Palette -
+ +
+ + Cards +
+ Mail + Tasks + Music + Dashboard +
+ Color Palette +
-
- {isFullscreen && ( - - - - - Toggle Theme - - )} +
+ {isFullscreen && ( - - {isFullscreen ? "Exit full screen" : "Full screen"} - + Toggle Theme - {!isCodePanelOpen && !isFullscreen && ( - - - - - Hide Code Panel - - )} -
+ )} + + + + + + {isFullscreen ? "Exit full screen" : "Full screen"} + +
+
- -
- - - - - + +
+ + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - + + + - -
-
- -
+ +
+ +
); diff --git a/types/theme.ts b/types/theme.ts index a042fb5c..2b4e3dfb 100644 --- a/types/theme.ts +++ b/types/theme.ts @@ -58,8 +58,6 @@ export interface ThemeEditorState { export interface ThemeEditorPreviewProps { styles: ThemeStyles; currentMode: "light" | "dark"; - isCodePanelOpen: boolean; - onCodePanelToggle: (open: boolean) => void; } export interface ThemeEditorControlsProps { From 2232aecde3905439f5e577c8b1ab822ff318c90b Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Thu, 17 Apr 2025 22:53:55 +0530 Subject: [PATCH 05/11] fix: Mobile --- components/editor/editor.tsx | 23 +++++++++-------------- components/editor/header.tsx | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index a202a380..29829b63 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -73,7 +73,7 @@ const Editor: React.FC = ({ config }) => { {/* Mobile Layout */}
- + Controls @@ -81,21 +81,16 @@ const Editor: React.FC = ({ config }) => { Preview - - Code - - -
- -
+ + - -
+ +
diff --git a/components/editor/header.tsx b/components/editor/header.tsx index c9ac759f..6988f5b6 100644 --- a/components/editor/header.tsx +++ b/components/editor/header.tsx @@ -16,7 +16,7 @@ export function Header() { return (
-
+
From bc3f8bbbcd93f0314f9514c2637f4c743ed2eca5 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Thu, 17 Apr 2025 23:05:00 +0530 Subject: [PATCH 06/11] fix: Scroll on control panel --- components/editor/editor.tsx | 14 ++++++++------ components/editor/theme-control-panel.tsx | 11 +++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index 29829b63..01d37527 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -51,7 +51,7 @@ const Editor: React.FC = ({ config }) => {
-
+
= ({ config }) => { - +
+ +
diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index 8987a6b5..e6ef5651 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -99,8 +99,11 @@ const ThemeControlPanel = ({ onPresetChange={applyThemePreset} />
-
- +
+
Colors @@ -109,7 +112,7 @@ const ThemeControlPanel = ({
- + - + updateStyle("accent", color)} From f2797888abcbc9600d22935212f0706fd696fde4 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Thu, 17 Apr 2025 23:16:14 +0530 Subject: [PATCH 07/11] fix: lint errors --- app/editor/theme/page.tsx | 1 - components/editor/action-bar.tsx | 9 +-------- components/editor/code-panel-dialog.tsx | 5 +---- components/editor/code-panel.tsx | 8 ++------ components/editor/editor.tsx | 2 +- components/editor/header.tsx | 3 +-- components/editor/theme-control-panel.tsx | 18 +++--------------- components/editor/theme-preview-panel.tsx | 2 +- 8 files changed, 10 insertions(+), 38 deletions(-) diff --git a/app/editor/theme/page.tsx b/app/editor/theme/page.tsx index 9b748548..48721c70 100644 --- a/app/editor/theme/page.tsx +++ b/app/editor/theme/page.tsx @@ -3,7 +3,6 @@ import { cn } from "@/lib/utils"; import Editor from "@/components/editor/editor"; import { Metadata } from "next"; import { Header } from "../../../components/editor/header"; -import { ActionBar } from "@/components/editor/action-bar"; export const metadata: Metadata = { title: "tweakcn — Theme Generator for shadcn/ui", diff --git a/components/editor/action-bar.tsx b/components/editor/action-bar.tsx index bad5f20b..abe119ff 100644 --- a/components/editor/action-bar.tsx +++ b/components/editor/action-bar.tsx @@ -1,8 +1,6 @@ "use client"; -import ThemePresetSelect from "./theme-preset-select"; import { useEditorStore } from "@/store/editor-store"; -import { getPresetThemeStyles, presets } from "@/utils/theme-presets"; import { Button } from "../ui/button"; import { FileCode, RefreshCw, Code, Moon, Sun } from "lucide-react"; import CssImportDialog from "./css-import-dialog"; @@ -15,15 +13,10 @@ import * as SwitchPrimitives from "@radix-ui/react-switch"; import { useTheme } from "../theme-provider"; export function ActionBar() { - const { themeState, applyThemePreset, resetToCurrentPreset, setThemeState } = - useEditorStore(); + const { themeState, resetToCurrentPreset, setThemeState } = useEditorStore(); const [cssImportOpen, setCssImportOpen] = useState(false); const [codePanelOpen, setCodePanelOpen] = useState(false); - const handlePresetChange = (preset: string) => { - applyThemePreset(preset); - }; - const handleCssImport = (css: string) => { // This just shows a success toast for now const { lightColors, darkColors } = parseCssInput(css); diff --git a/components/editor/code-panel-dialog.tsx b/components/editor/code-panel-dialog.tsx index ebddea13..0fcaa207 100644 --- a/components/editor/code-panel-dialog.tsx +++ b/components/editor/code-panel-dialog.tsx @@ -17,10 +17,7 @@ export function CodePanelDialog({
- onOpenChange(false)} - /> +
diff --git a/components/editor/code-panel.tsx b/components/editor/code-panel.tsx index 8ad38f44..c74ccc66 100644 --- a/components/editor/code-panel.tsx +++ b/components/editor/code-panel.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { Button } from "@/components/ui/button"; -import { Copy, Check, PanelRight } from "lucide-react"; +import { Copy, Check } from "lucide-react"; import { ThemeEditorState } from "@/types/editor"; import { ScrollArea, ScrollBar } from "../ui/scroll-area"; import { ColorFormat } from "../../types"; @@ -14,12 +14,8 @@ import { import { usePostHog } from "posthog-js/react"; import { useEditorStore } from "@/store/editor-store"; import { usePreferencesStore } from "@/store/preferences-store"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { generateThemeCode } from "@/utils/theme-style-generator"; + interface CodePanelProps { themeEditorState: ThemeEditorState; } diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index 01d37527..ced646aa 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import { ResizablePanelGroup, ResizablePanel, diff --git a/components/editor/header.tsx b/components/editor/header.tsx index 6988f5b6..e02ee5e5 100644 --- a/components/editor/header.tsx +++ b/components/editor/header.tsx @@ -1,11 +1,10 @@ "use client"; import Link from "next/link"; -import { Moon, Sun, Heart } from "lucide-react"; +import { Heart } from "lucide-react"; import GitHubIcon from "@/assets/github.svg"; import TwitterIcon from "@/assets/twitter.svg"; import DiscordIcon from "@/assets/discord.svg"; -import { useTheme } from "@/components/theme-provider"; import Logo from "@/assets/logo.svg"; import { useGithubStars } from "@/hooks/use-github-stars"; import { SocialLink } from "@/components/social-link"; diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index e6ef5651..795dbed5 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import { ThemeEditorControlsProps, ThemeStyleProps } from "@/types/theme"; import ControlSection from "./control-section"; import ColorPicker from "./color-picker"; @@ -16,7 +16,7 @@ import { import { useEditorStore } from "../../store/editor-store"; import { Label } from "../ui/label"; import { SliderWithInput } from "./slider-with-input"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs"; +import { Tabs, TabsList, TabsContent } from "../ui/tabs"; import ThemeFontSelect from "./theme-font-select"; import { DEFAULT_FONT_MONO, @@ -27,11 +27,7 @@ import { } from "../../config/theme"; import { Separator } from "../ui/separator"; import { AlertCircle } from "lucide-react"; -import CssImportDialog from "./css-import-dialog"; -import { toast } from "../ui/use-toast"; -import { parseCssInput } from "../../utils/parse-css-input"; import ShadowControl from "./shadow-control"; -import ThemeControlActions from "./theme-control-actions"; import TabsTriggerPill from "./theme-preview/tabs-trigger-pill"; const ThemeControlPanel = ({ @@ -39,15 +35,7 @@ const ThemeControlPanel = ({ currentMode, onChange, }: ThemeEditorControlsProps) => { - const { - applyThemePreset, - themeState, - resetToCurrentPreset, - resetToDefault, - hasDefaultThemeChanged, - hasCurrentPresetChanged, - } = useEditorStore(); - const [cssImportOpen, setCssImportOpen] = useState(false); + const { applyThemePreset, themeState } = useEditorStore(); const currentStyles = React.useMemo( () => ({ diff --git a/components/editor/theme-preview-panel.tsx b/components/editor/theme-preview-panel.tsx index bdc8f82e..e9ff8a8b 100644 --- a/components/editor/theme-preview-panel.tsx +++ b/components/editor/theme-preview-panel.tsx @@ -47,7 +47,7 @@ const ThemePreviewPanel = ({
From 4aaa8a40665711e5848c3bd4edb1e279412663a6 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Fri, 18 Apr 2025 03:43:16 +0530 Subject: [PATCH 08/11] integrate contrast checker --- components/editor/action-bar.tsx | 16 +- components/editor/color-picker.tsx | 27 +- components/editor/contrast-checker.tsx | 380 +++++++++++++++------- components/editor/control-section.tsx | 7 +- components/editor/theme-control-panel.tsx | 1 + components/editor/theme-preset-select.tsx | 8 +- store/editor-store.ts | 34 -- 7 files changed, 296 insertions(+), 177 deletions(-) diff --git a/components/editor/action-bar.tsx b/components/editor/action-bar.tsx index abe119ff..62eb4bb3 100644 --- a/components/editor/action-bar.tsx +++ b/components/editor/action-bar.tsx @@ -11,9 +11,15 @@ import { CodePanelDialog } from "./code-panel-dialog"; import { Separator } from "../ui/separator"; import * as SwitchPrimitives from "@radix-ui/react-switch"; import { useTheme } from "../theme-provider"; +import ContrastChecker from "./contrast-checker"; export function ActionBar() { - const { themeState, resetToCurrentPreset, setThemeState } = useEditorStore(); + const { + themeState, + resetToCurrentPreset, + setThemeState, + hasCurrentPresetChanged, + } = useEditorStore(); const [cssImportOpen, setCssImportOpen] = useState(false); const [codePanelOpen, setCodePanelOpen] = useState(false); @@ -65,6 +71,9 @@ export function ActionBar() {
+ diff --git a/components/editor/color-picker.tsx b/components/editor/color-picker.tsx index f125c186..76e9d27c 100644 --- a/components/editor/color-picker.tsx +++ b/components/editor/color-picker.tsx @@ -3,12 +3,7 @@ import { Label } from "@/components/ui/label"; import { ColorPickerProps } from "@/types"; import { debounce } from "@/utils/debounce"; -const ColorPicker = ({ - color, - onChange, - label, - onlyShowPicker, -}: ColorPickerProps) => { +const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => { const [isOpen, setIsOpen] = useState(false); const [localColor, setLocalColor] = useState(color); @@ -36,24 +31,6 @@ const ColorPicker = ({ }; }, [debouncedOnChange]); - if (onlyShowPicker) { - return ( -
setIsOpen(!isOpen)} - > - -
- ); - } - return (
@@ -82,7 +59,7 @@ const ColorPicker = ({ type="text" value={localColor} onChange={handleColorChange} - className="flex-1 h-8 px-2 text-sm rounded bg-input/20" + className="flex-1 h-8 px-2 text-sm rounded bg-input/25 border border-border/20" />
diff --git a/components/editor/contrast-checker.tsx b/components/editor/contrast-checker.tsx index 6b873379..d47fd4e0 100644 --- a/components/editor/contrast-checker.tsx +++ b/components/editor/contrast-checker.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { useContrastChecker } from "../../hooks/use-contrast-checker"; import { ThemeStyleProps } from "@/types/theme"; import { Button } from "../ui/button"; @@ -10,17 +10,21 @@ import { DialogTrigger, DialogDescription, } from "../ui/dialog"; -import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; -import ColorPicker from "./color-picker"; -import { useEditorStore } from "@/store/editor-store"; -import { Contrast, Check, AlertTriangle } from "lucide-react"; +import { Contrast, Check, AlertTriangle, Info } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Card, CardContent } from "../ui/card"; +import { Badge } from "../ui/badge"; +import { ScrollArea } from "../ui/scroll-area"; +import { Separator } from "../ui/separator"; type ContrastCheckerProps = { - currentStyles: ThemeStyleProps | Partial; + currentStyles: ThemeStyleProps; }; const MIN_CONTRAST_RATIO = 4.5; +type ColorCategory = "content" | "interactive" | "functional"; + type ColorPair = { id: string; foregroundId: keyof ThemeStyleProps; @@ -28,34 +32,14 @@ type ColorPair = { foreground: string | undefined; background: string | undefined; label: string; + category: ColorCategory; }; const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { + const [filter, setFilter] = useState<"all" | "issues">("all"); + const colorPairsToCheck: ColorPair[] = [ - { - id: "primary", - foregroundId: "primary-foreground", - backgroundId: "primary", - foreground: currentStyles?.["primary-foreground"], - background: currentStyles?.["primary"], - label: "Primary", - }, - { - id: "secondary", - foregroundId: "secondary-foreground", - backgroundId: "secondary", - foreground: currentStyles?.["secondary-foreground"], - background: currentStyles?.["secondary"], - label: "Secondary", - }, - { - id: "accent", - foregroundId: "accent-foreground", - backgroundId: "accent", - foreground: currentStyles?.["accent-foreground"], - background: currentStyles?.["accent"], - label: "Accent", - }, + // Content - Base, background, cards, containers { id: "base", foregroundId: "foreground", @@ -63,6 +47,7 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["foreground"], background: currentStyles?.["background"], label: "Base", + category: "content", }, { id: "card", @@ -71,6 +56,7 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["card-foreground"], background: currentStyles?.["card"], label: "Card", + category: "content", }, { id: "popover", @@ -79,6 +65,7 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["popover-foreground"], background: currentStyles?.["popover"], label: "Popover", + category: "content", }, { id: "muted", @@ -87,7 +74,39 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["muted-foreground"], background: currentStyles?.["muted"], label: "Muted", + category: "content", }, + + // Interactive - Buttons, links, actions + { + id: "primary", + foregroundId: "primary-foreground", + backgroundId: "primary", + foreground: currentStyles?.["primary-foreground"], + background: currentStyles?.["primary"], + label: "Primary", + category: "interactive", + }, + { + id: "secondary", + foregroundId: "secondary-foreground", + backgroundId: "secondary", + foreground: currentStyles?.["secondary-foreground"], + background: currentStyles?.["secondary"], + label: "Secondary", + category: "interactive", + }, + { + id: "accent", + foregroundId: "accent-foreground", + backgroundId: "accent", + foreground: currentStyles?.["accent-foreground"], + background: currentStyles?.["accent"], + label: "Accent", + category: "interactive", + }, + + // Functional - Sidebar, destructive, special purposes { id: "destructive", foregroundId: "destructive-foreground", @@ -95,6 +114,7 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["destructive-foreground"], background: currentStyles?.["destructive"], label: "Destructive", + category: "functional", }, { id: "sidebar", @@ -103,6 +123,7 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["sidebar-foreground"], background: currentStyles?.["sidebar"], label: "Sidebar Base", + category: "functional", }, { id: "sidebar-primary", @@ -111,6 +132,7 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["sidebar-primary-foreground"], background: currentStyles?.["sidebar-primary"], label: "Sidebar Primary", + category: "functional", }, { id: "sidebar-accent", @@ -119,102 +141,238 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { foreground: currentStyles?.["sidebar-accent-foreground"], background: currentStyles?.["sidebar-accent"], label: "Sidebar Accent", + category: "functional", }, ]; - const { updateStyle } = useEditorStore(); - const validColorPairsToCheck = colorPairsToCheck.filter( (pair): pair is ColorPair & { foreground: string; background: string } => !!pair.foreground && !!pair.background ); const contrastResults = useContrastChecker(validColorPairsToCheck); + const getContrastResult = (pairId: string) => { + return contrastResults?.find((res) => res.id === pairId); + }; + + const totalIssues = contrastResults?.filter( + (result) => result.contrastRatio < MIN_CONTRAST_RATIO + ).length; + + const filteredPairs = + filter === "all" + ? colorPairsToCheck + : colorPairsToCheck.filter((pair) => { + const result = getContrastResult(pair.id); + return result && result.contrastRatio < MIN_CONTRAST_RATIO; + }); + + // Group color pairs by category + const categoryLabels: Record = { + content: "Content & Containers", + interactive: "Interactive Elements", + functional: "Navigation & Functional", + }; + + const categories: ColorCategory[] = ["content", "interactive", "functional"]; + const groupedPairs = categories + .map((category) => ({ + category, + label: categoryLabels[category], + pairs: filteredPairs.filter((pair) => pair.category === category), + })) + .filter((group) => group.pairs.length > 0); + return ( - - - - - Check Contrast - + - - - Contrast Checker - - Check the contrast ratios of your theme colors to ensure they meet - accessibility standards. You can also adjust the colors using the - color pickers below. The recommended minimum contrast ratio is{" "} - {MIN_CONTRAST_RATIO}. - -
- {colorPairsToCheck.map((pair) => { - const result = contrastResults?.find((res) => res.id === pair.id); - - return ( -
-

- {pair.label} -

-
- - updateStyle(pair.backgroundId, color) - } - label={pair.label} - onlyShowPicker - /> -
- {pair.foreground && pair.background && ( -

- Aa -

- )} - {(!pair.foreground || !pair.background) && ( -

N/A

- )} -
- - updateStyle(pair.foregroundId, color) - } - label={pair.label} - onlyShowPicker - /> -
-

- {result ? ( - <> - Contrast Ratio: {result.contrastRatio.toFixed(2)}{" "} - {result.contrastRatio >= MIN_CONTRAST_RATIO ? ( - - ) : ( - - )} - - ) : ( - "N/A" - )} -

-
- ); - })} + + +
+
+ + Contrast Checker + + + WCAG 2.0 AA requires a contrast ratio of at least{" "} + {MIN_CONTRAST_RATIO}:1{" • "} + + Learn more + + +
+
+ + +
+ + +
+ {groupedPairs.map((group) => ( +
+
+

{group.label}

+ +
+
+ {group.pairs.map((pair) => { + const result = getContrastResult(pair.id); + const isValid = + result?.contrastRatio !== undefined && + result?.contrastRatio >= MIN_CONTRAST_RATIO; + const contrastRatio = + result?.contrastRatio?.toFixed(2) ?? "N/A"; + + return ( + + +
+

{pair.label}

+ + {isValid ? ( + <> + + {contrastRatio} + + ) : ( + <> + + {contrastRatio} + + )} + +
+ +
+
+
+
+
+ + Background + + + {pair.background} + +
+
+ +
+
+
+ + Foreground + + + {pair.foreground} + +
+
+
+ +
+ {pair.foreground && pair.background ? ( +
+

+ Aa +

+

+ Sample Text +

+
+ ) : ( +

+ Preview +

+ )} +
+
+ + {!isValid && result && ( +
+

+ + Contrast ratio below {MIN_CONTRAST_RATIO}:1 + (WCAG 2.0 AA) +

+
+ )} +
+
+ ); + })} +
+
+ ))} +
+
); diff --git a/components/editor/control-section.tsx b/components/editor/control-section.tsx index 092da979..14cdbc06 100644 --- a/components/editor/control-section.tsx +++ b/components/editor/control-section.tsx @@ -13,7 +13,10 @@ const ControlSection = ({ const [isExpanded, setIsExpanded] = useState(expanded); return ( -
+
setIsExpanded(!isExpanded)} @@ -35,7 +38,7 @@ const ControlSection = ({
{children}
diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index bcb75626..b01da17d 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -19,6 +19,7 @@ import { SliderWithInput } from "./slider-with-input"; import { Tabs, TabsList, TabsContent } from "../ui/tabs"; import ThemeFontSelect from "./theme-font-select"; import { + COMMON_STYLES, DEFAULT_FONT_MONO, DEFAULT_FONT_SANS, DEFAULT_FONT_SERIF, diff --git a/components/editor/theme-preset-select.tsx b/components/editor/theme-preset-select.tsx index 483eab48..45f0a727 100644 --- a/components/editor/theme-preset-select.tsx +++ b/components/editor/theme-preset-select.tsx @@ -126,7 +126,7 @@ const ThemePresetSelect: React.FC = ({ + + - + + + + + Import CSS variables + + + + + + Reset to preset defaults + - + + + + + View theme code +
diff --git a/components/editor/contrast-checker.tsx b/components/editor/contrast-checker.tsx index d47fd4e0..d7739b35 100644 --- a/components/editor/contrast-checker.tsx +++ b/components/editor/contrast-checker.tsx @@ -16,6 +16,7 @@ import { Card, CardContent } from "../ui/card"; import { Badge } from "../ui/badge"; import { ScrollArea } from "../ui/scroll-area"; import { Separator } from "../ui/separator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; type ContrastCheckerProps = { currentStyles: ThemeStyleProps; @@ -185,15 +186,20 @@ const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { return ( - - + + + + + Check contrast accessibility + + diff --git a/components/editor/theme-preset-select.tsx b/components/editor/theme-preset-select.tsx index 45f0a727..bf415ed8 100644 --- a/components/editor/theme-preset-select.tsx +++ b/components/editor/theme-preset-select.tsx @@ -275,27 +275,35 @@ const ThemePresetSelect: React.FC = ({ - + + + + + Previous theme + - + + + + + Next theme +
); }; From bb2e1e7b0e7b0ea9fad74472cca20bf1c6757174 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Fri, 18 Apr 2025 16:55:07 +0530 Subject: [PATCH 10/11] code button color secondary --- components/editor/action-bar.tsx | 4 ++-- types/index.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/components/editor/action-bar.tsx b/components/editor/action-bar.tsx index 2c7671e5..c8b47394 100644 --- a/components/editor/action-bar.tsx +++ b/components/editor/action-bar.tsx @@ -114,9 +114,9 @@ export function ActionBar() {
+ + + + + +

Toggle theme

+
+
- - {!isValid && result && ( -
-

- - Contrast ratio below {MIN_CONTRAST_RATIO}:1 - (WCAG 2.0 AA) -

-
- )} ); diff --git a/components/editor/theme-preset-select.tsx b/components/editor/theme-preset-select.tsx index bf415ed8..4f34d240 100644 --- a/components/editor/theme-preset-select.tsx +++ b/components/editor/theme-preset-select.tsx @@ -76,10 +76,11 @@ const ThemePresetSelect: React.FC = ({ [presets] ); const value = presetNames?.find((name) => name === currentPreset); - const currentIndex = useMemo( - () => presetNames.indexOf(value || "default"), - [presetNames, value] - ); + const currentIndex = + useMemo( + () => presetNames.indexOf(value || "default"), + [presetNames, value] + ) ?? 0; const randomize = useCallback(() => { const random = Math.floor(Math.random() * presetNames.length);