From 57a9e301d44f319e160f618413e8fcbb7d2e1ad4 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:36:13 +0200 Subject: [PATCH] feat: tweak color swatch, and button bgs (#9330) * feat: tweak color swatch, and button bgs * snapshots --- packages/common/src/colors.ts | 2 + .../components/ColorPicker/ColorPicker.scss | 34 +++++++---- .../components/ColorPicker/ColorPicker.tsx | 9 ++- .../ColorPicker/CustomColorList.tsx | 4 +- .../components/ColorPicker/HotkeyLabel.tsx | 6 +- .../ColorPicker/PickerColorList.tsx | 2 +- .../components/ColorPicker/ShadeList.tsx | 2 +- .../components/ColorPicker/TopPicks.tsx | 7 +++ .../ColorPicker/colorPickerUtils.ts | 58 +++++++++++++------ packages/excalidraw/css/styles.scss | 12 +--- packages/excalidraw/css/theme.scss | 4 +- .../__snapshots__/excalidraw.test.tsx.snap | 12 ++-- 12 files changed, 94 insertions(+), 58 deletions(-) diff --git a/packages/common/src/colors.ts b/packages/common/src/colors.ts index 84d04bcf4..4dc45616f 100644 --- a/packages/common/src/colors.ts +++ b/packages/common/src/colors.ts @@ -2,6 +2,8 @@ import oc from "open-color"; 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 const pick = , K extends readonly (keyof R)[]>( source: R, diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.scss b/packages/excalidraw/components/ColorPicker/ColorPicker.scss index bcb39281d..56b40869b 100644 --- a/packages/excalidraw/components/ColorPicker/ColorPicker.scss +++ b/packages/excalidraw/components/ColorPicker/ColorPicker.scss @@ -15,7 +15,7 @@ .color-picker-container { display: grid; - grid-template-columns: 1fr 8px 1.625rem; + grid-template-columns: 1fr 20px 1.625rem; padding: 0.25rem 0px; align-items: center; @@ -27,14 +27,19 @@ .color-picker__top-picks { display: flex; justify-content: space-between; + align-items: center; } .color-picker__button { - --radius: 6px; + --radius: 4px; --size: 1.375rem; + &.has-outline { + box-shadow: inset 0 0 0 1px #d9d9d9; + } + padding: 0; - margin: 1px; + margin: 0; width: var(--size); height: var(--size); border: 0; @@ -46,15 +51,19 @@ font-family: inherit; 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 { content: ""; position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - box-shadow: 0 0 0 1px var(--swatch-color); + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + box-shadow: 0 0 0 1px var(--color-gray-30); border-radius: var(--radius); filter: var(--theme-filter); } @@ -70,7 +79,7 @@ bottom: var(--offset); box-shadow: 0 0 0 1px var(--color-primary-darkest); z-index: 1; // due hover state so this has preference - border-radius: calc(var(--radius) + 1px); + border-radius: var(--radius); filter: var(--theme-filter); } } @@ -125,10 +134,11 @@ .color-picker__button__hotkey-label { position: absolute; - right: 4px; - bottom: 4px; + right: 5px; + bottom: 3px; filter: none; font-size: 11px; + font-weight: 500; } .color-picker { diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx index 34f6c6d12..eb6d82d9e 100644 --- a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx +++ b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx @@ -2,7 +2,11 @@ import * as Popover from "@radix-ui/react-popover"; import clsx from "clsx"; 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"; @@ -19,7 +23,7 @@ import { ColorInput } from "./ColorInput"; import { Picker } from "./Picker"; import PickerHeading from "./PickerHeading"; import { TopPicks } from "./TopPicks"; -import { activeColorPickerSectionAtom } from "./colorPickerUtils"; +import { activeColorPickerSectionAtom, isColorDark } from "./colorPickerUtils"; import "./ColorPicker.scss"; @@ -190,6 +194,7 @@ const ColorPickerTrigger = ({ type="button" className={clsx("color-picker__button active-color properties-trigger", { "is-transparent": color === "transparent" || !color, + "has-outline": !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD), })} aria-label={label} style={color ? { "--swatch-color": color } : undefined} diff --git a/packages/excalidraw/components/ColorPicker/CustomColorList.tsx b/packages/excalidraw/components/ColorPicker/CustomColorList.tsx index 2c735102a..45d5db84c 100644 --- a/packages/excalidraw/components/ColorPicker/CustomColorList.tsx +++ b/packages/excalidraw/components/ColorPicker/CustomColorList.tsx @@ -40,7 +40,7 @@ export const CustomColorList = ({ tabIndex={-1} type="button" className={clsx( - "color-picker__button color-picker__button--large", + "color-picker__button color-picker__button--large has-outline", { active: color === c, "is-transparent": c === "transparent" || !c, @@ -56,7 +56,7 @@ export const CustomColorList = ({ key={i} >
- + ); })} diff --git a/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx b/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx index 6e4d5e39c..898a28970 100644 --- a/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx +++ b/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx @@ -1,24 +1,22 @@ import React from "react"; -import { getContrastYIQ } from "./colorPickerUtils"; +import { isColorDark } from "./colorPickerUtils"; interface HotkeyLabelProps { color: string; keyLabel: string | number; - isCustomColor?: boolean; isShade?: boolean; } const HotkeyLabel = ({ color, keyLabel, - isCustomColor = false, isShade = false, }: HotkeyLabelProps) => { return (
{isShade && "⇧"} diff --git a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx index 50594a59e..38e5cf8c5 100644 --- a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx +++ b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx @@ -65,7 +65,7 @@ const PickerColorList = ({ tabIndex={-1} type="button" className={clsx( - "color-picker__button color-picker__button--large", + "color-picker__button color-picker__button--large has-outline", { active: colorObj?.colorName === key, "is-transparent": color === "transparent" || !color, diff --git a/packages/excalidraw/components/ColorPicker/ShadeList.tsx b/packages/excalidraw/components/ColorPicker/ShadeList.tsx index aa2c25ea0..1c8e4c4eb 100644 --- a/packages/excalidraw/components/ColorPicker/ShadeList.tsx +++ b/packages/excalidraw/components/ColorPicker/ShadeList.tsx @@ -55,7 +55,7 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => { key={i} type="button" className={clsx( - "color-picker__button color-picker__button--large", + "color-picker__button color-picker__button--large has-outline", { active: i === shade }, )} aria-label="Shade" diff --git a/packages/excalidraw/components/ColorPicker/TopPicks.tsx b/packages/excalidraw/components/ColorPicker/TopPicks.tsx index 6d18a9587..8531172fb 100644 --- a/packages/excalidraw/components/ColorPicker/TopPicks.tsx +++ b/packages/excalidraw/components/ColorPicker/TopPicks.tsx @@ -1,11 +1,14 @@ import clsx from "clsx"; import { + COLOR_OUTLINE_CONTRAST_THRESHOLD, DEFAULT_CANVAS_BACKGROUND_PICKS, DEFAULT_ELEMENT_BACKGROUND_PICKS, DEFAULT_ELEMENT_STROKE_PICKS, } from "@excalidraw/common"; +import { isColorDark } from "./colorPickerUtils"; + import type { ColorPickerType } from "./colorPickerUtils"; interface TopPicksProps { @@ -51,6 +54,10 @@ export const TopPicks = ({ className={clsx("color-picker__button", { active: color === activeColor, "is-transparent": color === "transparent" || !color, + "has-outline": !isColorDark( + color, + COLOR_OUTLINE_CONTRAST_THRESHOLD, + ), })} style={{ "--swatch-color": color }} key={color} diff --git a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts index 4925a3145..f572bd49f 100644 --- a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts +++ b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts @@ -93,19 +93,42 @@ export type ActiveColorPickerSectionAtomType = export const activeColorPickerSectionAtom = atom(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; - return yiq >= 160 ? "black" : "white"; + return yiq; }; -// inspiration from https://stackoverflow.com/a/11868398 -export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => { - if (isCustomColor) { - const style = new Option().style; - style.color = bgHex; +// YIQ algo, inspiration from https://stackoverflow.com/a/11868398 +export const isColorDark = (color: string, threshold = 160): boolean => { + // no color ("") -> assume it default to black + if (!color) { + return true; + } - if (style.color) { - const rgb = style.color + if (color === "transparent") { + 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(/\)$/, "") .replace(/\s/g, "") @@ -114,20 +137,17 @@ export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => { const g = parseInt(rgb[1]); 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? - if (bgHex === "transparent") { - return "black"; - } + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); - const r = parseInt(bgHex.substring(1, 3), 16); - const g = parseInt(bgHex.substring(3, 5), 16); - const b = parseInt(bgHex.substring(5, 7), 16); - - return calculateContrast(r, g, b); + return calculateContrast(r, g, b) < threshold; }; export type ColorPickerType = diff --git a/packages/excalidraw/css/styles.scss b/packages/excalidraw/css/styles.scss index 33f9b4df0..6f1d9cd48 100644 --- a/packages/excalidraw/css/styles.scss +++ b/packages/excalidraw/css/styles.scss @@ -173,7 +173,7 @@ body.excalidraw-cursor-resize * { .buttonList { flex-wrap: wrap; display: flex; - column-gap: 0.375rem; + column-gap: 0.5rem; row-gap: 0.5rem; label { @@ -386,16 +386,10 @@ body.excalidraw-cursor-resize * { .App-menu__left { overflow-y: auto; - padding: 0.75rem 0.75rem 0.25rem 0.75rem; - width: 11.875rem; + padding: 0.75rem; + width: 12.5rem; box-sizing: border-box; position: absolute; - - .buttonList label, - .buttonList button, - .buttonList .zIndexButton { - --button-bg: transparent; - } } .dropdown-select { diff --git a/packages/excalidraw/css/theme.scss b/packages/excalidraw/css/theme.scss index fd6a8dacb..1d6a56966 100644 --- a/packages/excalidraw/css/theme.scss +++ b/packages/excalidraw/css/theme.scss @@ -148,7 +148,7 @@ --border-radius-lg: 0.5rem; --color-surface-high: #f1f0ff; - --color-surface-mid: #f2f2f7; + --color-surface-mid: #f6f6f9; --color-surface-low: #ececf4; --color-surface-lowest: #ffffff; --color-on-surface: #1b1b1f; @@ -252,7 +252,7 @@ --color-logo-text: #e2dfff; - --color-surface-high: hsl(245, 10%, 21%); + --color-surface-high: #2e2d39; --color-surface-low: hsl(240, 8%, 15%); --color-surface-mid: hsl(240 6% 10%); --color-surface-lowest: hsl(0, 0%, 7%); diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap index e5e431dfc..bbcc8d7e0 100644 --- a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap @@ -572,7 +572,7 @@ exports[` > Test UIOptions prop > Test canvasActions > should rende class="color-picker__top-picks" >