update: the palette & components
This commit is contained in:
parent
57deaed2e1
commit
adbee9647e
13
bun.lock
13
bun.lock
@ -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=="],
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"packages/elysia",
|
||||
"packages/temp_frontend",
|
||||
"packages/solid",
|
||||
"packages/plaette"
|
||||
"packages/palette"
|
||||
],
|
||||
"dependencies": {
|
||||
"arg": "^5.0.2",
|
||||
|
||||
@ -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"
|
||||
126
packages/palette/src/App.tsx
Normal file
126
packages/palette/src/App.tsx
Normal 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;
|
||||
66
packages/palette/src/ThemeContext.tsx
Normal file
66
packages/palette/src/ThemeContext.tsx
Normal 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;
|
||||
};
|
||||
35
packages/palette/src/colorTokens.ts
Normal file
35
packages/palette/src/colorTokens.ts
Normal 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)
|
||||
}
|
||||
};
|
||||
};
|
||||
30
packages/palette/src/components/Check.tsx
Normal file
30
packages/palette/src/components/Check.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
85
packages/palette/src/components/ColorBlock.tsx
Normal file
85
packages/palette/src/components/ColorBlock.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
75
packages/palette/src/components/Components.tsx
Normal file
75
packages/palette/src/components/Components.tsx
Normal 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"]) }}>
|
||||
《尘海绘仙缘》是洛凛于 2024 年 12 月 15 日投稿至哔哩哔哩的 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 };
|
||||
17
packages/palette/src/components/Palette.tsx
Normal file
17
packages/palette/src/components/Palette.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
@ -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">
|
||||
@ -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>
|
||||
);
|
||||
26
packages/palette/src/utils.ts
Normal file
26
packages/palette/src/utils.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user