mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-30 18:34:22 +01:00 
			
		
		
		
	feat: tweak color swatch, and button bgs (#9330)
* feat: tweak color swatch, and button bgs * snapshots
This commit is contained in:
		| @@ -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 = <R extends Record<string, any>, K extends readonly (keyof R)[]>( | ||||
|   source: R, | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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} | ||||
|   | ||||
| @@ -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} | ||||
|           > | ||||
|             <div className="color-picker__button-outline" /> | ||||
|             <HotkeyLabel color={c} keyLabel={i + 1} isCustomColor /> | ||||
|             <HotkeyLabel color={c} keyLabel={i + 1} /> | ||||
|           </button> | ||||
|         ); | ||||
|       })} | ||||
|   | ||||
| @@ -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 ( | ||||
|     <div | ||||
|       className="color-picker__button__hotkey-label" | ||||
|       style={{ | ||||
|         color: getContrastYIQ(color, isCustomColor), | ||||
|         color: isColorDark(color) ? "#fff" : "#000", | ||||
|       }} | ||||
|     > | ||||
|       {isShade && "⇧"} | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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} | ||||
|   | ||||
| @@ -93,19 +93,42 @@ export type ActiveColorPickerSectionAtomType = | ||||
| export const activeColorPickerSectionAtom = | ||||
|   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; | ||||
|   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 = | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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%); | ||||
|   | ||||
| @@ -572,7 +572,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende | ||||
|               class="color-picker__top-picks" | ||||
|             > | ||||
|               <button | ||||
|                 class="color-picker__button active" | ||||
|                 class="color-picker__button active has-outline" | ||||
|                 data-testid="color-top-pick-#ffffff" | ||||
|                 style="--swatch-color: #ffffff;" | ||||
|                 title="#ffffff" | ||||
| @@ -583,7 +583,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende | ||||
|                 /> | ||||
|               </button> | ||||
|               <button | ||||
|                 class="color-picker__button" | ||||
|                 class="color-picker__button has-outline" | ||||
|                 data-testid="color-top-pick-#f8f9fa" | ||||
|                 style="--swatch-color: #f8f9fa;" | ||||
|                 title="#f8f9fa" | ||||
| @@ -594,7 +594,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende | ||||
|                 /> | ||||
|               </button> | ||||
|               <button | ||||
|                 class="color-picker__button" | ||||
|                 class="color-picker__button has-outline" | ||||
|                 data-testid="color-top-pick-#f5faff" | ||||
|                 style="--swatch-color: #f5faff;" | ||||
|                 title="#f5faff" | ||||
| @@ -605,7 +605,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende | ||||
|                 /> | ||||
|               </button> | ||||
|               <button | ||||
|                 class="color-picker__button" | ||||
|                 class="color-picker__button has-outline" | ||||
|                 data-testid="color-top-pick-#fffce8" | ||||
|                 style="--swatch-color: #fffce8;" | ||||
|                 title="#fffce8" | ||||
| @@ -616,7 +616,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende | ||||
|                 /> | ||||
|               </button> | ||||
|               <button | ||||
|                 class="color-picker__button" | ||||
|                 class="color-picker__button has-outline" | ||||
|                 data-testid="color-top-pick-#fdf8f6" | ||||
|                 style="--swatch-color: #fdf8f6;" | ||||
|                 title="#fdf8f6" | ||||
| @@ -635,7 +635,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende | ||||
|               aria-expanded="false" | ||||
|               aria-haspopup="dialog" | ||||
|               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" | ||||
|               style="--swatch-color: #ffffff;" | ||||
|               title="Show background color picker" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 David Luzar
					David Luzar