mirror of
https://github.com/jnsahaj/tweakcn.git
synced 2026-09-01 15:37:45 +08:00
Merge pull request #35 from jnsahaj/feature/editor-ui-revamp
Feature/editor UI revamp
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useEditorStore } from "@/store/editor-store";
|
||||
import { Button } from "../ui/button";
|
||||
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";
|
||||
import ContrastChecker from "./contrast-checker";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
export function ActionBar() {
|
||||
const {
|
||||
themeState,
|
||||
resetToCurrentPreset,
|
||||
setThemeState,
|
||||
hasCurrentPresetChanged,
|
||||
} = useEditorStore();
|
||||
const [cssImportOpen, setCssImportOpen] = useState(false);
|
||||
const [codePanelOpen, setCodePanelOpen] = useState(false);
|
||||
|
||||
const handleCssImport = (css: string) => {
|
||||
const { lightColors, darkColors } = parseCssInput(css);
|
||||
const styles = {
|
||||
...themeState.styles,
|
||||
light: { ...themeState.styles.light, ...lightColors },
|
||||
dark: { ...themeState.styles.dark, ...darkColors },
|
||||
};
|
||||
|
||||
setThemeState({
|
||||
...themeState,
|
||||
styles,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "CSS imported",
|
||||
description: "Your custom CSS has been imported successfully",
|
||||
});
|
||||
};
|
||||
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
const handleThemeToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { clientX: x, clientY: y } = event;
|
||||
toggleTheme({ x, y });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
<div className="flex h-14 items-center justify-end gap-4 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="px-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<SwitchPrimitives.Root
|
||||
checked={theme === "dark"}
|
||||
onClick={handleThemeToggle}
|
||||
className="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent data-[state=unchecked]:bg-input"
|
||||
>
|
||||
<SwitchPrimitives.Thumb className="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 flex items-center justify-center">
|
||||
{theme === "dark" ? (
|
||||
<Moon className="size-3" />
|
||||
) : (
|
||||
<Sun className="size-3" />
|
||||
)}
|
||||
</SwitchPrimitives.Thumb>
|
||||
</SwitchPrimitives.Root>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Toggle light/dark mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<ContrastChecker
|
||||
currentStyles={themeState.styles[themeState.currentMode]}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 gap-1.5 text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
onClick={() => setCssImportOpen(true)}
|
||||
>
|
||||
<FileCode className="size-3.5" />
|
||||
<span className="text-sm hidden md:block">Import</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Import CSS variables</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 gap-1.5 text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
onClick={resetToCurrentPreset}
|
||||
disabled={!hasCurrentPresetChanged()}
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
<span className="text-sm hidden md:block">Reset</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reset to preset defaults</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-8 px-2 gap-1.5"
|
||||
onClick={() => setCodePanelOpen(true)}
|
||||
>
|
||||
<Code className="size-3.5" />
|
||||
<span className="text-sm">Code</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View theme code</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CssImportDialog
|
||||
open={cssImportOpen}
|
||||
onOpenChange={setCssImportOpen}
|
||||
onImport={handleCssImport}
|
||||
/>
|
||||
<CodePanelDialog
|
||||
open={codePanelOpen}
|
||||
onOpenChange={setCodePanelOpen}
|
||||
themeEditorState={themeState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl h-[80vh] overflow-hidden">
|
||||
<div className="h-full overflow-auto">
|
||||
<CodePanel themeEditorState={themeEditorState} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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,21 +14,13 @@ 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;
|
||||
onCodePanelToggle: () => void;
|
||||
}
|
||||
|
||||
const CodePanel: React.FC<CodePanelProps> = ({
|
||||
themeEditorState,
|
||||
onCodePanelToggle,
|
||||
}) => {
|
||||
const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
|
||||
const [registryCopied, setRegistryCopied] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const posthog = usePostHog();
|
||||
@@ -102,20 +94,7 @@ const CodePanel: React.FC<CodePanelProps> = ({
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<div className="flex-none mb-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="text-lg font-semibold">Code</h2>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onCodePanelToggle}
|
||||
className="h-8 invisible md:visible group"
|
||||
>
|
||||
<PanelRight className="size-4 group-hover:scale-120 transition-all" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Collapse Code Panel</TooltipContent>
|
||||
</Tooltip>
|
||||
<h2 className="text-lg font-semibold">Theme Code</h2>
|
||||
</div>
|
||||
{preset && preset !== "default" && (
|
||||
<div className="mt-4 rounded-md overflow-hidden border">
|
||||
|
||||
@@ -40,11 +40,10 @@ const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => {
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">{localColor}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className="h-8 w-8 rounded border cursor-pointer overflow-hidden relative flex items-center justify-center"
|
||||
className="h-8 w-8 border cursor-pointer overflow-hidden relative flex items-center justify-center rounded"
|
||||
style={{ backgroundColor: localColor }}
|
||||
onClick={() => 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/25 border border-border/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
import React, { useState } 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 { Contrast, Check, AlertTriangle, Info, Moon, Sun } 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";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
|
||||
type ContrastCheckerProps = {
|
||||
currentStyles: ThemeStyleProps;
|
||||
};
|
||||
|
||||
const MIN_CONTRAST_RATIO = 4.5;
|
||||
|
||||
type ColorCategory = "content" | "interactive" | "functional";
|
||||
|
||||
type ColorPair = {
|
||||
id: string;
|
||||
foregroundId: keyof ThemeStyleProps;
|
||||
backgroundId: keyof ThemeStyleProps;
|
||||
foreground: string | undefined;
|
||||
background: string | undefined;
|
||||
label: string;
|
||||
category: ColorCategory;
|
||||
};
|
||||
|
||||
const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => {
|
||||
const [filter, setFilter] = useState<"all" | "issues">("all");
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
const colorPairsToCheck: ColorPair[] = [
|
||||
// Content - Base, background, cards, containers
|
||||
{
|
||||
id: "base",
|
||||
foregroundId: "foreground",
|
||||
backgroundId: "background",
|
||||
foreground: currentStyles?.["foreground"],
|
||||
background: currentStyles?.["background"],
|
||||
label: "Base",
|
||||
category: "content",
|
||||
},
|
||||
{
|
||||
id: "card",
|
||||
foregroundId: "card-foreground",
|
||||
backgroundId: "card",
|
||||
foreground: currentStyles?.["card-foreground"],
|
||||
background: currentStyles?.["card"],
|
||||
label: "Card",
|
||||
category: "content",
|
||||
},
|
||||
{
|
||||
id: "popover",
|
||||
foregroundId: "popover-foreground",
|
||||
backgroundId: "popover",
|
||||
foreground: currentStyles?.["popover-foreground"],
|
||||
background: currentStyles?.["popover"],
|
||||
label: "Popover",
|
||||
category: "content",
|
||||
},
|
||||
{
|
||||
id: "muted",
|
||||
foregroundId: "muted-foreground",
|
||||
backgroundId: "muted",
|
||||
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",
|
||||
backgroundId: "destructive",
|
||||
foreground: currentStyles?.["destructive-foreground"],
|
||||
background: currentStyles?.["destructive"],
|
||||
label: "Destructive",
|
||||
category: "functional",
|
||||
},
|
||||
{
|
||||
id: "sidebar",
|
||||
foregroundId: "sidebar-foreground",
|
||||
backgroundId: "sidebar",
|
||||
foreground: currentStyles?.["sidebar-foreground"],
|
||||
background: currentStyles?.["sidebar"],
|
||||
label: "Sidebar Base",
|
||||
category: "functional",
|
||||
},
|
||||
{
|
||||
id: "sidebar-primary",
|
||||
foregroundId: "sidebar-primary-foreground",
|
||||
backgroundId: "sidebar-primary",
|
||||
foreground: currentStyles?.["sidebar-primary-foreground"],
|
||||
background: currentStyles?.["sidebar-primary"],
|
||||
label: "Sidebar Primary",
|
||||
category: "functional",
|
||||
},
|
||||
{
|
||||
id: "sidebar-accent",
|
||||
foregroundId: "sidebar-accent-foreground",
|
||||
backgroundId: "sidebar-accent",
|
||||
foreground: currentStyles?.["sidebar-accent-foreground"],
|
||||
background: currentStyles?.["sidebar-accent"],
|
||||
label: "Sidebar Accent",
|
||||
category: "functional",
|
||||
},
|
||||
];
|
||||
|
||||
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<ColorCategory, string> = {
|
||||
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 (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="relative h-8 px-2 gap-1.5 text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
>
|
||||
<Contrast className="h-4 w-4" />
|
||||
<span className="text-sm hidden md:block">Contrast</span>
|
||||
</Button>
|
||||
<TooltipContent>Check contrast accessibility</TooltipContent>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-screen-lg max-h-[90vh]">
|
||||
<DialogHeader className="mb-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
Contrast Checker
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm mt-1">
|
||||
WCAG 2.0 AA requires a contrast ratio of at least{" "}
|
||||
{MIN_CONTRAST_RATIO}:1{" • "}
|
||||
<a
|
||||
href="https://www.w3.org/TR/WCAG21/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline hover:text-primary/80 transition-colors"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<div className="items-center gap-2 hidden md:flex">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => toggleTheme({ x: e.clientX, y: e.clientY })}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Sun className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Moon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p className="text-xs">Toggle theme</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant={filter === "all" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "issues" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setFilter("issues")}
|
||||
>
|
||||
<AlertTriangle className={cn("h-3 w-3 mr-1")} />
|
||||
Issues ({totalIssues})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="h-[calc(90vh-12rem)]">
|
||||
<div className="space-y-6">
|
||||
{groupedPairs.map((group) => (
|
||||
<div key={group.category} className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-md font-semibold">{group.label}</h2>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{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 (
|
||||
<Card
|
||||
key={pair.id}
|
||||
className={cn(
|
||||
"transition-all duration-200",
|
||||
!isValid && " border-dashed"
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3
|
||||
className={cn(
|
||||
"font-medium flex items-center",
|
||||
!isValid && "text-destructive"
|
||||
)}
|
||||
>
|
||||
{pair.label}
|
||||
{!isValid && (
|
||||
<AlertTriangle className="size-3.5 ml-1" />
|
||||
)}
|
||||
</h3>
|
||||
<Badge
|
||||
variant={isValid ? "default" : "destructive"}
|
||||
className={cn(
|
||||
"flex items-center gap-1 text-xs",
|
||||
isValid
|
||||
? "bg-muted text-muted-foreground"
|
||||
: "bg-destructive text-destructive-foreground"
|
||||
)}
|
||||
>
|
||||
{isValid ? (
|
||||
<>
|
||||
<Check className="h-3 w-3" />
|
||||
{contrastRatio}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{contrastRatio}
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex flex-col items-center gap-3 flex-1">
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div
|
||||
style={{
|
||||
backgroundColor:
|
||||
pair.background ?? "#000000",
|
||||
}}
|
||||
className="h-12 w-12 rounded-md border shadow-sm flex-shrink-0"
|
||||
></div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">
|
||||
Background
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{pair.background}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div
|
||||
style={{
|
||||
backgroundColor:
|
||||
pair.foreground ?? "#ffffff",
|
||||
}}
|
||||
className="h-12 w-12 rounded-md border shadow-sm flex-shrink-0"
|
||||
></div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">
|
||||
Foreground
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{pair.foreground}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor:
|
||||
pair.background ?? "transparent",
|
||||
}}
|
||||
className="flex-1 h-full min-h-[120px] rounded-lg border shadow-sm flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
{pair.foreground && pair.background ? (
|
||||
<div className="text-center p-4">
|
||||
<p
|
||||
style={{ color: pair.foreground }}
|
||||
className="text-4xl font-bold tracking-wider mb-2"
|
||||
>
|
||||
Aa
|
||||
</p>
|
||||
<p
|
||||
style={{ color: pair.foreground }}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Sample Text
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Preview
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContrastChecker;
|
||||
@@ -13,7 +13,10 @@ const ControlSection = ({
|
||||
const [isExpanded, setIsExpanded] = useState(expanded);
|
||||
|
||||
return (
|
||||
<div id={id} className={cn("mb-4 border rounded-lg overflow-hidden", className)}>
|
||||
<div
|
||||
id={id}
|
||||
className={cn("mb-4 border rounded-lg overflow-hidden", className)}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between p-3 cursor-pointer bg-background hover:bg-muted"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
@@ -35,7 +38,7 @@ const ControlSection = ({
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-200",
|
||||
isExpanded ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0",
|
||||
isExpanded ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0"
|
||||
)}
|
||||
>
|
||||
<div className="p-3 bg-background border-t">{children}</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
ResizablePanelGroup,
|
||||
ResizablePanel,
|
||||
@@ -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<EditorProps> = ({ 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<EditorProps> = ({ config }) => {
|
||||
<div className="h-full hidden md:block">
|
||||
<ResizablePanelGroup direction="horizontal" className="h-full">
|
||||
<ResizablePanel defaultSize={30} minSize={20} maxSize={30}>
|
||||
<div className="h-full p-4">
|
||||
<div className="h-full flex flex-col">
|
||||
<Controls
|
||||
styles={styles}
|
||||
onChange={handleStyleChange}
|
||||
@@ -64,34 +62,18 @@ const Editor: React.FC<EditorProps> = ({ config }) => {
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={45} minSize={20}>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 min-h-0 p-4">
|
||||
<Preview
|
||||
styles={styles}
|
||||
currentMode={themeState.currentMode}
|
||||
isCodePanelOpen={isCodePanelOpen}
|
||||
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
|
||||
/>
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<Preview styles={styles} currentMode={themeState.currentMode} />
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
{isCodePanelOpen && (
|
||||
<>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={25} minSize={10}>
|
||||
<CodePanel
|
||||
themeEditorState={themeState}
|
||||
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
|
||||
{/* Mobile Layout */}
|
||||
<div className="h-full md:hidden">
|
||||
<Tabs defaultValue="controls" className="h-full">
|
||||
<TabsList className="w-full">
|
||||
<TabsList className="w-full rounded-none">
|
||||
<TabsTrigger value="controls" className="flex-1">
|
||||
<Sliders className="h-4 w-4 mr-2" />
|
||||
Controls
|
||||
@@ -99,12 +81,9 @@ const Editor: React.FC<EditorProps> = ({ config }) => {
|
||||
<TabsTrigger value="preview" className="flex-1">
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code" className="flex-1">
|
||||
Code
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="controls" className="h-[calc(100%-2.5rem)]">
|
||||
<div className="h-full p-4">
|
||||
<TabsContent value="controls" className="h-[calc(100%-2.5rem)] mt-0">
|
||||
<div className="h-full flex flex-col">
|
||||
<Controls
|
||||
styles={styles}
|
||||
onChange={handleStyleChange}
|
||||
@@ -112,22 +91,11 @@ const Editor: React.FC<EditorProps> = ({ config }) => {
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="preview" className="h-[calc(100%-2.5rem)]">
|
||||
<div className="h-full p-4">
|
||||
<Preview
|
||||
styles={styles}
|
||||
currentMode={themeState.currentMode}
|
||||
isCodePanelOpen={isCodePanelOpen}
|
||||
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
|
||||
/>
|
||||
<TabsContent value="preview" className="h-[calc(100%-2.5rem)] mt-0">
|
||||
<div className="h-full flex flex-col">
|
||||
<Preview styles={styles} currentMode={themeState.currentMode} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="code" className="h-[calc(100%-2.5rem)]">
|
||||
<CodePanel
|
||||
themeEditorState={themeState}
|
||||
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
"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";
|
||||
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<HTMLButtonElement>) => {
|
||||
const { clientX: x, clientY: y } = event;
|
||||
toggleTheme({ x, y });
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="border-b">
|
||||
<div className="px-2 md:px-4 py-4 flex items-center gap-2 justify-between">
|
||||
<div className="p-4 flex items-center gap-2 justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<Logo className="size-6" title="tweakcn" />
|
||||
@@ -38,7 +30,7 @@ export function Header() {
|
||||
<GitHubIcon className="size-4" />
|
||||
{stargazersCount > 0 && stargazersCount.toLocaleString()}
|
||||
</SocialLink>
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<div className="hidden md:flex items-center gap-3.5">
|
||||
<SocialLink
|
||||
href="https://github.com/sponsors/jnsahaj"
|
||||
@@ -54,20 +46,6 @@ export function Header() {
|
||||
<SocialLink href="https://x.com/iamsahaj_xyz">
|
||||
<TwitterIcon className="size-4" />
|
||||
</SocialLink>
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<SwitchPrimitives.Root
|
||||
checked={theme === "dark"}
|
||||
onClick={handleThemeToggle}
|
||||
className="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent data-[state=unchecked]:bg-input"
|
||||
>
|
||||
<SwitchPrimitives.Thumb className="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 flex items-center justify-center">
|
||||
{theme === "dark" ? (
|
||||
<Moon className="size-3" />
|
||||
) : (
|
||||
<Sun className="size-3" />
|
||||
)}
|
||||
</SwitchPrimitives.Thumb>
|
||||
</SwitchPrimitives.Root>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -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,37 +16,26 @@ 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 {
|
||||
COMMON_STYLES,
|
||||
DEFAULT_FONT_MONO,
|
||||
DEFAULT_FONT_SANS,
|
||||
DEFAULT_FONT_SERIF,
|
||||
COMMON_STYLES,
|
||||
defaultThemeState,
|
||||
} 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 = ({
|
||||
styles,
|
||||
currentMode,
|
||||
onChange,
|
||||
}: ThemeEditorControlsProps) => {
|
||||
const {
|
||||
applyThemePreset,
|
||||
themeState,
|
||||
resetToCurrentPreset,
|
||||
resetToDefault,
|
||||
hasDefaultThemeChanged,
|
||||
hasCurrentPresetChanged,
|
||||
} = useEditorStore();
|
||||
const [cssImportOpen, setCssImportOpen] = useState(false);
|
||||
const { applyThemePreset, themeState } = useEditorStore();
|
||||
|
||||
const currentStyles = React.useMemo(
|
||||
() => ({
|
||||
@@ -82,22 +71,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 +79,368 @@ const ThemeControlPanel = ({
|
||||
const radius = parseFloat(currentStyles.radius.replace("rem", ""));
|
||||
|
||||
return (
|
||||
<div className="space-y-4 h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-lg font-semibold">Theme Editor</h2>
|
||||
</div>
|
||||
<ThemeControlActions
|
||||
hasChanges={hasDefaultThemeChanged()}
|
||||
hasPresetChanges={hasCurrentPresetChanged()}
|
||||
onReset={resetToDefault}
|
||||
onResetToPreset={resetToCurrentPreset}
|
||||
onImportClick={() => setCssImportOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 ml-1">
|
||||
<>
|
||||
<div className="border-b">
|
||||
<ThemePresetSelect
|
||||
presets={presets}
|
||||
currentPreset={themeState.preset || null}
|
||||
onPresetChange={applyThemePreset}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4 min-h-0 flex-1 flex flex-col">
|
||||
<Tabs
|
||||
defaultValue="colors"
|
||||
className="w-full flex-1 flex flex-col min-h-0"
|
||||
>
|
||||
<div className="px-4 mt-2">
|
||||
<TabsList className="inline-flex w-fit items-center justify-center rounded-full bg-background px-0 text-muted-foreground">
|
||||
<TabsTriggerPill value="colors">Colors</TabsTriggerPill>
|
||||
<TabsTriggerPill value="typography">Typography</TabsTriggerPill>
|
||||
<TabsTriggerPill value="other">Other</TabsTriggerPill>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="colors" className="w-full h-full">
|
||||
<TabsList className="grid grid-cols-3 mb-3 w-full">
|
||||
<TabsTrigger value="colors">Colors</TabsTrigger>
|
||||
<TabsTrigger value="typography">Typography</TabsTrigger>
|
||||
<TabsTrigger value="other">Other</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="h-full pb-40">
|
||||
<TabsContent value="colors">
|
||||
<ControlSection title="Primary Colors" id="primary-colors" expanded>
|
||||
<ColorPicker
|
||||
color={currentStyles.primary}
|
||||
onChange={(color) => updateStyle("primary", color)}
|
||||
label="Primary"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["primary-foreground"]}
|
||||
onChange={(color) => updateStyle("primary-foreground", color)}
|
||||
label="Primary Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Secondary Colors" expanded>
|
||||
<ColorPicker
|
||||
color={currentStyles.secondary}
|
||||
onChange={(color) => updateStyle("secondary", color)}
|
||||
label="Secondary"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["secondary-foreground"]}
|
||||
onChange={(color) => updateStyle("secondary-foreground", color)}
|
||||
label="Secondary Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Accent Colors" expanded>
|
||||
<ColorPicker
|
||||
color={currentStyles.accent}
|
||||
onChange={(color) => updateStyle("accent", color)}
|
||||
label="Accent"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["accent-foreground"]}
|
||||
onChange={(color) => updateStyle("accent-foreground", color)}
|
||||
label="Accent Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Base Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.background}
|
||||
onChange={(color) => updateStyle("background", color)}
|
||||
label="Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles.foreground}
|
||||
onChange={(color) => updateStyle("foreground", color)}
|
||||
label="Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Card Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.card}
|
||||
onChange={(color) => updateStyle("card", color)}
|
||||
label="Card Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["card-foreground"]}
|
||||
onChange={(color) => updateStyle("card-foreground", color)}
|
||||
label="Card Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Popover Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.popover}
|
||||
onChange={(color) => updateStyle("popover", color)}
|
||||
label="Popover Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["popover-foreground"]}
|
||||
onChange={(color) => updateStyle("popover-foreground", color)}
|
||||
label="Popover Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Muted Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.muted}
|
||||
onChange={(color) => updateStyle("muted", color)}
|
||||
label="Muted"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["muted-foreground"]}
|
||||
onChange={(color) => updateStyle("muted-foreground", color)}
|
||||
label="Muted Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Destructive Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.destructive}
|
||||
onChange={(color) => updateStyle("destructive", color)}
|
||||
label="Destructive"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["destructive-foreground"]}
|
||||
onChange={(color) =>
|
||||
updateStyle("destructive-foreground", color)
|
||||
}
|
||||
label="Destructive Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Border & Input Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.border}
|
||||
onChange={(color) => updateStyle("border", color)}
|
||||
label="Border"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles.input}
|
||||
onChange={(color) => updateStyle("input", color)}
|
||||
label="Input"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles.ring}
|
||||
onChange={(color) => updateStyle("ring", color)}
|
||||
label="Ring"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Chart Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-1"]}
|
||||
onChange={(color) => updateStyle("chart-1", color)}
|
||||
label="Chart 1"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-2"]}
|
||||
onChange={(color) => updateStyle("chart-2", color)}
|
||||
label="Chart 2"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-3"]}
|
||||
onChange={(color) => updateStyle("chart-3", color)}
|
||||
label="Chart 3"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-4"]}
|
||||
onChange={(color) => updateStyle("chart-4", color)}
|
||||
label="Chart 4"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-5"]}
|
||||
onChange={(color) => updateStyle("chart-5", color)}
|
||||
label="Chart 5"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Sidebar Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.sidebar}
|
||||
onChange={(color) => updateStyle("sidebar", color)}
|
||||
label="Sidebar Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-foreground"]}
|
||||
onChange={(color) => updateStyle("sidebar-foreground", color)}
|
||||
label="Sidebar Foreground"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-primary"]}
|
||||
onChange={(color) => updateStyle("sidebar-primary", color)}
|
||||
label="Sidebar Primary"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-primary-foreground"]}
|
||||
onChange={(color) =>
|
||||
updateStyle("sidebar-primary-foreground", color)
|
||||
}
|
||||
label="Sidebar Primary Foreground"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-accent"]}
|
||||
onChange={(color) => updateStyle("sidebar-accent", color)}
|
||||
label="Sidebar Accent"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-accent-foreground"]}
|
||||
onChange={(color) =>
|
||||
updateStyle("sidebar-accent-foreground", color)
|
||||
}
|
||||
label="Sidebar Accent Foreground"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-border"]}
|
||||
onChange={(color) => updateStyle("sidebar-border", color)}
|
||||
label="Sidebar Border"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-ring"]}
|
||||
onChange={(color) => updateStyle("sidebar-ring", color)}
|
||||
label="Sidebar Ring"
|
||||
/>
|
||||
</ControlSection>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="typography" className="flex flex-col gap-4">
|
||||
<div className="p-3 bg-muted/50 rounded-md border mb-2 flex items-start gap-2.5">
|
||||
<AlertCircle className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>
|
||||
To use custom fonts, embed them in your project. <br />
|
||||
See{" "}
|
||||
<a
|
||||
href="https://tailwindcss.com/docs/font-family"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2 hover:text-muted-foreground/90"
|
||||
>
|
||||
Tailwind docs
|
||||
</a>{" "}
|
||||
for details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ControlSection title="Font Family" expanded>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="font-sans" className="text-xs mb-1.5 block">
|
||||
Sans-Serif Font
|
||||
</Label>
|
||||
<ThemeFontSelect
|
||||
fonts={{ ...sansSerifFonts, ...serifFonts, ...monoFonts }}
|
||||
defaultValue={DEFAULT_FONT_SANS}
|
||||
currentFont={getAppliedThemeFont(themeState, "font-sans")}
|
||||
onFontChange={(value) => updateStyle("font-sans", value)}
|
||||
<ScrollArea className="h-full pb-40 p-4 pt-0 flex-1">
|
||||
<TabsContent value="colors">
|
||||
<ControlSection
|
||||
title="Primary Colors"
|
||||
id="primary-colors"
|
||||
expanded
|
||||
>
|
||||
<ColorPicker
|
||||
color={currentStyles.primary}
|
||||
onChange={(color) => updateStyle("primary", color)}
|
||||
label="Primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="font-serif" className="text-xs mb-1.5 block">
|
||||
Serif Font
|
||||
</Label>
|
||||
<ThemeFontSelect
|
||||
fonts={{ ...serifFonts, ...sansSerifFonts, ...monoFonts }}
|
||||
defaultValue={DEFAULT_FONT_SERIF}
|
||||
currentFont={getAppliedThemeFont(themeState, "font-serif")}
|
||||
onFontChange={(value) => updateStyle("font-serif", value)}
|
||||
<ColorPicker
|
||||
color={currentStyles["primary-foreground"]}
|
||||
onChange={(color) => updateStyle("primary-foreground", color)}
|
||||
label="Primary Foreground"
|
||||
/>
|
||||
</div>
|
||||
</ControlSection>
|
||||
|
||||
<Separator className="my-4" />
|
||||
<div>
|
||||
<Label htmlFor="font-mono" className="text-xs mb-1.5 block">
|
||||
Monospace Font
|
||||
</Label>
|
||||
<ThemeFontSelect
|
||||
fonts={{ ...monoFonts, ...sansSerifFonts, ...serifFonts }}
|
||||
defaultValue={DEFAULT_FONT_MONO}
|
||||
currentFont={getAppliedThemeFont(themeState, "font-mono")}
|
||||
onFontChange={(value) => updateStyle("font-mono", value)}
|
||||
<ControlSection title="Secondary Colors" expanded>
|
||||
<ColorPicker
|
||||
color={currentStyles.secondary}
|
||||
onChange={(color) => updateStyle("secondary", color)}
|
||||
label="Secondary"
|
||||
/>
|
||||
</div>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Letter Spacing" expanded>
|
||||
<SliderWithInput
|
||||
value={parseFloat(
|
||||
currentStyles["letter-spacing"]?.replace("em", "")
|
||||
)}
|
||||
onChange={(value) =>
|
||||
updateStyle("letter-spacing", `${value}em`)
|
||||
}
|
||||
min={-0.5}
|
||||
max={0.5}
|
||||
step={0.025}
|
||||
unit="em"
|
||||
label="Letter Spacing"
|
||||
/>
|
||||
</ControlSection>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="other">
|
||||
<ControlSection title="Radius" expanded>
|
||||
<SliderWithInput
|
||||
value={radius}
|
||||
onChange={(value) => updateStyle("radius", `${value}rem`)}
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.025}
|
||||
unit="rem"
|
||||
label="Radius"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Spacing" expanded>
|
||||
<SliderWithInput
|
||||
value={parseFloat(currentStyles.spacing?.replace("rem", ""))}
|
||||
onChange={(value) => updateStyle("spacing", `${value}rem`)}
|
||||
min={0.15}
|
||||
max={0.35}
|
||||
step={0.01}
|
||||
unit="rem"
|
||||
label="Spacing"
|
||||
/>
|
||||
</ControlSection>
|
||||
<div className="mt-6">
|
||||
<ShadowControl
|
||||
shadowColor={currentStyles["shadow-color"]}
|
||||
shadowOpacity={parseFloat(currentStyles["shadow-opacity"])}
|
||||
shadowBlur={parseFloat(
|
||||
currentStyles["shadow-blur"]?.replace("px", "")
|
||||
)}
|
||||
shadowSpread={parseFloat(
|
||||
currentStyles["shadow-spread"]?.replace("px", "")
|
||||
)}
|
||||
shadowOffsetX={parseFloat(
|
||||
currentStyles["shadow-offset-x"]?.replace("px", "")
|
||||
)}
|
||||
shadowOffsetY={parseFloat(
|
||||
currentStyles["shadow-offset-y"]?.replace("px", "")
|
||||
)}
|
||||
onChange={(key, value) => {
|
||||
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`);
|
||||
<ColorPicker
|
||||
color={currentStyles["secondary-foreground"]}
|
||||
onChange={(color) =>
|
||||
updateStyle("secondary-foreground", color)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
label="Secondary Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<CssImportDialog
|
||||
open={cssImportOpen}
|
||||
onOpenChange={setCssImportOpen}
|
||||
onImport={handleCssImport}
|
||||
/>
|
||||
</div>
|
||||
<ControlSection title="Accent Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.accent}
|
||||
onChange={(color) => updateStyle("accent", color)}
|
||||
label="Accent"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["accent-foreground"]}
|
||||
onChange={(color) => updateStyle("accent-foreground", color)}
|
||||
label="Accent Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Base Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.background}
|
||||
onChange={(color) => updateStyle("background", color)}
|
||||
label="Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles.foreground}
|
||||
onChange={(color) => updateStyle("foreground", color)}
|
||||
label="Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Card Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.card}
|
||||
onChange={(color) => updateStyle("card", color)}
|
||||
label="Card Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["card-foreground"]}
|
||||
onChange={(color) => updateStyle("card-foreground", color)}
|
||||
label="Card Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Popover Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.popover}
|
||||
onChange={(color) => updateStyle("popover", color)}
|
||||
label="Popover Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["popover-foreground"]}
|
||||
onChange={(color) => updateStyle("popover-foreground", color)}
|
||||
label="Popover Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Muted Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.muted}
|
||||
onChange={(color) => updateStyle("muted", color)}
|
||||
label="Muted"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["muted-foreground"]}
|
||||
onChange={(color) => updateStyle("muted-foreground", color)}
|
||||
label="Muted Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Destructive Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.destructive}
|
||||
onChange={(color) => updateStyle("destructive", color)}
|
||||
label="Destructive"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["destructive-foreground"]}
|
||||
onChange={(color) =>
|
||||
updateStyle("destructive-foreground", color)
|
||||
}
|
||||
label="Destructive Foreground"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Border & Input Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.border}
|
||||
onChange={(color) => updateStyle("border", color)}
|
||||
label="Border"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles.input}
|
||||
onChange={(color) => updateStyle("input", color)}
|
||||
label="Input"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles.ring}
|
||||
onChange={(color) => updateStyle("ring", color)}
|
||||
label="Ring"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Chart Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-1"]}
|
||||
onChange={(color) => updateStyle("chart-1", color)}
|
||||
label="Chart 1"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-2"]}
|
||||
onChange={(color) => updateStyle("chart-2", color)}
|
||||
label="Chart 2"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-3"]}
|
||||
onChange={(color) => updateStyle("chart-3", color)}
|
||||
label="Chart 3"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-4"]}
|
||||
onChange={(color) => updateStyle("chart-4", color)}
|
||||
label="Chart 4"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["chart-5"]}
|
||||
onChange={(color) => updateStyle("chart-5", color)}
|
||||
label="Chart 5"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Sidebar Colors">
|
||||
<ColorPicker
|
||||
color={currentStyles.sidebar}
|
||||
onChange={(color) => updateStyle("sidebar", color)}
|
||||
label="Sidebar Background"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-foreground"]}
|
||||
onChange={(color) => updateStyle("sidebar-foreground", color)}
|
||||
label="Sidebar Foreground"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-primary"]}
|
||||
onChange={(color) => updateStyle("sidebar-primary", color)}
|
||||
label="Sidebar Primary"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-primary-foreground"]}
|
||||
onChange={(color) =>
|
||||
updateStyle("sidebar-primary-foreground", color)
|
||||
}
|
||||
label="Sidebar Primary Foreground"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-accent"]}
|
||||
onChange={(color) => updateStyle("sidebar-accent", color)}
|
||||
label="Sidebar Accent"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-accent-foreground"]}
|
||||
onChange={(color) =>
|
||||
updateStyle("sidebar-accent-foreground", color)
|
||||
}
|
||||
label="Sidebar Accent Foreground"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-border"]}
|
||||
onChange={(color) => updateStyle("sidebar-border", color)}
|
||||
label="Sidebar Border"
|
||||
/>
|
||||
<ColorPicker
|
||||
color={currentStyles["sidebar-ring"]}
|
||||
onChange={(color) => updateStyle("sidebar-ring", color)}
|
||||
label="Sidebar Ring"
|
||||
/>
|
||||
</ControlSection>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="typography" className="flex flex-col gap-4">
|
||||
<div className="p-3 bg-muted/50 rounded-md border mb-2 flex items-start gap-2.5">
|
||||
<AlertCircle className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>
|
||||
To use custom fonts, embed them in your project. <br />
|
||||
See{" "}
|
||||
<a
|
||||
href="https://tailwindcss.com/docs/font-family"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2 hover:text-muted-foreground/90"
|
||||
>
|
||||
Tailwind docs
|
||||
</a>{" "}
|
||||
for details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ControlSection title="Font Family" expanded>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="font-sans" className="text-xs mb-1.5 block">
|
||||
Sans-Serif Font
|
||||
</Label>
|
||||
<ThemeFontSelect
|
||||
fonts={{ ...sansSerifFonts, ...serifFonts, ...monoFonts }}
|
||||
defaultValue={DEFAULT_FONT_SANS}
|
||||
currentFont={getAppliedThemeFont(themeState, "font-sans")}
|
||||
onFontChange={(value) => updateStyle("font-sans", value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="font-serif" className="text-xs mb-1.5 block">
|
||||
Serif Font
|
||||
</Label>
|
||||
<ThemeFontSelect
|
||||
fonts={{ ...serifFonts, ...sansSerifFonts, ...monoFonts }}
|
||||
defaultValue={DEFAULT_FONT_SERIF}
|
||||
currentFont={getAppliedThemeFont(themeState, "font-serif")}
|
||||
onFontChange={(value) => updateStyle("font-serif", value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
<div>
|
||||
<Label htmlFor="font-mono" className="text-xs mb-1.5 block">
|
||||
Monospace Font
|
||||
</Label>
|
||||
<ThemeFontSelect
|
||||
fonts={{ ...monoFonts, ...sansSerifFonts, ...serifFonts }}
|
||||
defaultValue={DEFAULT_FONT_MONO}
|
||||
currentFont={getAppliedThemeFont(themeState, "font-mono")}
|
||||
onFontChange={(value) => updateStyle("font-mono", value)}
|
||||
/>
|
||||
</div>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Letter Spacing" expanded>
|
||||
<SliderWithInput
|
||||
value={parseFloat(
|
||||
currentStyles["letter-spacing"]?.replace("em", "")
|
||||
)}
|
||||
onChange={(value) =>
|
||||
updateStyle("letter-spacing", `${value}em`)
|
||||
}
|
||||
min={-0.5}
|
||||
max={0.5}
|
||||
step={0.025}
|
||||
unit="em"
|
||||
label="Letter Spacing"
|
||||
/>
|
||||
</ControlSection>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="other">
|
||||
<ControlSection title="Radius" expanded>
|
||||
<SliderWithInput
|
||||
value={radius}
|
||||
onChange={(value) => updateStyle("radius", `${value}rem`)}
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.025}
|
||||
unit="rem"
|
||||
label="Radius"
|
||||
/>
|
||||
</ControlSection>
|
||||
|
||||
<ControlSection title="Spacing" expanded>
|
||||
<SliderWithInput
|
||||
value={parseFloat(currentStyles.spacing?.replace("rem", ""))}
|
||||
onChange={(value) => updateStyle("spacing", `${value}rem`)}
|
||||
min={0.15}
|
||||
max={0.35}
|
||||
step={0.01}
|
||||
unit="rem"
|
||||
label="Spacing"
|
||||
/>
|
||||
</ControlSection>
|
||||
<div className="mt-6">
|
||||
<ShadowControl
|
||||
shadowColor={currentStyles["shadow-color"]}
|
||||
shadowOpacity={parseFloat(currentStyles["shadow-opacity"])}
|
||||
shadowBlur={parseFloat(
|
||||
currentStyles["shadow-blur"]?.replace("px", "")
|
||||
)}
|
||||
shadowSpread={parseFloat(
|
||||
currentStyles["shadow-spread"]?.replace("px", "")
|
||||
)}
|
||||
shadowOffsetX={parseFloat(
|
||||
currentStyles["shadow-offset-x"]?.replace("px", "")
|
||||
)}
|
||||
shadowOffsetY={parseFloat(
|
||||
currentStyles["shadow-offset-y"]?.replace("px", "")
|
||||
)}
|
||||
onChange={(key, value) => {
|
||||
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`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
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";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Moon,
|
||||
Search,
|
||||
Shuffle,
|
||||
@@ -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,12 +71,16 @@ const ThemePresetSelect: React.FC<ThemePresetSelectProps> = ({
|
||||
const mode = themeState.currentMode;
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const presetNames = useMemo(() => ["default", ...Object.keys(presets)], [presets]);
|
||||
const value = presetNames?.find((name) => name === currentPreset);
|
||||
const currentIndex = useMemo(
|
||||
() => presetNames.indexOf(value || "default"),
|
||||
[presetNames, value]
|
||||
const presetNames = useMemo(
|
||||
() => ["default", ...Object.keys(presets)],
|
||||
[presets]
|
||||
);
|
||||
const value = presetNames?.find((name) => name === currentPreset);
|
||||
const currentIndex =
|
||||
useMemo(
|
||||
() => presetNames.indexOf(value || "default"),
|
||||
[presetNames, value]
|
||||
) ?? 0;
|
||||
|
||||
const randomize = useCallback(() => {
|
||||
const random = Math.floor(Math.random() * presetNames.length);
|
||||
@@ -107,14 +120,14 @@ const ThemePresetSelect: React.FC<ThemePresetSelectProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center">
|
||||
<TooltipProvider>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<PopoverTrigger className="bg-muted/10" asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"w-full md:min-w-64 h-10 justify-between group relative",
|
||||
"w-full md:min-w-56 min-h-14 rounded-none justify-between group relative",
|
||||
(!value || value === "default") &&
|
||||
!hasChangedThemeFromDefault &&
|
||||
"ring-2 ring-offset-1 ring-offset-background ring-primary/30 animate-pulse"
|
||||
@@ -123,26 +136,34 @@ const ThemePresetSelect: React.FC<ThemePresetSelectProps> = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex gap-0.5">
|
||||
<ColorBox
|
||||
color={getPresetThemeStyles(value || "default")[mode].primary}
|
||||
color={
|
||||
getPresetThemeStyles(value || "default")[mode].primary
|
||||
}
|
||||
/>
|
||||
<ColorBox
|
||||
color={getPresetThemeStyles(value || "default")[mode].accent}
|
||||
color={
|
||||
getPresetThemeStyles(value || "default")[mode].accent
|
||||
}
|
||||
/>
|
||||
<ColorBox
|
||||
color={getPresetThemeStyles(value || "default")[mode].secondary}
|
||||
color={
|
||||
getPresetThemeStyles(value || "default")[mode].secondary
|
||||
}
|
||||
/>
|
||||
<ColorBox
|
||||
color={getPresetThemeStyles(value || "default")[mode].border}
|
||||
color={
|
||||
getPresetThemeStyles(value || "default")[mode].border
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span className="capitalize font-medium">
|
||||
{presets[value || "default"]?.label || "default"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className="size-4 shrink-0 opacity-50" />
|
||||
<ChevronDown className="size-4 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[300px]" align="start">
|
||||
<PopoverContent className="p-0 w-[300px] ml-4" align="center">
|
||||
<Command className="rounded-lg border shadow-md w-full">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex items-center w-full border-b px-3 py-1">
|
||||
@@ -219,7 +240,9 @@ const ThemePresetSelect: React.FC<ThemePresetSelectProps> = ({
|
||||
color={getPresetThemeStyles(presetName)[mode].accent}
|
||||
/>
|
||||
<ColorBox
|
||||
color={getPresetThemeStyles(presetName)[mode].secondary}
|
||||
color={
|
||||
getPresetThemeStyles(presetName)[mode].secondary
|
||||
}
|
||||
/>
|
||||
<ColorBox
|
||||
color={getPresetThemeStyles(presetName)[mode].border}
|
||||
@@ -229,14 +252,15 @@ const ThemePresetSelect: React.FC<ThemePresetSelectProps> = ({
|
||||
<span className="capitalize text-sm font-medium">
|
||||
{presets[presetName]?.label || presetName}
|
||||
</span>
|
||||
{presets[presetName] && isThemeNew(presets[presetName]) && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs rounded-full"
|
||||
>
|
||||
New
|
||||
</Badge>
|
||||
)}
|
||||
{presets[presetName] &&
|
||||
isThemeNew(presets[presetName]) && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs rounded-full"
|
||||
>
|
||||
New
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{presetName === value && (
|
||||
<Check className="h-4 w-4 shrink-0 opacity-70" />
|
||||
@@ -250,25 +274,37 @@ const ThemePresetSelect: React.FC<ThemePresetSelectProps> = ({
|
||||
</Popover>
|
||||
</TooltipProvider>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-10 w-10 shrink-0"
|
||||
title="Previous theme"
|
||||
onClick={() => cycleTheme("prev")}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-10 w-10 shrink-0"
|
||||
title="Next theme"
|
||||
onClick={() => cycleTheme("next")}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-14 shrink-0 rounded-none bg-muted/10"
|
||||
onClick={() => cycleTheme("prev")}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Previous theme</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-14 shrink-0 rounded-none bg-muted/10"
|
||||
onClick={() => cycleTheme("next")}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Next theme</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ActionBar } from "./action-bar";
|
||||
|
||||
const DemoCards = lazy(() => import("@/components/examples/demo-cards"));
|
||||
const DemoMail = lazy(() => import("@/components/examples/mail"));
|
||||
@@ -27,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();
|
||||
@@ -43,91 +42,72 @@ const ThemePreviewPanel = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"max-h-full flex flex-col",
|
||||
isFullscreen && "fixed inset-0 z-50 bg-background p-4"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold">Theme Preview</h2>
|
||||
<div className="flex items-center gap-0">
|
||||
{isFullscreen && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleThemeToggle}
|
||||
className="h-8 group"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Sun className="size-4 group-hover:scale-120 transition-all" />
|
||||
) : (
|
||||
<Moon className="size-4 group-hover:scale-120 transition-all" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Toggle Theme</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleFullscreen}
|
||||
className="h-8 group"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize className="size-4 group-hover:scale-120 transition-all" />
|
||||
) : (
|
||||
<Maximize className="size-4 group-hover:scale-120 transition-all" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isFullscreen ? "Exit full screen" : "Full screen"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{!isCodePanelOpen && !isFullscreen && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onCodePanelToggle(!isCodePanelOpen)}
|
||||
className="h-8 invisible md:visible group"
|
||||
aria-label="Show Code Panel"
|
||||
>
|
||||
<PanelRight className="size-4 group-hover:scale-120 transition-all" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Hide Code Panel</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<>
|
||||
<ActionBar />
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-0 flex flex-col flex-1",
|
||||
isFullscreen && "fixed inset-0 z-50 bg-background"
|
||||
)}
|
||||
>
|
||||
<Tabs defaultValue="cards" className="flex flex-col overflow-hidden">
|
||||
<TabsList className="inline-flex w-fit h-9 items-center justify-center rounded-full bg-background px-0 text-muted-foreground">
|
||||
<TabsTriggerPill value="cards">Cards</TabsTriggerPill>
|
||||
<div className="hidden md:flex">
|
||||
<TabsTriggerPill value="mail">Mail</TabsTriggerPill>
|
||||
<TabsTriggerPill value="tasks">Tasks</TabsTriggerPill>
|
||||
<TabsTriggerPill value="music">Music</TabsTriggerPill>
|
||||
<TabsTriggerPill value="dashboard">Dashboard</TabsTriggerPill>
|
||||
</div>
|
||||
<TabsTriggerPill value="colors">Color Palette</TabsTriggerPill>
|
||||
</TabsList>
|
||||
<div className="flex items-center justify-between px-4 mt-2">
|
||||
<TabsList className="inline-flex w-fit items-center justify-center rounded-full bg-background px-0 text-muted-foreground">
|
||||
<TabsTriggerPill value="cards">Cards</TabsTriggerPill>
|
||||
<div className="hidden md:flex">
|
||||
<TabsTriggerPill value="mail">Mail</TabsTriggerPill>
|
||||
<TabsTriggerPill value="tasks">Tasks</TabsTriggerPill>
|
||||
<TabsTriggerPill value="music">Music</TabsTriggerPill>
|
||||
<TabsTriggerPill value="dashboard">Dashboard</TabsTriggerPill>
|
||||
</div>
|
||||
<TabsTriggerPill value="colors">Color Palette</TabsTriggerPill>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="rounded-lg border mt-2 flex flex-col flex-1">
|
||||
<div className="flex flex-col flex-1">
|
||||
<TabsContent
|
||||
value="cards"
|
||||
className="space-y-6 mt-0 py-4 px-4 h-full"
|
||||
>
|
||||
<div className="flex items-center gap-0">
|
||||
{isFullscreen && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleThemeToggle}
|
||||
className="h-8 group"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Sun className="size-4 group-hover:scale-120 transition-all" />
|
||||
) : (
|
||||
<Moon className="size-4 group-hover:scale-120 transition-all" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Toggle Theme</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleFullscreen}
|
||||
className="h-8 group"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize className="size-4 group-hover:scale-120 transition-all" />
|
||||
) : (
|
||||
<Maximize className="size-4 group-hover:scale-120 transition-all" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isFullscreen ? "Exit full screen" : "Full screen"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="rounded-lg flex flex-col flex-1 border m-4 mt-2 overflow-hidden">
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
<TabsContent value="cards" className="space-y-6 my-4 px-4 h-full">
|
||||
<ExamplesPreviewContainer>
|
||||
<DemoCards />
|
||||
</ExamplesPreviewContainer>
|
||||
@@ -146,7 +126,7 @@ const ThemePreviewPanel = ({
|
||||
value="tasks"
|
||||
className="space-y-6 mt-0 h-full @container"
|
||||
>
|
||||
<ExamplesPreviewContainer className="min-w-[1300px]">
|
||||
<ExamplesPreviewContainer className="min-w-[1300px] ">
|
||||
<DemoTasks />
|
||||
</ExamplesPreviewContainer>
|
||||
</TabsContent>
|
||||
@@ -178,7 +158,7 @@ const ThemePreviewPanel = ({
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getContrastRatio } from "../utils/contrast-checker";
|
||||
import { debounce } from "../utils/debounce";
|
||||
|
||||
type ColorPair = {
|
||||
id: string;
|
||||
foreground: string;
|
||||
background: string;
|
||||
};
|
||||
|
||||
type ContrastResult = {
|
||||
id: string;
|
||||
contrastRatio: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook that calculates the contrast ratio between foreground and background colors for a list of pairs.
|
||||
* @param colorPairs - An array of color pairs, each with an id, foreground color, and background color.
|
||||
* @returns An array of objects, each containing the id and calculated contrast ratio for a pair.
|
||||
*/
|
||||
export function useContrastChecker(colorPairs: ColorPair[]) {
|
||||
const [contrastResults, setContrastResults] = useState<ContrastResult[]>([]);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -58,8 +58,6 @@ export interface ThemeEditorState {
|
||||
export interface ThemeEditorPreviewProps {
|
||||
styles: ThemeStyles;
|
||||
currentMode: "light" | "dark";
|
||||
isCodePanelOpen: boolean;
|
||||
onCodePanelToggle: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export interface ThemeEditorControlsProps {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user