fix: some bugs, improve experience
This commit is contained in:
parent
e0c15a9217
commit
d23c4d0986
10
bun.lock
10
bun.lock
@ -109,13 +109,15 @@
|
||||
"@types/pg": "^8.11.11",
|
||||
},
|
||||
},
|
||||
"packages/plaette": {
|
||||
"packages/palette": {
|
||||
"name": "plaette",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@primereact/types": "^11.0.0-alpha.1",
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"@types/culori": "^4.0.1",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"culori": "^4.0.2",
|
||||
"jotai": "^2.15.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
@ -812,6 +814,8 @@
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
|
||||
"@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
@ -928,6 +932,8 @@
|
||||
|
||||
"@vercel/nft": ["@vercel/nft@0.30.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-2mgJZv4AYBFkD/nJ4QmiX5Ymxi+AisPLPcS/KPXVqniyQNqKXX+wjieAbDXQP3HcogfEbpHoRMs49Cd4pfkk8g=="],
|
||||
|
||||
"@vercel/speed-insights": ["@vercel/speed-insights@1.2.0", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw=="],
|
||||
|
||||
"@vinxi/listhen": ["@vinxi/listhen@1.5.6", "", { "dependencies": { "@parcel/watcher": "^2.3.0", "@parcel/watcher-wasm": "2.3.0", "citty": "^0.1.5", "clipboardy": "^4.0.0", "consola": "^3.2.3", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.10.0", "http-shutdown": "^1.2.2", "jiti": "^1.21.0", "mlly": "^1.5.0", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.3.2", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw=="],
|
||||
|
||||
"@vinxi/plugin-directives": ["@vinxi/plugin-directives@0.5.1", "", { "dependencies": { "@babel/parser": "^7.23.5", "acorn": "^8.10.0", "acorn-jsx": "^5.3.2", "acorn-loose": "^8.3.0", "acorn-typescript": "^1.4.3", "astring": "^1.8.6", "magicast": "^0.2.10", "recast": "^0.23.4", "tslib": "^2.6.2" }, "peerDependencies": { "vinxi": "^0.5.5" } }, "sha512-pH/KIVBvBt7z7cXrUH/9uaqcdxjegFC7+zvkZkdOyWzs+kQD5KPf3cl8kC+5ayzXHT+OMlhGhyitytqN3cGmHg=="],
|
||||
@ -2048,7 +2054,7 @@
|
||||
|
||||
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||
|
||||
"plaette": ["plaette@workspace:packages/plaette"],
|
||||
"plaette": ["plaette@workspace:packages/palette"],
|
||||
|
||||
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
|
||||
|
||||
|
||||
1
packages/palette/.gitignore
vendored
1
packages/palette/.gitignore
vendored
@ -22,3 +22,4 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.vercel
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@primereact/types": "^11.0.0-alpha.1",
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"@types/culori": "^4.0.1",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"culori": "^4.0.2",
|
||||
"jotai": "^2.15.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
|
||||
@ -58,13 +58,13 @@ function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen my-12 mx-6">
|
||||
<div className="min-h-screen my-12 sm:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8 text-on-background">CVSA Color Palette Generator</h1>
|
||||
<h1 className="text-3xl font-bold mb-8 ml-3 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">
|
||||
<div className="xl:col-span-1 sm:bg-white sm:dark:bg-zinc-800 rounded-lg shadow-sm p-3 sm:p-6">
|
||||
<h2 className="text-xl font-semibold text-on-background mb-4">Color Selection</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
@ -100,7 +100,7 @@ function App() {
|
||||
|
||||
{/* 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="sm:bg-white sm:dark:bg-zinc-800 rounded-lg shadow-sm p-3 sm:p-6">
|
||||
<div className="flex h-8 mb-4 justify-between items-center">
|
||||
<h2 className="text-xl font-semibold">Color Palette</h2>
|
||||
<Icon />
|
||||
@ -108,7 +108,7 @@ function App() {
|
||||
|
||||
<ColorPalette baseColor={selectedColor} />
|
||||
</div>
|
||||
<div className="bg-white dark:bg-zinc-800 rounded-lg shadow-sm p-6">
|
||||
<div className="sm:bg-white sm:dark:bg-zinc-800 rounded-lg shadow-sm p-3 sm:p-6">
|
||||
<h2 className="text-xl font-semibold mb-6">Components</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SearchBar baseColor={selectedColor} />
|
||||
|
||||
@ -44,7 +44,7 @@ export const ColorBlock = ({ baseColor, text, l, c, h }: ColorBlockProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-30 h-36 flex flex-col items-center">
|
||||
<div className="w-26 md: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
|
||||
|
||||
@ -31,7 +31,7 @@ const Paragraph = ({ baseColor }: { baseColor: Oklch }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-18 flex items-center justify-center rounded-md"
|
||||
className="w-full h-18 flex items-center justify-center rounded-md px-4"
|
||||
style={{ backgroundColor: formatHex(tokens.background) }}
|
||||
>
|
||||
<p style={{ color: formatHex(tokens["body-text"]) }}>
|
||||
@ -47,23 +47,24 @@ const Buttons = ({ baseColor }: { baseColor: Oklch }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-18 flex items-center justify-center rounded-md gap-4 px-10"
|
||||
className="w-full py-4 grid [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))] place-items-center
|
||||
items-center justify-between 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"
|
||||
className="w-24 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"
|
||||
className="w-24 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"
|
||||
className="w-24 cursor-pointer font-medium py-2 px-4 rounded-lg"
|
||||
style={{ backgroundColor: formatHex(tokens.error), color: formatHex(tokens["on-error"]) }}
|
||||
>
|
||||
Delete
|
||||
|
||||
@ -8,7 +8,8 @@ export function ColorPalette({ baseColor }: { baseColor: Oklch }) {
|
||||
const tokens = buildColorTokens(baseColor)[theme];
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 [grid-template-columns:repeat(auto-fill,120px)] justify-between">
|
||||
<div className="mx-6 grid gap-1 md:gap-4 [grid-template-columns:repeat(auto-fill,104px)]
|
||||
md:[grid-template-columns:repeat(auto-fill,120px)] justify-between">
|
||||
{Object.entries(tokens).map(([name, color]) => (
|
||||
<ColorBlock key={name} baseColor={color} text={name} />
|
||||
))}
|
||||
|
||||
@ -5,33 +5,40 @@ export const Handle = ({
|
||||
pos,
|
||||
color,
|
||||
onChange,
|
||||
maxValue
|
||||
maxValue,
|
||||
onTouchMove,
|
||||
onTouchStart
|
||||
}: {
|
||||
pos: number;
|
||||
color: Omit<Oklch, "mode">;
|
||||
onChange: (value: number) => void;
|
||||
maxValue: number;
|
||||
onTouchMove?: (e: React.TouchEvent) => void;
|
||||
onTouchStart?: (e: React.TouchEvent) => void;
|
||||
}) => {
|
||||
const handleRef = useRef<HTMLDivElement>(null);
|
||||
const isDragging = useRef(false);
|
||||
const isTouching = useRef(false);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const getValueFromPosition = (clientX: number) => {
|
||||
const sliderRect = handleRef.current?.closest(".relative")?.getBoundingClientRect();
|
||||
if (!sliderRect) return 0;
|
||||
|
||||
const x = clientX - sliderRect.left;
|
||||
const percentage = Math.max(0, Math.min(1, x / sliderRect.width));
|
||||
return (percentage * maxValue);
|
||||
};
|
||||
|
||||
const handleMouseDown = () => {
|
||||
isDragging.current = true;
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging.current || !handleRef.current) return;
|
||||
|
||||
const sliderRect = handleRef.current.closest(".relative")?.getBoundingClientRect();
|
||||
if (!sliderRect) return;
|
||||
|
||||
const x = e.clientX - sliderRect.left;
|
||||
const percentage = Math.max(0, Math.min(100, (x / sliderRect.width) * 100));
|
||||
const value = (percentage / 100) * maxValue;
|
||||
if (!isDragging.current) return;
|
||||
|
||||
const value = getValueFromPosition(e.clientX);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
@ -41,17 +48,50 @@ export const Handle = ({
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
isTouching.current = true;
|
||||
|
||||
const touch = e.touches[0];
|
||||
if (touch) {
|
||||
const value = getValueFromPosition(touch.clientX);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
onTouchStart?.(e);
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
if (!isTouching.current) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
if (touch) {
|
||||
const value = getValueFromPosition(touch.clientX);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
onTouchMove?.(e);
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
isTouching.current = false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={handleRef}
|
||||
className="absolute z-5 top-full left-0 size-7 border-white border-width-3
|
||||
shadow-[0px_0px_7px_2px_rgba(0,0,0,0.35)] cursor-grab active:cursor-grabbing"
|
||||
shadow-[0px_0px_7px_2px_rgba(0,0,0,0.35)] cursor-grab active:cursor-grabbing
|
||||
touch-none select-none"
|
||||
style={{
|
||||
left: `${pos}%`,
|
||||
backgroundColor: `oklch(${color.l} ${color.c} ${color.h})`,
|
||||
transform: "translateY(-50%) translateX(-50%) rotate(45deg)"
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchEnd}
|
||||
/>
|
||||
);
|
||||
};
|
||||
};
|
||||
@ -14,15 +14,45 @@ interface SliderProps {
|
||||
}
|
||||
|
||||
export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: SliderProps) => {
|
||||
const [value, setValue] = React.useState(color[channel]!.toFixed(precision[channel]));
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const canvasRef = useRef<null | HTMLCanvasElement>(null);
|
||||
useOklchCanvas({ channel: channel, max: maxValue[channel], canvasRef: canvasRef, color, useP3 });
|
||||
|
||||
const getSliderPosition = (value: number, max: number) => {
|
||||
return (value / max) * 100;
|
||||
};
|
||||
|
||||
const getValueFromPosition = (clientX: number) => {
|
||||
if (!containerRef.current) return 0;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
const percentage = Math.max(0, Math.min(1, x / rect.width));
|
||||
return round(percentage * maxValue[channel], precision[channel]);
|
||||
};
|
||||
|
||||
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value);
|
||||
onChange(value);
|
||||
if (value > maxValue[channel]) onChange(maxValue[channel]);
|
||||
else if (value < 0 || isNaN(value)) onChange(0);
|
||||
else onChange(value);
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value);
|
||||
if (value > maxValue[channel]) {
|
||||
onChange(maxValue[channel]);
|
||||
setValue(maxValue[channel].toFixed(precision[channel]));
|
||||
} else if (value < 0 || isNaN(value)) {
|
||||
onChange(0);
|
||||
setValue("0");
|
||||
} else {
|
||||
onChange(value);
|
||||
setValue(value.toFixed(precision[channel]));
|
||||
}
|
||||
};
|
||||
|
||||
const buttonHanlder = (type: "increase" | "decrease") => {
|
||||
@ -36,6 +66,21 @@ export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: Slider
|
||||
onChange(round(value, precision[channel]));
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
const touch = e.touches[0];
|
||||
if (touch) {
|
||||
const newValue = getValueFromPosition(touch.clientX);
|
||||
handleOnChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
const touch = e.touches[0];
|
||||
if (touch) {
|
||||
const newValue = getValueFromPosition(touch.clientX);
|
||||
handleOnChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
@ -46,8 +91,9 @@ export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: Slider
|
||||
type="text"
|
||||
className="w-28 h-10 text-right text-[15px] font-mono bg-zinc-200 dark:bg-zinc-700 rounded-lg pl-3 pr-6 focus:outline-none
|
||||
focus:ring-2 focus:ring-black dark:focus:ring-white font-stretch-semi-expanded"
|
||||
value={color[channel]}
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
step={Math.pow(10, -precision[channel])}
|
||||
aria-label={i18nProvider(channel)}
|
||||
aria-keyshortcuts={channel}
|
||||
@ -72,7 +118,12 @@ export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: Slider
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-10">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative h-10"
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchStart={handleTouchStart}
|
||||
>
|
||||
<div className="absolute z-3 inset-0 rounded-xl overflow-hidden">
|
||||
<canvas ref={canvasRef} width={400} height={40} className="w-full h-full" />
|
||||
</div>
|
||||
@ -91,8 +142,10 @@ export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: Slider
|
||||
color={color}
|
||||
onChange={handleOnChange}
|
||||
maxValue={maxValue[channel]}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchStart={handleTouchStart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@ -3,11 +3,14 @@ import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import { ThemeProvider } from "./ThemeContext.tsx";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/react"
|
||||
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
<SpeedInsights/>
|
||||
</ThemeProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user