1
0

fix: some bugs, improve experience

This commit is contained in:
alikia2x (寒寒) 2025-10-04 03:49:35 +08:00
parent e0c15a9217
commit d23c4d0986
10 changed files with 138 additions and 31 deletions

View File

@ -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=="],

View File

@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.vercel

View File

@ -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",

View File

@ -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} />

View File

@ -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

View File

@ -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

View File

@ -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} />
))}

View File

@ -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}
/>
);
};
};

View File

@ -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>
);
};
};

View File

@ -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>
);