From e081a4bc1510a94b6a282095f6a0004e8a51feba Mon Sep 17 00:00:00 2001
From: Henry <48483883+hfellerhoff@users.noreply.github.com>
Date: Sat, 31 Jan 2026 12:16:17 -0500
Subject: [PATCH] better color editing
---
.../color-theme/ColorThemePopoverButton.tsx | 223 +++++++++++-------
1 file changed, 144 insertions(+), 79 deletions(-)
diff --git a/src/components/color-theme/ColorThemePopoverButton.tsx b/src/components/color-theme/ColorThemePopoverButton.tsx
index 8946d349..5dc0b96c 100644
--- a/src/components/color-theme/ColorThemePopoverButton.tsx
+++ b/src/components/color-theme/ColorThemePopoverButton.tsx
@@ -4,12 +4,13 @@ import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import { LaptopIcon, MoonIcon, PaletteIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { ResponsiveButton } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { ResponsiveDropdown } from "../ui/responsive-dropdown";
import { Slider } from "../ui/slider";
import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
+import { Input } from "../ui/input";
import { EnableCustomVideoPlayerToggle } from "./EnableCustomVideoPlayerToggle";
import { ShowShortcutsToggle } from "./ShowShortcutsToggle";
import { ShowArticleStyleToggle } from "./ShowArticleStyleToggle";
@@ -17,6 +18,10 @@ import type React from "react";
import { authClient } from "~/lib/auth-client";
import { orpc } from "~/lib/orpc";
+function clamp(value: number, min: number, max: number) {
+ return Math.min(Math.max(value, min), max);
+}
+
function getCssVariable(name: string) {
const value = window
.getComputedStyle(document.documentElement)
@@ -31,21 +36,6 @@ function getCssVariable(name: string) {
return numberValue;
}
-function FormSection({
- label,
- children,
-}: {
- label: string;
- children: React.ReactNode;
-}) {
- return (
-
-
{label}
- {children}
-
- );
-}
-
const themes = ["light", "dark"] as const;
type Theme = (typeof themes)[number];
function getThemeOrDefault(theme: string | undefined): Theme {
@@ -55,6 +45,50 @@ function getThemeOrDefault(theme: string | undefined): Theme {
return themes[0];
}
+function useDebouncedCssValue(options: {
+ cssVariable: string;
+ initialValue: number;
+ clamp: (value: number) => number;
+ format: (value: number) => string;
+ onCommit: (value: number) => void;
+}) {
+ const [committed, setCommitted] = useState(options.initialValue);
+ const [local, setLocal] = useState(null);
+ const flushTimeoutRef = useRef | null>(null);
+
+ const onChange = (value: number) => {
+ const clamped = options.clamp(value);
+ setLocal(clamped);
+ if (flushTimeoutRef.current) clearTimeout(flushTimeoutRef.current);
+ flushTimeoutRef.current = setTimeout(() => {
+ document.documentElement.style.setProperty(
+ options.cssVariable,
+ options.format(clamped),
+ );
+ }, 50);
+ };
+
+ const commit = (value: number) => {
+ if (flushTimeoutRef.current) clearTimeout(flushTimeoutRef.current);
+ const clamped = options.clamp(value);
+ setCommitted(clamped);
+ setLocal(null);
+ document.documentElement.style.setProperty(
+ options.cssVariable,
+ options.format(clamped),
+ );
+ options.onCommit(clamped);
+ };
+
+ return {
+ value: local ?? committed,
+ committed,
+ setCommitted,
+ onChange,
+ onCommit: commit,
+ };
+}
+
function EditColorsForm() {
const { data } = authClient.useSession();
@@ -64,91 +98,122 @@ function EditColorsForm() {
const { resolvedTheme } = useTheme();
- const [hue, setHue] = useState(0);
- const [saturation, setSaturation] = useState(0);
- const [lightness, setLightness] = useState(0);
-
- useEffect(() => {
- setTimeout(() => {
- setHue(getCssVariable(`--${resolvedTheme}-hue`));
- setSaturation(getCssVariable(`--${resolvedTheme}-sat`));
- setLightness(getCssVariable(`--${resolvedTheme}-lgt`));
- }, 0);
- }, [resolvedTheme]);
-
- const updateCssVariable =
- (
- name: string,
- setter: React.Dispatch>,
- kind: "number" | "percentage",
- ) =>
- (values: number[]) => {
- const value = values[0];
- if (value === undefined) return;
-
- setter(value);
-
- const formattedValue = kind === "number" ? value.toString() : `${value}%`;
- document.documentElement.style.setProperty(name, formattedValue);
- };
+ const brightnessMin = resolvedTheme === "dark" ? 0 : 70;
+ const brightnessMax = resolvedTheme === "dark" ? 30 : 100;
- const saveValuesToDatabase = () => {
+ const saveValuesToDatabase = (hsl: [number, number, number]) => {
if (!data?.session.id) return;
saveThemeHSLToDatabase({
theme: getThemeOrDefault(resolvedTheme),
- hsl: [hue, saturation, lightness],
+ hsl,
});
};
- const brightnessMin = resolvedTheme === "dark" ? 0 : 70;
- const brightnessMax = resolvedTheme === "dark" ? 30 : 100;
+ const hue = useDebouncedCssValue({
+ cssVariable: `--${resolvedTheme}-hue`,
+ initialValue: 0,
+ clamp: (v) => clamp(v, 0, 360),
+ format: (v) => v.toString(),
+ onCommit: (v) =>
+ saveValuesToDatabase([v, saturation.committed, lightness.committed]),
+ });
+
+ const saturation = useDebouncedCssValue({
+ cssVariable: `--${resolvedTheme}-sat`,
+ initialValue: 0,
+ clamp: (v) => clamp(v, 0, 100),
+ format: (v) => `${v}%`,
+ onCommit: (v) =>
+ saveValuesToDatabase([hue.committed, v, lightness.committed]),
+ });
+
+ const lightness = useDebouncedCssValue({
+ cssVariable: `--${resolvedTheme}-lgt`,
+ initialValue: 0,
+ clamp: (v) => clamp(v, brightnessMin, brightnessMax),
+ format: (v) => `${v}%`,
+ onCommit: (v) =>
+ saveValuesToDatabase([hue.committed, saturation.committed, v]),
+ });
+
+ useEffect(() => {
+ setTimeout(() => {
+ hue.setCommitted(getCssVariable(`--${resolvedTheme}-hue`));
+ saturation.setCommitted(getCssVariable(`--${resolvedTheme}-sat`));
+ lightness.setCommitted(getCssVariable(`--${resolvedTheme}-lgt`));
+ }, 0);
+ }, [hue, lightness, resolvedTheme, saturation]);
return (
-
-
+
+
+
Saturation
+ saturation.onChange(parseInt(e.target.value) || 0)}
+ onBlur={(e) => saturation.onCommit(parseInt(e.target.value) || 0)}
+ />
+
+
+ saturation.onChange(values[0] ?? saturation.committed)
+ }
+ onValueCommit={(values) =>
+ saturation.onCommit(values[0] ?? saturation.committed)
+ }
/>
-
-
+
+
);
}