1
0

update: the palette & components

This commit is contained in:
alikia2x (寒寒) 2025-10-04 02:46:29 +08:00
parent 57deaed2e1
commit adbee9647e
29 changed files with 482 additions and 94 deletions

View File

@ -117,6 +117,9 @@
"@primereact/types": "^11.0.0-alpha.1",
"@primeuix/themes": "^1.2.5",
"culori": "^4.0.2",
"jotai": "^2.15.0",
"lucide-react": "^0.544.0",
"motion": "^12.23.22",
"primereact": "11.0.0-alpha.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@ -1447,6 +1450,8 @@
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
"framer-motion": ["framer-motion@12.23.22", "", { "dependencies": { "motion-dom": "^12.23.21", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"frontend": ["frontend@workspace:packages/frontend"],
@ -1645,6 +1650,8 @@
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"jotai": ["jotai@2.15.0", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
@ -1869,6 +1876,12 @@
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
"motion": ["motion@12.23.22", "", { "dependencies": { "framer-motion": "^12.23.22", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-iSq6X9vLHbeYwmHvhK//+U74ROaPnZmBuy60XZzqNl0QtZkWfoZyMDHYnpKuWFv0sNMqHgED8aCXk94LCoQPGg=="],
"motion-dom": ["motion-dom@12.23.21", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ=="],
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

View File

@ -11,7 +11,7 @@
"packages/elysia",
"packages/temp_frontend",
"packages/solid",
"packages/plaette"
"packages/palette"
],
"dependencies": {
"arg": "^5.0.2",

View File

@ -13,6 +13,9 @@
"@primereact/types": "^11.0.0-alpha.1",
"@primeuix/themes": "^1.2.5",
"culori": "^4.0.2",
"jotai": "^2.15.0",
"lucide-react": "^0.544.0",
"motion": "^12.23.22",
"primereact": "11.0.0-alpha.1",
"react": "^19.1.1",
"react-dom": "^19.1.1"

View File

@ -0,0 +1,126 @@
import "virtual:uno.css";
import { type Oklch } from "culori";
import { Picker } from "./components/Picker/Picker";
import { Switch } from "./Switch";
import { i18nProvider } from "./utils";
import { useTheme } from "./ThemeContext";
import { ColorPalette } from "./components/Plaette";
import { Buttons, Paragraph, SearchBar } from "./components/Components";
import { AnimatePresence, motion } from "motion/react";
import { Moon, Sun } from "lucide-react";
import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
const defaultColor: Oklch = { mode: "oklch", h: 29.2339, c: 0.244572, l: 0.596005 };
const colorAtom = atomWithStorage<Oklch>("selectedColor", defaultColor);
const p3Atom = atomWithStorage<boolean>("showP3", false);
function App() {
const [useP3, setUseP3] = useAtom(p3Atom);
const [selectedColor, setSelectedColor] = useAtom(colorAtom);
const { theme, toggleTheme } = useTheme();
const Icon = () => {
if (theme === "dark") {
return (
<AnimatePresence>
<motion.div
exit={{ opacity: 0, scale: 0.6 }}
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
onClick={toggleTheme}
transition={{ duration: 0.5, type: "spring" }}
className="hover:bg-black/10 dark:hover:bg-white/10 w-10 h-10
rounded-full flex items-center justify-center"
>
<Moon size={24} strokeWidth={2.5} />
</motion.div>
</AnimatePresence>
);
} else {
return (
<AnimatePresence>
<motion.div
exit={{ opacity: 0, scale: 0.6 }}
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
onClick={toggleTheme}
transition={{ duration: 0.5, type: "spring" }}
className="hover:bg-black/10 dark:hover:bg-white/10 w-10 h-10
rounded-full flex items-center justify-center"
>
<Sun size={24} strokeWidth={2.5} />
</motion.div>
</AnimatePresence>
);
}
};
return (
<div className="min-h-screen my-12 mx-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-8 text-on-background">CVSA Color Palette Generator</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:[grid-template-columns:2fr_3fr] xl:grid-cols-3 gap-8">
{/* Left Column - Color Picker */}
<div className="xl:col-span-1 bg-white dark:bg-zinc-800 rounded-lg shadow-sm p-6">
<h2 className="text-xl font-semibold text-on-background mb-4">Color Selection</h2>
<div className="space-y-6">
<div>
<label className="block font-bold text-on-background mb-2">OKLCH Color Picker</label>
<div className="mx-3">
<Picker
className="m-3"
i18n={i18nProvider}
useP3={useP3}
selectedColor={selectedColor}
onColorChange={setSelectedColor}
/>
<div className="flex justify-between mt-10">
<span className="font-medium mr-2">Show P3</span>
<Switch checked={useP3} onChange={setUseP3} />
</div>
</div>
</div>
<div>
<label className="block font-bold text-on-background mb-2">
Extract Colors from Image
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
<p className="text-on-surface-variant text-sm">
Image color extraction feature coming soon...
</p>
</div>
</div>
</div>
</div>
{/* Right Column */}
<div className="xl:col-span-2 flex flex-col gap-5">
<div className="bg-white dark:bg-zinc-800 rounded-lg shadow-sm p-6">
<div className="flex h-8 mb-4 justify-between items-center">
<h2 className="text-xl font-semibold">Color Palette</h2>
<Icon />
</div>
<ColorPalette baseColor={selectedColor} />
</div>
<div className="bg-white dark:bg-zinc-800 rounded-lg shadow-sm p-6">
<h2 className="text-xl font-semibold mb-6">Components</h2>
<div className="flex flex-col gap-2">
<SearchBar baseColor={selectedColor} />
<Paragraph baseColor={selectedColor} />
<Buttons baseColor={selectedColor} />
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default App;

View File

@ -0,0 +1,66 @@
// ThemeContext.tsx
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { type ThemeMode } from "./colorTokens";
type ThemeContextType = {
theme: ThemeMode;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const useMediaQuery = (query: string): boolean => {
const isClient = typeof window !== "undefined";
const [matches, setMatches] = useState<boolean>(() => {
if (!isClient) return false;
return window.matchMedia(query).matches;
});
useEffect(() => {
if (!isClient) return;
const mediaQueryList = window.matchMedia(query);
if (mediaQueryList.matches !== matches) {
setMatches(mediaQueryList.matches);
}
const listener = (event: MediaQueryListEvent) => setMatches(event.matches);
mediaQueryList.addEventListener("change", listener);
return () => mediaQueryList.removeEventListener("change", listener);
}, [query, matches, isClient]);
return matches;
};
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
const initialTheme: ThemeMode = prefersDark ? "dark" : "light";
const [theme, setTheme] = useState<ThemeMode>(initialTheme);
const [userToggled, setUserToggled] = useState(false);
useEffect(() => {
if (!userToggled) {
setTheme(prefersDark ? "dark" : "light");
}
}, [prefersDark, userToggled]);
const toggleTheme = useCallback(() => {
setTheme((currentTheme) => (currentTheme === "light" ? "dark" : "light"));
setUserToggled(true);
}, []);
const contextValue = { theme, toggleTheme };
return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
};
export const useTheme = () => {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
return ctx;
};

View File

@ -0,0 +1,35 @@
import { type Oklch } from "culori";
import { getAdjustedColor } from "./utils";
export type ThemeMode = "light" | "dark";
export const buildColorTokens = (base: Oklch) => {
return {
light: {
background: getAdjustedColor(base, 0.98, 0.01),
"bg-elevated-1": getAdjustedColor(base, 1, 0.008),
"body-text": getAdjustedColor(base, 0.1, 0.01),
"border-var-1": getAdjustedColor(base, 0.845, 0.004),
"border-var-2": getAdjustedColor(base, 0.8, 0.007),
"border-var-3": getAdjustedColor(base, 0.755, 0.01),
primary: getAdjustedColor(base, 0.48, 0.08),
"on-primary": getAdjustedColor(base, 0.999, 0.001),
"on-bg-var-2": getAdjustedColor(base, 0.398, 0.0234),
error: { mode: "oklch", l: 0.506, c: 0.192, h: 27.7 } as Oklch,
"on-error": getAdjustedColor(base, 0.99, 0.01)
},
dark: {
background: getAdjustedColor(base, 0.15, 0.002),
"bg-elevated-1": getAdjustedColor(base, 0.2, 0.004),
"body-text": getAdjustedColor(base, 0.9, 0.01),
"border-var-1": getAdjustedColor(base, 0.3, 0.004),
"border-var-2": getAdjustedColor(base, 0.4, 0.007),
"border-var-3": getAdjustedColor(base, 0.5, 0.01),
primary: getAdjustedColor(base, 0.84, 0.1),
"on-primary": getAdjustedColor(base, 0.3, 0.08),
"on-bg-var-2": getAdjustedColor(base, 0.83, 0.028),
error: { mode: "oklch", l: 0.65, c: 0.223, h: 27.8 } as Oklch,
"on-error": getAdjustedColor(base, 0.9, 0.01)
}
};
};

View File

@ -0,0 +1,30 @@
import { SVGProps } from "react";
export const Checkmark: React.FC<SVGProps<SVGSVGElement>> = (props) => {
return (
<svg viewBox="10 5 90 85" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="none"
stroke="currentColor"
strokeWidth={props.strokeWidth || 14}
strokeLinecap="round"
strokeLinejoin="round"
d="M25 50 L45 70 L75 30"
strokeDasharray="100"
strokeDashoffset="100"
style={{
animation: "draw 0.3s forwards ease-out"
}}
/>
<style>
{`
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
`}
</style>
</svg>
);
};

View File

@ -0,0 +1,85 @@
import { useState } from "react";
import { type Oklch, formatHex } from "culori";
import { Copy } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { Checkmark } from "./Check";
import { getAdjustedColor } from "../utils";
interface ColorBlockProps {
baseColor: Oklch;
text: string;
l?: number;
c?: number;
h?: number;
}
const copy = async (text: string) => {
await navigator.clipboard.writeText(text);
};
export const ColorBlock = ({ baseColor, text, l, c, h }: ColorBlockProps) => {
const [hover, setHover] = useState(false);
const [check, setCheck] = useState(false);
const color = getAdjustedColor(baseColor, l, c, h);
const Icon = () => {
if (!check) {
return (
<AnimatePresence>
<motion.div exit={{ opacity: 0 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<Copy size={14} strokeWidth={2.5} />
</motion.div>
</AnimatePresence>
);
} else {
return (
<AnimatePresence>
<motion.div exit={{ opacity: 0 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<Checkmark width={16} height={16} strokeWidth={14} />
</motion.div>
</AnimatePresence>
);
}
};
return (
<div className="w-30 h-36 flex flex-col items-center">
<div className="w-full h-20 relative rounded-lg duration-50" style={{ backgroundColor: formatHex(color) }} />
<span className="mt-2 text-sm">{text}</span>
<div
className="flex items-center justify-center text-sm font-medium px-2 py-0.5 cursor-pointer
mt-1 hover:bg-gray-200 dark:hover:bg-zinc-700 rounded-md"
onClick={() => {
copy(formatHex(color));
setCheck(true);
setTimeout(() => {
setCheck(false);
}, 2500);
}}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => {
setHover(false);
setCheck(false);
}}
>
<AnimatePresence>
{hover && (
<motion.div
exit={{ opacity: 0, width: 0 }}
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: 22 }}
transition={{
opacity: { duration: 0.2, ease: "backOut" },
width: { type: "spring", bounce: 0.2, duration: 0.5 }
}}
>
<Icon />
</motion.div>
)}
</AnimatePresence>
{formatHex(color)}
</div>
</div>
);
};

View File

@ -0,0 +1,75 @@
import { buildColorTokens } from "../colorTokens";
import { useTheme } from "../ThemeContext";
import { formatHex, Oklch } from "culori";
const SearchBar = ({ baseColor }: { baseColor: Oklch }) => {
const { theme } = useTheme();
const tokens = buildColorTokens(baseColor)[theme];
return (
<div
className="w-full h-18 flex items-center justify-center rounded-md"
style={{ backgroundColor: formatHex(tokens.background) }}
>
<input
type="text"
placeholder="搜索"
className="w-2/3 h-10 rounded-lg focus:outline-none text-center"
style={{
backgroundColor: formatHex(tokens["bg-elevated-1"]),
border: `2px solid ${formatHex(tokens["border-var-1"])}`,
color: formatHex(tokens["body-text"])
}}
/>
</div>
);
};
const Paragraph = ({ baseColor }: { baseColor: Oklch }) => {
const { theme } = useTheme();
const tokens = buildColorTokens(baseColor)[theme];
return (
<div
className="w-full h-18 flex items-center justify-center rounded-md"
style={{ backgroundColor: formatHex(tokens.background) }}
>
<p style={{ color: formatHex(tokens["body-text"]) }}>
20241215稿Synthesizer V,
</p>
</div>
);
};
const Buttons = ({ baseColor }: { baseColor: Oklch }) => {
const { theme } = useTheme();
const tokens = buildColorTokens(baseColor)[theme];
return (
<div
className="w-full h-18 flex items-center justify-center rounded-md gap-4 px-10"
style={{ backgroundColor: formatHex(tokens.background) }}
>
<button
className="cursor-pointer font-medium py-1.5 px-4 rounded-lg border-2"
style={{ borderColor: formatHex(tokens["border-var-3"]), color: formatHex(tokens["on-bg-var-2"]) }}
>
Cancel
</button>
<button
className="cursor-pointer font-medium py-2 px-4 rounded-lg"
style={{ backgroundColor: formatHex(tokens.primary), color: formatHex(tokens["on-primary"]) }}
>
Confirm
</button>
<button
className="cursor-pointer font-medium py-2 px-4 rounded-lg"
style={{ backgroundColor: formatHex(tokens.error), color: formatHex(tokens["on-error"]) }}
>
Delete
</button>
</div>
);
};
export { SearchBar, Paragraph, Buttons };

View File

@ -0,0 +1,17 @@
import { Oklch } from "culori";
import { buildColorTokens } from "../colorTokens";
import { useTheme } from "../ThemeContext";
import { ColorBlock } from "./ColorBlock";
export function ColorPalette({ baseColor }: { baseColor: Oklch }) {
const { theme } = useTheme();
const tokens = buildColorTokens(baseColor)[theme];
return (
<div className="grid gap-4 [grid-template-columns:repeat(auto-fill,120px)] justify-between">
{Object.entries(tokens).map(([name, color]) => (
<ColorBlock key={name} baseColor={color} text={name} />
))}
</div>
);
}

View File

@ -5,7 +5,7 @@ import { displaySupportsP3, roundOklch } from "./utils";
export type LchChannel = "l" | "c" | "h";
const toOklchString = (color: Oklch) => {
export const toOklchString = (color: Oklch) => {
return `oklch(${color.l} ${color.c} ${color.h})`;
};
@ -76,12 +76,12 @@ export const Picker = ({ useP3, i18n, selectedColor, onColorChange, ...rest }: P
const [displayColor, setDisplayColor] = useState<Oklch>(selectedColor);
const [hexText, setHexText] = useState(formatHex(selectedColor));
const [oklchText, setOklchText] = useState(toOklchString(selectedColor));
const colorGamut = useP3 ? "p3" : "rgb";
useEffect(() => {
try {
setHexText(formatHex(selectedColor));
setOklchText(toOklchString(selectedColor));
setDisplayColor(selectedColor);
} catch (error) {
console.warn("Invalid color combination");
}
@ -112,7 +112,6 @@ export const Picker = ({ useP3, i18n, selectedColor, onColorChange, ...rest }: P
const hex = e.target.value;
const color = parse(hex);
const oklchColor = oklch(rgb(color));
console.log("hey");
if (oklchColor) {
onColorChange(roundOklch(oklchColor));
setHexText(hex);

View File

@ -36,7 +36,6 @@ export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: Slider
onChange(round(value, precision[channel]));
};
console.log(useP3)
return (
<div className="mb-6">

View File

@ -1,11 +1,13 @@
import { StrictMode } from "react";
import Aura from "@primeuix/themes/aura";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { ThemeProvider } from "./ThemeContext.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>
);

View File

@ -0,0 +1,26 @@
import { Oklch } from "culori";
import { i18nKeys } from "./components/Picker/Picker";
export const i18nProvider = (key: i18nKeys) => {
switch (key) {
case "l":
return "Lightness";
case "c":
return "Chroma";
case "h":
return "Hue";
case "fallback":
return "Fallback";
case "unsupported":
return "Unavailable on this monitor";
}
};
export const getAdjustedColor = (color: Oklch, l?: number, c?: number, h?: number) => {
const newColor = { ...color };
if (l) newColor.l = l;
if (c) newColor.c = c;
if (h) newColor.h = h;
return newColor;
};

View File

@ -1,88 +0,0 @@
import "virtual:uno.css";
import { useEffect, useState } from "react";
import { argbFromHex, themeFromSourceColor, applyTheme } from "@material/material-color-utilities";
import { type Oklch, formatHex } from "culori";
import { i18nKeys, Picker } from "./Picker/Picker";
import { Switch } from "./Switch";
const defaultColor: Oklch = { mode: "oklch", h: 29.2339, c: 0.244572, l: 0.596005 };
const i18nProvider = (key: i18nKeys) => {
switch (key) {
case "l":
return "Lightness";
case "c":
return "Chroma";
case "h":
return "Hue";
case "fallback":
return "Fallback";
case "unsupported":
return "Unavailable on this monitor";
}
};
function App() {
const [useP3, setUseP3] = useState(false);
const [selectedColor, setSelectedColor] = useState<Oklch>(defaultColor);
const colorHex = formatHex(selectedColor);
useEffect(() => {
const theme = themeFromSourceColor(argbFromHex(colorHex));
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(theme, { target: document.body, dark: systemDark });
}, [colorHex]);
return (
<div className="min-h-screen my-12 mx-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-8 text-on-background">CVSA Color Palette Generator</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:[grid-template-columns:2fr_3fr] xl:grid-cols-3 gap-8">
{/* Left Column - Color Picker */}
<div className="xl:col-span-1 bg-white dark:bg-zinc-800 rounded-lg shadow-sm p-6">
<h2 className="text-xl font-semibold text-on-background mb-4">Color Selection</h2>
<div className="space-y-6">
<div>
<label className="block font-bold text-on-background mb-2">OKLCH Color Picker</label>
<div className="mx-3">
<Picker
className="m-3"
i18n={i18nProvider}
useP3={useP3}
selectedColor={selectedColor}
onColorChange={setSelectedColor}
/>
<div className="flex justify-between mt-10">
<span className="font-medium mr-2">Show P3</span>
<Switch checked={useP3} onChange={setUseP3} />
</div>
</div>
</div>
<div>
<label className="block font-bold text-on-background mb-2">
Extract Colors from Image
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
<p className="text-on-surface-variant text-sm">
Image color extraction feature coming soon...
</p>
</div>
</div>
</div>
</div>
<div className="xl:col-span-2">
<div className="bg-white dark:bg-zinc-800 rounded-lg shadow-sm p-6">
<h2 className="text-xl font-semibold mb-6">Color Palette</h2>
</div>
</div>
</div>
</div>
</div>
);
}
export default App;