Brand color without correct hex value compatibility (#1737)
* enabled fallback support and og-brand-color fix * --added react-colorful package * added colorpicker component and added it to settings/profile * lint fix * typo fix in server/viewer * clean-up * improved colorpicker component * added swatch component and integrated with brand color picker * improved ux for color picker swatch * added colorname integration Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>pull/1715/head
parent
447def5849
commit
522875da74
|
@ -1,5 +1,158 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const brandColor = "#292929";
|
||||
const brandTextColor = "#ffffff";
|
||||
|
||||
export function colorNameToHex(color: string) {
|
||||
const colors = {
|
||||
aliceblue: "#f0f8ff",
|
||||
antiquewhite: "#faebd7",
|
||||
aqua: "#00ffff",
|
||||
aquamarine: "#7fffd4",
|
||||
azure: "#f0ffff",
|
||||
beige: "#f5f5dc",
|
||||
bisque: "#ffe4c4",
|
||||
black: "#000000",
|
||||
blanchedalmond: "#ffebcd",
|
||||
blue: "#0000ff",
|
||||
blueviolet: "#8a2be2",
|
||||
brown: "#a52a2a",
|
||||
burlywood: "#deb887",
|
||||
cadetblue: "#5f9ea0",
|
||||
chartreuse: "#7fff00",
|
||||
chocolate: "#d2691e",
|
||||
coral: "#ff7f50",
|
||||
cornflowerblue: "#6495ed",
|
||||
cornsilk: "#fff8dc",
|
||||
crimson: "#dc143c",
|
||||
cyan: "#00ffff",
|
||||
darkblue: "#00008b",
|
||||
darkcyan: "#008b8b",
|
||||
darkgoldenrod: "#b8860b",
|
||||
darkgray: "#a9a9a9",
|
||||
darkgreen: "#006400",
|
||||
darkkhaki: "#bdb76b",
|
||||
darkmagenta: "#8b008b",
|
||||
darkolivegreen: "#556b2f",
|
||||
darkorange: "#ff8c00",
|
||||
darkorchid: "#9932cc",
|
||||
darkred: "#8b0000",
|
||||
darksalmon: "#e9967a",
|
||||
darkseagreen: "#8fbc8f",
|
||||
darkslateblue: "#483d8b",
|
||||
darkslategray: "#2f4f4f",
|
||||
darkturquoise: "#00ced1",
|
||||
darkviolet: "#9400d3",
|
||||
deeppink: "#ff1493",
|
||||
deepskyblue: "#00bfff",
|
||||
dimgray: "#696969",
|
||||
dodgerblue: "#1e90ff",
|
||||
firebrick: "#b22222",
|
||||
floralwhite: "#fffaf0",
|
||||
forestgreen: "#228b22",
|
||||
fuchsia: "#ff00ff",
|
||||
gainsboro: "#dcdcdc",
|
||||
ghostwhite: "#f8f8ff",
|
||||
gold: "#ffd700",
|
||||
goldenrod: "#daa520",
|
||||
gray: "#808080",
|
||||
green: "#008000",
|
||||
greenyellow: "#adff2f",
|
||||
honeydew: "#f0fff0",
|
||||
hotpink: "#ff69b4",
|
||||
"indianred ": "#cd5c5c",
|
||||
indigo: "#4b0082",
|
||||
ivory: "#fffff0",
|
||||
khaki: "#f0e68c",
|
||||
lavender: "#e6e6fa",
|
||||
lavenderblush: "#fff0f5",
|
||||
lawngreen: "#7cfc00",
|
||||
lemonchiffon: "#fffacd",
|
||||
lightblue: "#add8e6",
|
||||
lightcoral: "#f08080",
|
||||
lightcyan: "#e0ffff",
|
||||
lightgoldenrodyellow: "#fafad2",
|
||||
lightgrey: "#d3d3d3",
|
||||
lightgreen: "#90ee90",
|
||||
lightpink: "#ffb6c1",
|
||||
lightsalmon: "#ffa07a",
|
||||
lightseagreen: "#20b2aa",
|
||||
lightskyblue: "#87cefa",
|
||||
lightslategray: "#778899",
|
||||
lightsteelblue: "#b0c4de",
|
||||
lightyellow: "#ffffe0",
|
||||
lime: "#00ff00",
|
||||
limegreen: "#32cd32",
|
||||
linen: "#faf0e6",
|
||||
magenta: "#ff00ff",
|
||||
maroon: "#800000",
|
||||
mediumaquamarine: "#66cdaa",
|
||||
mediumblue: "#0000cd",
|
||||
mediumorchid: "#ba55d3",
|
||||
mediumpurple: "#9370d8",
|
||||
mediumseagreen: "#3cb371",
|
||||
mediumslateblue: "#7b68ee",
|
||||
mediumspringgreen: "#00fa9a",
|
||||
mediumturquoise: "#48d1cc",
|
||||
mediumvioletred: "#c71585",
|
||||
midnightblue: "#191970",
|
||||
mintcream: "#f5fffa",
|
||||
mistyrose: "#ffe4e1",
|
||||
moccasin: "#ffe4b5",
|
||||
navajowhite: "#ffdead",
|
||||
navy: "#000080",
|
||||
oldlace: "#fdf5e6",
|
||||
olive: "#808000",
|
||||
olivedrab: "#6b8e23",
|
||||
orange: "#ffa500",
|
||||
orangered: "#ff4500",
|
||||
orchid: "#da70d6",
|
||||
palegoldenrod: "#eee8aa",
|
||||
palegreen: "#98fb98",
|
||||
paleturquoise: "#afeeee",
|
||||
palevioletred: "#d87093",
|
||||
papayawhip: "#ffefd5",
|
||||
peachpuff: "#ffdab9",
|
||||
peru: "#cd853f",
|
||||
pink: "#ffc0cb",
|
||||
plum: "#dda0dd",
|
||||
powderblue: "#b0e0e6",
|
||||
purple: "#800080",
|
||||
rebeccapurple: "#663399",
|
||||
red: "#ff0000",
|
||||
rosybrown: "#bc8f8f",
|
||||
royalblue: "#4169e1",
|
||||
saddlebrown: "#8b4513",
|
||||
salmon: "#fa8072",
|
||||
sandybrown: "#f4a460",
|
||||
seagreen: "#2e8b57",
|
||||
seashell: "#fff5ee",
|
||||
sienna: "#a0522d",
|
||||
silver: "#c0c0c0",
|
||||
skyblue: "#87ceeb",
|
||||
slateblue: "#6a5acd",
|
||||
slategray: "#708090",
|
||||
snow: "#fffafa",
|
||||
springgreen: "#00ff7f",
|
||||
steelblue: "#4682b4",
|
||||
tan: "#d2b48c",
|
||||
teal: "#008080",
|
||||
thistle: "#d8bfd8",
|
||||
tomato: "#ff6347",
|
||||
turquoise: "#40e0d0",
|
||||
violet: "#ee82ee",
|
||||
wheat: "#f5deb3",
|
||||
white: "#ffffff",
|
||||
whitesmoke: "#f5f5f5",
|
||||
yellow: "#ffff00",
|
||||
yellowgreen: "#9acd32",
|
||||
};
|
||||
|
||||
return colors[color.toLowerCase() as keyof typeof colors] !== undefined
|
||||
? colors[color.toLowerCase() as keyof typeof colors]
|
||||
: false;
|
||||
}
|
||||
|
||||
function computeContrastRatio(a: number[], b: number[]) {
|
||||
const lum1 = computeLuminance(a[0], a[1], a[2]);
|
||||
const lum2 = computeLuminance(b[0], b[1], b[2]);
|
||||
|
@ -22,14 +175,30 @@ function hexToRGB(hex: string) {
|
|||
}
|
||||
|
||||
function getContrastingTextColor(bgColor: string | null): string {
|
||||
bgColor = bgColor == "" || bgColor == null ? "#292929" : bgColor;
|
||||
bgColor = bgColor == "" || bgColor == null ? brandColor : bgColor;
|
||||
const rgb = hexToRGB(bgColor);
|
||||
const whiteContrastRatio = computeContrastRatio(rgb, [255, 255, 255]);
|
||||
const blackContrastRatio = computeContrastRatio(rgb, [41, 41, 41]); //#292929
|
||||
return whiteContrastRatio > blackContrastRatio ? "#ffffff" : "#292929";
|
||||
return whiteContrastRatio > blackContrastRatio ? brandTextColor : brandColor;
|
||||
}
|
||||
|
||||
const BrandColor = ({ val = "#292929" }: { val: string | undefined | null }) => {
|
||||
export function isValidHexCode(val: string | null) {
|
||||
if (val) {
|
||||
val = val.indexOf("#") === 0 ? val : "#" + val;
|
||||
const regex = new RegExp("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$");
|
||||
return regex.test(val);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function fallBackHex(val: string | null): string {
|
||||
if (val) if (colorNameToHex(val)) return colorNameToHex(val) as string;
|
||||
return brandColor;
|
||||
}
|
||||
|
||||
const BrandColor = ({ val = brandColor }: { val: string | undefined | null }) => {
|
||||
// ensure acceptable hex-code
|
||||
val = isValidHexCode(val) ? (val?.indexOf("#") === 0 ? val : "#" + val) : fallBackHex(val);
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("--brand-color", val);
|
||||
document.documentElement.style.setProperty("--brand-text-color", getContrastingTextColor(val));
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import classNames from "@lib/classNames";
|
||||
|
||||
export type SwatchProps = {
|
||||
size?: "base" | "sm" | "lg";
|
||||
backgroundColor: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const Swatch = (props: SwatchProps) => {
|
||||
const { size, backgroundColor, onClick } = props;
|
||||
return (
|
||||
<div className="p-1 border-2 border-gray-200 shadow-sm">
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{ backgroundColor }}
|
||||
className={classNames(
|
||||
"cursor-pointer",
|
||||
size === "sm" && "w-6 h-6 rounded-sm",
|
||||
size === "base" && "w-16 h-16 rounded-sm",
|
||||
size === "lg" && "w-24 h-24 rounded-sm"
|
||||
)}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Swatch;
|
|
@ -0,0 +1,100 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
|
||||
import { isValidHexCode, fallBackHex } from "@components/CustomBranding";
|
||||
import Swatch from "@components/Swatch";
|
||||
|
||||
type Handler = (event: MouseEvent | Event) => void;
|
||||
function useEventListener<
|
||||
KW extends keyof WindowEventMap,
|
||||
KH extends keyof HTMLElementEventMap,
|
||||
T extends HTMLElement | void = void
|
||||
>(
|
||||
eventName: KW | KH,
|
||||
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
|
||||
element?: React.RefObject<T>
|
||||
) {
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef<typeof handler>();
|
||||
useEffect(() => {
|
||||
// Define the listening target
|
||||
const targetElement: T | Window = element?.current || window;
|
||||
if (!(targetElement && targetElement.addEventListener)) {
|
||||
return;
|
||||
}
|
||||
// Update saved handler if necessary
|
||||
if (savedHandler.current !== handler) {
|
||||
savedHandler.current = handler;
|
||||
}
|
||||
// Create event listener that calls handler function stored in ref
|
||||
const eventListener: typeof handler = (event) => {
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
if (!!savedHandler?.current) {
|
||||
savedHandler.current(event);
|
||||
}
|
||||
};
|
||||
targetElement.addEventListener(eventName, eventListener);
|
||||
// Remove event listener on cleanup
|
||||
return () => {
|
||||
targetElement.removeEventListener(eventName, eventListener);
|
||||
};
|
||||
}, [eventName, element, handler]);
|
||||
}
|
||||
|
||||
function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
||||
ref: React.RefObject<T>,
|
||||
handler: Handler,
|
||||
mouseEvent: "mousedown" | "mouseup" = "mousedown"
|
||||
): void {
|
||||
useEventListener(mouseEvent, (event) => {
|
||||
const el = ref?.current;
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (!el || el.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
handler(event);
|
||||
});
|
||||
}
|
||||
export type ColorPickerProps = {
|
||||
defaultValue: string;
|
||||
onChange: (text: string) => void;
|
||||
};
|
||||
|
||||
const ColorPicker = (props: ColorPickerProps) => {
|
||||
const init = !isValidHexCode(props.defaultValue) ? fallBackHex(props.defaultValue) : props.defaultValue;
|
||||
const [color, setColor] = useState(init);
|
||||
const [isOpen, toggle] = useState(false);
|
||||
const popover = useRef() as React.MutableRefObject<HTMLInputElement>;
|
||||
const close = useCallback(() => toggle(false), []);
|
||||
useOnClickOutside(popover, close);
|
||||
return (
|
||||
<div className="relative flex items-center justify-center mt-1">
|
||||
<Swatch size="sm" backgroundColor={color} onClick={() => toggle(!isOpen)} />
|
||||
|
||||
{isOpen && (
|
||||
<div className="popover" ref={popover}>
|
||||
<HexColorPicker
|
||||
className="!w-32 !h-32 !absolute !top-10 !left-0 !z-10"
|
||||
color={color}
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
props.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<HexColorInput
|
||||
className="block w-full px-3 py-2 ml-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-800 focus:border-neutral-800 sm:text-sm"
|
||||
color={color}
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
props.onChange(val);
|
||||
}}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorPicker;
|
|
@ -85,6 +85,7 @@
|
|||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.0",
|
||||
"react": "^17.0.2",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-date-picker": "^8.3.6",
|
||||
"react-digit-input": "^2.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -169,4 +170,4 @@
|
|||
"prisma": {
|
||||
"seed": "ts-node ./prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import { Alert } from "@components/ui/Alert";
|
|||
import Avatar from "@components/ui/Avatar";
|
||||
import Badge from "@components/ui/Badge";
|
||||
import Button from "@components/ui/Button";
|
||||
import ColorPicker from "@components/ui/colorpicker";
|
||||
|
||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
|
@ -151,7 +152,6 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
const emailRef = useRef<HTMLInputElement>(null!);
|
||||
const descriptionRef = useRef<HTMLTextAreaElement>(null!);
|
||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||
const brandColorRef = useRef<HTMLInputElement>(null!);
|
||||
const hideBrandingRef = useRef<HTMLInputElement>(null!);
|
||||
const [selectedTheme, setSelectedTheme] = useState<typeof themeOptions[number] | undefined>();
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(props.user.timeZone);
|
||||
|
@ -167,6 +167,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
const [imageSrc, setImageSrc] = useState<string>(props.user.avatar || "");
|
||||
const [hasErrors, setHasErrors] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [brandColor, setBrandColor] = useState(props.user.brandColor);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.user.theme) return;
|
||||
|
@ -184,7 +185,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
const enteredEmail = emailRef.current.value;
|
||||
const enteredDescription = descriptionRef.current.value;
|
||||
const enteredAvatar = avatarRef.current.value;
|
||||
const enteredBrandColor = brandColorRef.current.value;
|
||||
const enteredBrandColor = brandColor;
|
||||
const enteredTimeZone = typeof selectedTimeZone === "string" ? selectedTimeZone : selectedTimeZone.value;
|
||||
const enteredWeekStartDay = selectedWeekStartDay.value;
|
||||
const enteredHideBranding = hideBrandingRef.current.checked;
|
||||
|
@ -402,17 +403,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
<label htmlFor="brandColor" className="block text-sm font-medium text-gray-700">
|
||||
{t("brand_color")}
|
||||
</label>
|
||||
<div className="flex mt-1">
|
||||
<input
|
||||
ref={brandColorRef}
|
||||
type="text"
|
||||
name="brandColor"
|
||||
id="brandColor"
|
||||
placeholder="#hex-code"
|
||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-800 focus:border-neutral-800 sm:text-sm"
|
||||
defaultValue={props.user.brandColor}
|
||||
/>
|
||||
</div>
|
||||
<ColorPicker defaultValue={props.user.brandColor} onChange={setBrandColor} />
|
||||
<hr className="mt-6" />
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -601,6 +601,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
input: z.object({
|
||||
username: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
avatar: z.string().optional(),
|
||||
timeZone: z.string().optional(),
|
||||
|
|
|
@ -9603,6 +9603,11 @@ react-calendar@^3.3.1:
|
|||
merge-class-names "^1.1.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-colorful@^5.5.1:
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
|
||||
integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==
|
||||
|
||||
react-date-picker@^8.3.3:
|
||||
version "8.3.5"
|
||||
resolved "https://registry.yarnpkg.com/react-date-picker/-/react-date-picker-8.3.5.tgz#3972db3fc1f37cfd6b09c09bf4988817b4d2be72"
|
||||
|
|
Loading…
Reference in New Issue