feat: tweak color swatch, and button bgs (#9330)

* feat: tweak color swatch, and button bgs

* snapshots
This commit is contained in:
David Luzar 2025-04-02 14:36:13 +02:00 committed by GitHub
parent 7c58477382
commit 57a9e301d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 94 additions and 58 deletions

View File

@ -2,6 +2,8 @@ import oc from "open-color";
import type { Merge } from "./utility-types"; import type { Merge } from "./utility-types";
export const COLOR_OUTLINE_CONTRAST_THRESHOLD = 240;
// FIXME can't put to utils.ts rn because of circular dependency // FIXME can't put to utils.ts rn because of circular dependency
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>( const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
source: R, source: R,

View File

@ -15,7 +15,7 @@
.color-picker-container { .color-picker-container {
display: grid; display: grid;
grid-template-columns: 1fr 8px 1.625rem; grid-template-columns: 1fr 20px 1.625rem;
padding: 0.25rem 0px; padding: 0.25rem 0px;
align-items: center; align-items: center;
@ -27,14 +27,19 @@
.color-picker__top-picks { .color-picker__top-picks {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
.color-picker__button { .color-picker__button {
--radius: 6px; --radius: 4px;
--size: 1.375rem; --size: 1.375rem;
&.has-outline {
box-shadow: inset 0 0 0 1px #d9d9d9;
}
padding: 0; padding: 0;
margin: 1px; margin: 0;
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
border: 0; border: 0;
@ -46,15 +51,19 @@
font-family: inherit; font-family: inherit;
box-sizing: border-box; box-sizing: border-box;
&:hover:not(.active) { &:hover:not(.active):not(.color-picker__button--large) {
transform: scale(1.075);
}
&:hover:not(.active).color-picker__button--large {
&::after { &::after {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: -1px;
left: 0; left: -1px;
right: 0; right: -1px;
bottom: 0; bottom: -1px;
box-shadow: 0 0 0 1px var(--swatch-color); box-shadow: 0 0 0 1px var(--color-gray-30);
border-radius: var(--radius); border-radius: var(--radius);
filter: var(--theme-filter); filter: var(--theme-filter);
} }
@ -70,7 +79,7 @@
bottom: var(--offset); bottom: var(--offset);
box-shadow: 0 0 0 1px var(--color-primary-darkest); box-shadow: 0 0 0 1px var(--color-primary-darkest);
z-index: 1; // due hover state so this has preference z-index: 1; // due hover state so this has preference
border-radius: calc(var(--radius) + 1px); border-radius: var(--radius);
filter: var(--theme-filter); filter: var(--theme-filter);
} }
} }
@ -125,10 +134,11 @@
.color-picker__button__hotkey-label { .color-picker__button__hotkey-label {
position: absolute; position: absolute;
right: 4px; right: 5px;
bottom: 4px; bottom: 3px;
filter: none; filter: none;
font-size: 11px; font-size: 11px;
font-weight: 500;
} }
.color-picker { .color-picker {

View File

@ -2,7 +2,11 @@ import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx"; import clsx from "clsx";
import { useRef } from "react"; import { useRef } from "react";
import { COLOR_PALETTE, isTransparent } from "@excalidraw/common"; import {
COLOR_OUTLINE_CONTRAST_THRESHOLD,
COLOR_PALETTE,
isTransparent,
} from "@excalidraw/common";
import type { ColorTuple, ColorPaletteCustom } from "@excalidraw/common"; import type { ColorTuple, ColorPaletteCustom } from "@excalidraw/common";
@ -19,7 +23,7 @@ import { ColorInput } from "./ColorInput";
import { Picker } from "./Picker"; import { Picker } from "./Picker";
import PickerHeading from "./PickerHeading"; import PickerHeading from "./PickerHeading";
import { TopPicks } from "./TopPicks"; import { TopPicks } from "./TopPicks";
import { activeColorPickerSectionAtom } from "./colorPickerUtils"; import { activeColorPickerSectionAtom, isColorDark } from "./colorPickerUtils";
import "./ColorPicker.scss"; import "./ColorPicker.scss";
@ -190,6 +194,7 @@ const ColorPickerTrigger = ({
type="button" type="button"
className={clsx("color-picker__button active-color properties-trigger", { className={clsx("color-picker__button active-color properties-trigger", {
"is-transparent": color === "transparent" || !color, "is-transparent": color === "transparent" || !color,
"has-outline": !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
})} })}
aria-label={label} aria-label={label}
style={color ? { "--swatch-color": color } : undefined} style={color ? { "--swatch-color": color } : undefined}

View File

@ -40,7 +40,7 @@ export const CustomColorList = ({
tabIndex={-1} tabIndex={-1}
type="button" type="button"
className={clsx( className={clsx(
"color-picker__button color-picker__button--large", "color-picker__button color-picker__button--large has-outline",
{ {
active: color === c, active: color === c,
"is-transparent": c === "transparent" || !c, "is-transparent": c === "transparent" || !c,
@ -56,7 +56,7 @@ export const CustomColorList = ({
key={i} key={i}
> >
<div className="color-picker__button-outline" /> <div className="color-picker__button-outline" />
<HotkeyLabel color={c} keyLabel={i + 1} isCustomColor /> <HotkeyLabel color={c} keyLabel={i + 1} />
</button> </button>
); );
})} })}

View File

@ -1,24 +1,22 @@
import React from "react"; import React from "react";
import { getContrastYIQ } from "./colorPickerUtils"; import { isColorDark } from "./colorPickerUtils";
interface HotkeyLabelProps { interface HotkeyLabelProps {
color: string; color: string;
keyLabel: string | number; keyLabel: string | number;
isCustomColor?: boolean;
isShade?: boolean; isShade?: boolean;
} }
const HotkeyLabel = ({ const HotkeyLabel = ({
color, color,
keyLabel, keyLabel,
isCustomColor = false,
isShade = false, isShade = false,
}: HotkeyLabelProps) => { }: HotkeyLabelProps) => {
return ( return (
<div <div
className="color-picker__button__hotkey-label" className="color-picker__button__hotkey-label"
style={{ style={{
color: getContrastYIQ(color, isCustomColor), color: isColorDark(color) ? "#fff" : "#000",
}} }}
> >
{isShade && "⇧"} {isShade && "⇧"}

View File

@ -65,7 +65,7 @@ const PickerColorList = ({
tabIndex={-1} tabIndex={-1}
type="button" type="button"
className={clsx( className={clsx(
"color-picker__button color-picker__button--large", "color-picker__button color-picker__button--large has-outline",
{ {
active: colorObj?.colorName === key, active: colorObj?.colorName === key,
"is-transparent": color === "transparent" || !color, "is-transparent": color === "transparent" || !color,

View File

@ -55,7 +55,7 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
key={i} key={i}
type="button" type="button"
className={clsx( className={clsx(
"color-picker__button color-picker__button--large", "color-picker__button color-picker__button--large has-outline",
{ active: i === shade }, { active: i === shade },
)} )}
aria-label="Shade" aria-label="Shade"

View File

@ -1,11 +1,14 @@
import clsx from "clsx"; import clsx from "clsx";
import { import {
COLOR_OUTLINE_CONTRAST_THRESHOLD,
DEFAULT_CANVAS_BACKGROUND_PICKS, DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS, DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_PICKS, DEFAULT_ELEMENT_STROKE_PICKS,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { isColorDark } from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils"; import type { ColorPickerType } from "./colorPickerUtils";
interface TopPicksProps { interface TopPicksProps {
@ -51,6 +54,10 @@ export const TopPicks = ({
className={clsx("color-picker__button", { className={clsx("color-picker__button", {
active: color === activeColor, active: color === activeColor,
"is-transparent": color === "transparent" || !color, "is-transparent": color === "transparent" || !color,
"has-outline": !isColorDark(
color,
COLOR_OUTLINE_CONTRAST_THRESHOLD,
),
})} })}
style={{ "--swatch-color": color }} style={{ "--swatch-color": color }}
key={color} key={color}

View File

@ -93,19 +93,42 @@ export type ActiveColorPickerSectionAtomType =
export const activeColorPickerSectionAtom = export const activeColorPickerSectionAtom =
atom<ActiveColorPickerSectionAtomType>(null); atom<ActiveColorPickerSectionAtomType>(null);
const calculateContrast = (r: number, g: number, b: number) => { const calculateContrast = (r: number, g: number, b: number): number => {
const yiq = (r * 299 + g * 587 + b * 114) / 1000; const yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 160 ? "black" : "white"; return yiq;
}; };
// inspiration from https://stackoverflow.com/a/11868398 // YIQ algo, inspiration from https://stackoverflow.com/a/11868398
export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => { export const isColorDark = (color: string, threshold = 160): boolean => {
if (isCustomColor) { // no color ("") -> assume it default to black
const style = new Option().style; if (!color) {
style.color = bgHex; return true;
}
if (style.color) { if (color === "transparent") {
const rgb = style.color return false;
}
// a string color (white etc) or any other format -> convert to rgb by way
// of creating a DOM node and retrieving the computeStyle
if (!color.startsWith("#")) {
const node = document.createElement("div");
node.style.color = color;
if (node.style.color) {
// making invisible so document doesn't reflow (hopefully).
// display=none works too, but supposedly not in all browsers
node.style.position = "absolute";
node.style.visibility = "hidden";
node.style.width = "0";
node.style.height = "0";
// needs to be in DOM else browser won't compute the style
document.body.appendChild(node);
const computedColor = getComputedStyle(node).color;
document.body.removeChild(node);
// computed style is in rgb() format
const rgb = computedColor
.replace(/^(rgb|rgba)\(/, "") .replace(/^(rgb|rgba)\(/, "")
.replace(/\)$/, "") .replace(/\)$/, "")
.replace(/\s/g, "") .replace(/\s/g, "")
@ -114,20 +137,17 @@ export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
const g = parseInt(rgb[1]); const g = parseInt(rgb[1]);
const b = parseInt(rgb[2]); const b = parseInt(rgb[2]);
return calculateContrast(r, g, b); return calculateContrast(r, g, b) < threshold;
} }
// invalid color -> assume it default to black
return true;
} }
// TODO: ? is this wanted? const r = parseInt(color.slice(1, 3), 16);
if (bgHex === "transparent") { const g = parseInt(color.slice(3, 5), 16);
return "black"; const b = parseInt(color.slice(5, 7), 16);
}
const r = parseInt(bgHex.substring(1, 3), 16); return calculateContrast(r, g, b) < threshold;
const g = parseInt(bgHex.substring(3, 5), 16);
const b = parseInt(bgHex.substring(5, 7), 16);
return calculateContrast(r, g, b);
}; };
export type ColorPickerType = export type ColorPickerType =

View File

@ -173,7 +173,7 @@ body.excalidraw-cursor-resize * {
.buttonList { .buttonList {
flex-wrap: wrap; flex-wrap: wrap;
display: flex; display: flex;
column-gap: 0.375rem; column-gap: 0.5rem;
row-gap: 0.5rem; row-gap: 0.5rem;
label { label {
@ -386,16 +386,10 @@ body.excalidraw-cursor-resize * {
.App-menu__left { .App-menu__left {
overflow-y: auto; overflow-y: auto;
padding: 0.75rem 0.75rem 0.25rem 0.75rem; padding: 0.75rem;
width: 11.875rem; width: 12.5rem;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
.buttonList label,
.buttonList button,
.buttonList .zIndexButton {
--button-bg: transparent;
}
} }
.dropdown-select { .dropdown-select {

View File

@ -148,7 +148,7 @@
--border-radius-lg: 0.5rem; --border-radius-lg: 0.5rem;
--color-surface-high: #f1f0ff; --color-surface-high: #f1f0ff;
--color-surface-mid: #f2f2f7; --color-surface-mid: #f6f6f9;
--color-surface-low: #ececf4; --color-surface-low: #ececf4;
--color-surface-lowest: #ffffff; --color-surface-lowest: #ffffff;
--color-on-surface: #1b1b1f; --color-on-surface: #1b1b1f;
@ -252,7 +252,7 @@
--color-logo-text: #e2dfff; --color-logo-text: #e2dfff;
--color-surface-high: hsl(245, 10%, 21%); --color-surface-high: #2e2d39;
--color-surface-low: hsl(240, 8%, 15%); --color-surface-low: hsl(240, 8%, 15%);
--color-surface-mid: hsl(240 6% 10%); --color-surface-mid: hsl(240 6% 10%);
--color-surface-lowest: hsl(0, 0%, 7%); --color-surface-lowest: hsl(0, 0%, 7%);

View File

@ -572,7 +572,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
class="color-picker__top-picks" class="color-picker__top-picks"
> >
<button <button
class="color-picker__button active" class="color-picker__button active has-outline"
data-testid="color-top-pick-#ffffff" data-testid="color-top-pick-#ffffff"
style="--swatch-color: #ffffff;" style="--swatch-color: #ffffff;"
title="#ffffff" title="#ffffff"
@ -583,7 +583,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
/> />
</button> </button>
<button <button
class="color-picker__button" class="color-picker__button has-outline"
data-testid="color-top-pick-#f8f9fa" data-testid="color-top-pick-#f8f9fa"
style="--swatch-color: #f8f9fa;" style="--swatch-color: #f8f9fa;"
title="#f8f9fa" title="#f8f9fa"
@ -594,7 +594,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
/> />
</button> </button>
<button <button
class="color-picker__button" class="color-picker__button has-outline"
data-testid="color-top-pick-#f5faff" data-testid="color-top-pick-#f5faff"
style="--swatch-color: #f5faff;" style="--swatch-color: #f5faff;"
title="#f5faff" title="#f5faff"
@ -605,7 +605,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
/> />
</button> </button>
<button <button
class="color-picker__button" class="color-picker__button has-outline"
data-testid="color-top-pick-#fffce8" data-testid="color-top-pick-#fffce8"
style="--swatch-color: #fffce8;" style="--swatch-color: #fffce8;"
title="#fffce8" title="#fffce8"
@ -616,7 +616,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
/> />
</button> </button>
<button <button
class="color-picker__button" class="color-picker__button has-outline"
data-testid="color-top-pick-#fdf8f6" data-testid="color-top-pick-#fdf8f6"
style="--swatch-color: #fdf8f6;" style="--swatch-color: #fdf8f6;"
title="#fdf8f6" title="#fdf8f6"
@ -635,7 +635,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
aria-expanded="false" aria-expanded="false"
aria-haspopup="dialog" aria-haspopup="dialog"
aria-label="Canvas background" aria-label="Canvas background"
class="color-picker__button active-color properties-trigger" class="color-picker__button active-color properties-trigger has-outline"
data-state="closed" data-state="closed"
style="--swatch-color: #ffffff;" style="--swatch-color: #ffffff;"
title="Show background color picker" title="Show background color picker"