mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
feat: tweak color swatch, and button bgs (#9330)
* feat: tweak color swatch, and button bgs * snapshots
This commit is contained in:
parent
7c58477382
commit
57a9e301d4
12 changed files with 94 additions and 58 deletions
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue