diff --git a/.changeset/add_color_heros.md b/.changeset/add_color_heros.md new file mode 100644 index 000000000..52525613e --- /dev/null +++ b/.changeset/add_color_heros.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add background styling to user profile cards diff --git a/src/app/components/user-profile/CreatorChip.tsx b/src/app/components/user-profile/CreatorChip.tsx index ab4b4e82d..1e5bf84dc 100644 --- a/src/app/components/user-profile/CreatorChip.tsx +++ b/src/app/components/user-profile/CreatorChip.tsx @@ -16,8 +16,19 @@ import { useOpenSpaceSettings } from '$state/hooks/spaceSettings'; import { SpaceSettingsPage } from '$state/spaceSettings'; import { RoomSettingsPage } from '$state/roomSettings'; import { PowerColorBadge, PowerIcon } from '$components/power'; +import * as css from './styles.css'; -export function CreatorChip() { +export function CreatorChip({ + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const room = useRoom(); @@ -53,10 +64,12 @@ export function CreatorChip() { }} > -
+
{ @@ -80,8 +93,6 @@ export function CreatorChip() { } > : undefined} onClick={open} aria-pressed={!!cords} + className={css.UserHeroChip} + style={{ + backgroundColor: cardColor, + borderColor: backgroundColor, + color: textColor, + }} > {tag.name} diff --git a/src/app/components/user-profile/PowerChip.tsx b/src/app/components/user-profile/PowerChip.tsx index 5b9669847..58a54bd19 100644 --- a/src/app/components/user-profile/PowerChip.tsx +++ b/src/app/components/user-profile/PowerChip.tsx @@ -45,6 +45,7 @@ import { useMemberPowerCompare } from '$hooks/useMemberPowerCompare'; import { CutoutCard } from '$components/cutout-card'; import { PowerColorBadge, PowerIcon } from '$components/power'; import { EventType } from '$types/matrix-sdk'; +import * as css from './styles.css'; type SelfDemoteAlertProps = { power: number; @@ -146,7 +147,19 @@ function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps) ); } -export function PowerChip({ userId }: { userId: string }) { +export function PowerChip({ + userId, + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + userId: string; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const room = useRoom(); const space = useSpaceOptionally(); @@ -241,7 +254,11 @@ export function PowerChip({ userId }: { userId: string }) { {error && ( @@ -270,6 +287,8 @@ export function PowerChip({ userId }: { userId: string }) { radii="300" aria-disabled={changing || !canChangePowers || !canAssignPower} aria-pressed={selected} + className={css.UserHeroMenuItem} + style={{ backgroundColor: cardColor, color: textColor }} before={} after={ powerTagIconSrc ? ( @@ -287,13 +306,15 @@ export function PowerChip({ userId }: { userId: string }) { ); })} - -
+ +
{ if (room.isSpaceRoom()) { openSpaceSettings( @@ -319,8 +340,14 @@ export function PowerChip({ userId }: { userId: string }) { } > diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx index 07b5bbe62..c57532dd6 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -22,7 +22,6 @@ import { Avatar, } from 'folds'; import { useMatrixClient } from '$hooks/useMatrixClient'; -import { getMxIdServer } from '$utils/matrix'; import { useCloseUserRoomProfile } from '$state/hooks/userRoomProfile'; import { stopPropagation } from '$utils/keyboard'; import { copyToClipboard } from '$utils/dom'; @@ -43,10 +42,21 @@ import { useNickname, useSetNickname } from '$hooks/useNickname'; import { CutoutCard } from '$components/cutout-card'; import { SettingTile } from '$components/setting-tile'; import { RoomAvatar, RoomIcon } from '$components/room-avatar'; - -export function ServerChip({ server }: { server: string }) { - const mx = useMatrixClient(); - const myServer = getMxIdServer(mx.getSafeUserId()); +import * as css from './styles.css'; + +export function ServerChip({ + server, + innerColor, + cardColor, + textColor, + backgroundColor, +}: { + server: string; + innerColor?: string; + cardColor?: string; + textColor?: string; + backgroundColor?: string; +}) { const navigate = useNavigate(); const closeProfile = useCloseUserRoomProfile(); const [copied, setCopied] = useTimeoutToggle(); @@ -77,9 +87,14 @@ export function ServerChip({ server }: { server: string }) { }} > -
+
Copy Server Explore Community
-
+
Open in Browser @@ -124,7 +154,6 @@ export function ServerChip({ server }: { server: string }) { } > {server} @@ -144,7 +179,19 @@ export function ServerChip({ server }: { server: string }) { ); } -export function ShareChip({ userId }: { userId: string }) { +export function ShareChip({ + userId, + innerColor, + cardColor, + textColor, + backgroundColor, +}: { + userId: string; + innerColor?: string; + cardColor?: string; + textColor?: string; + backgroundColor?: string; +}) { const [cords, setCords] = useState(); const [copied, setCopied] = useTimeoutToggle(); @@ -173,12 +220,16 @@ export function ShareChip({ userId }: { userId: string }) { }} > -
+
{ copyToClipboard(userId); setCopied(); @@ -188,10 +239,14 @@ export function ShareChip({ userId }: { userId: string }) { Copy User ID { copyToClipboard(getMatrixToUser(userId)); setCopied(); @@ -206,7 +261,7 @@ export function ShareChip({ userId }: { userId: string }) { } > Share @@ -232,7 +293,19 @@ type MutualRoomsData = { directs: Room[]; }; -export function MutualRoomsChip({ userId }: { userId: string }) { +export function MutualRoomsChip({ + userId, + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + userId: string; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const mutualRoomSupported = useMutualRoomsSupport(); const mutualRoomsState = useMutualRooms(userId); @@ -298,7 +371,12 @@ export function MutualRoomsChip({ userId }: { userId: string }) { fill="None" size="300" radii="300" - style={{ paddingLeft: config.space.S100 }} + className={css.UserHeroMenuItem} + style={{ + paddingLeft: config.space.S100, + backgroundColor: cardColor, + color: textColor, + }} onClick={() => { if (room.isSpaceRoom()) { navigateSpace(roomId); @@ -325,12 +403,17 @@ export function MutualRoomsChip({ userId }: { userId: string }) { )} /> ) : ( - + )} } > - + {room.name} @@ -360,6 +443,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) { display: 'flex', maxWidth: toRem(200), maxHeight: '80vh', + backgroundColor: innerColor, }} > @@ -367,7 +451,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) { {mutual.spaces.length > 0 && ( @@ -402,7 +486,6 @@ export function MutualRoomsChip({ userId }: { userId: string }) { } > } disabled={ @@ -410,8 +493,14 @@ export function MutualRoomsChip({ userId }: { userId: string }) { } onClick={open} aria-pressed={!!cords} + className={css.UserHeroChip} + style={{ + backgroundColor: cardColor, + borderColor: backgroundColor, + color: textColor, + }} > - + {mutualRoomsState.status === AsyncStatus.Success && `${mutualRoomsState.data.length} Mutual Rooms`} {mutualRoomsState.status === AsyncStatus.Loading && 'Mutual Rooms'} @@ -438,7 +527,19 @@ export function IgnoredUserAlert() { ); } -export function OptionsChip({ userId }: { userId: string }) { +export function OptionsChip({ + userId, + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + userId: string; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const [cords, setCords] = useState(); const [editingNick, setEditingNick] = useState(false); @@ -503,12 +604,12 @@ export function OptionsChip({ userId }: { userId: string }) { }} > -
+
{editingNick ? ( Nickname Save @@ -543,10 +649,15 @@ export function OptionsChip({ userId }: { userId: string }) { radii="300" variant="Critical" fill="None" + className={css.UserHeroMenuItem} onClick={() => { setNickname(userId, undefined); close(); }} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > Clear @@ -561,6 +672,11 @@ export function OptionsChip({ userId }: { userId: string }) { radii="300" before={} onClick={() => setEditingNick(true)} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > {currentNick ? 'Edit Nickname' : 'Set Nickname'} @@ -574,6 +690,8 @@ export function OptionsChip({ userId }: { userId: string }) { toggleIgnore(); close(); }} + className={css.UserHeroMenuItem} + style={{ backgroundColor: cardColor }} before={ ignoring ? ( @@ -583,14 +701,26 @@ export function OptionsChip({ userId }: { userId: string }) { } disabled={ignoring} > - {ignored ? 'Unblock User' : 'Block User'} + + {ignored ? 'Unblock User' : 'Block User'} +
} > - + {ignoring ? ( ) : ( diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 34cf7635a..bb4acc103 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import { Avatar, Box, + color as standardColors, Icon, Icons, Modal, @@ -28,6 +29,8 @@ import { ImageViewer } from '$components/image-viewer'; import { AvatarPresence, PresenceBadge } from '$components/presence'; import { UserAvatar } from '$components/user-avatar'; import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze'; +import { useUserProfile } from '$hooks/useUserProfile'; +import { shadeColor, areColorsTooSimilar } from '$utils/shadeColor'; import * as css from './styles.css'; type UserHeroProps = { @@ -69,8 +72,23 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs const status = presence?.status; const isExpandable = (status?.length ?? 0) > 70; + const fetchedProfile = useUserProfile(userId); + const backgroundColor = fetchedProfile.heroColor ?? standardColors.Surface.Container; + const fetchedBrightness = fetchedProfile?.heroBrightness; + const isBackgroundDark = fetchedBrightness ? fetchedBrightness === 'dark' : undefined; + const cardColor = + shadeColor(backgroundColor, isBackgroundDark ? -80 : 80) ?? standardColors.Background.Container; + const textColor = + ((fetchedBrightness === 'dark' || areColorsTooSimilar('#000000', cardColor)) && '#FFFFFF') || + ((fetchedBrightness === 'light' || areColorsTooSimilar('#FFFFFF', cardColor)) && '#000000') || + undefined; + return ( - +
@@ -201,8 +221,7 @@ export function UserHeroName({ displayName, userId }: UserHeroNameProps) { const nick = useNickname(userId); // Sable username color and fonts - const { color, font } = useSableCosmetics(userId, useRoom()); - + const { color, font } = useSableCosmetics(userId, useRoom(), true); const shownName = nick ?? displayName ?? username ?? userId; return ( diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 4f5c4a69a..8fb3ac253 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -1,4 +1,16 @@ -import { Box, Button, config, Icon, Icons, Menu, MenuItem, Scroll, Text, toRem } from 'folds'; +import { + Box, + Button, + color, + config, + Icon, + Icons, + Menu, + MenuItem, + Scroll, + Text, + toRem, +} from 'folds'; import type { SyntheticEvent } from 'react'; import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -38,12 +50,14 @@ import { filterPronounsByLanguage } from '$utils/pronouns'; import { useSetting } from '$state/hooks/settings'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; import { TextViewerContent } from '$components/text-viewer'; +import { areColorsTooSimilar, shadeColor } from '$utils/shadeColor'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { PowerChip } from './PowerChip'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; import { UserHero, UserHeroName } from './UserHero'; import { KnownMembership } from '$types/matrix-sdk'; +import * as css from './styles.css'; const KNOWN_KEYS = new Set([ 'moe.sable.app.bio', @@ -64,6 +78,10 @@ type UserExtendedSectionProps = { profile: UserProfile; htmlReactParserOptions: HTMLReactParserOptions; linkifyOpts: LinkifyOpts; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; }; const renderValue = (val: unknown) => { @@ -77,6 +95,10 @@ function UserExtendedSection({ profile, htmlReactParserOptions, linkifyOpts, + backgroundColor, + innerColor, + cardColor, + textColor, }: Readonly) { const [showMisc, setShowMisc] = useState(false); const [miscDataIndex, setMiscDataIndex] = useState(-1); @@ -162,13 +184,24 @@ function UserExtendedSection({ return null; } return ( - + handleMiscSelector(-1)} > @@ -180,8 +213,7 @@ function UserExtendedSection({ size="300" radii="300" fill="None" - variant="Secondary" - style={{ justifyContent: 'Center' }} + style={{ justifyContent: 'Center', backgroundColor: cardColor, color: textColor }} onClick={() => handleMiscSelector(index)} > {key} @@ -189,7 +221,7 @@ function UserExtendedSection({ ))} ); - }, [miscDataIndex, showMisc, unknownFields]); + }, [cardColor, innerColor, miscDataIndex, showMisc, textColor, unknownFields]); const miscHeader = useMemo( () => ( @@ -204,6 +236,7 @@ function UserExtendedSection({ justifyContent: 'flex-start', width: 'fit-content', textAlign: 'center', + color: textColor, }} > @@ -215,11 +248,10 @@ function UserExtendedSection({ {showMisc && miscSelector} ), - [miscSelector, miscDataIndex, selectedUnknownField, showMisc, unknownFields] + [miscSelector, miscDataIndex, selectedUnknownField, showMisc, unknownFields, textColor] ); - return ( - + {(pronouns || localTime) && ( {pronouns && ( @@ -257,14 +289,23 @@ function UserExtendedSection({ visibility="Always" size="300" style={{ - backgroundColor: 'var(--sable-bg-container)', + backgroundColor: cardColor, borderRadius: config.radii.R400, + borderColor: backgroundColor, + borderStyle: 'solid', + borderWidth: '1px', maxHeight: '200px', marginTop: config.space.S0, overflowY: 'auto', }} > - + {unknownFields.length > 1 && ( @@ -314,16 +356,23 @@ function UserExtendedSection({ {miscHeader} {unknownFields.length > 1 && ( )} - + + - - + + {userId !== myUserId && ( @@ -472,7 +550,14 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly} onClick={handleMessage} - style={{ marginLeft: 'auto' }} + className={css.UserHeroChip} + style={{ + marginLeft: 'auto', + backgroundColor: + backgroundColor !== color.Surface.Container ? cardColor : undefined, + borderColor: backgroundColor, + color: backgroundColor !== color.Surface.Container ? textColor : undefined, + }} > Message @@ -482,13 +567,62 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly - {server && } - - {creator ? : } - {userId !== myUserId && } - {userId !== myUserId && } + {server && ( + + )} + + {creator ? ( + + ) : ( + + )} + {userId !== myUserId && ( + + )} + {userId !== myUserId && ( + + )} {ignored && } diff --git a/src/app/components/user-profile/styles.css.ts b/src/app/components/user-profile/styles.css.ts index 10b0c9e51..3ff2f6c10 100644 --- a/src/app/components/user-profile/styles.css.ts +++ b/src/app/components/user-profile/styles.css.ts @@ -78,3 +78,19 @@ export const UserHeroAvatarImg = style({ }, }, }); +export const UserHeroChip = style({ + borderStyle: 'solid', + borderWidth: '1px', + ':hover': { + filter: 'brightness(0.8)', + transform: 'translateY(-1px)', + }, +}); +export const UserHeroMenuItem = style({ + borderStyle: 'hidden', + borderWidth: '1px', + ':hover': { + filter: 'brightness(0.8)', + transform: 'translateY(-1px)', + }, +}); diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index fcc2b18ec..dfc3873c4 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -628,6 +628,44 @@ function ProfileExtended({ profile, userId }: Readonly) { }} /> + + + handleSaveField('chat.commet.profile_color_scheme', { + color, + brightness: profile?.heroColorScheme?.brightness, + }) + } + /> + + handleSaveField('chat.commet.profile_color_scheme', { + color: profile?.heroColorScheme?.color, + brightness: profile?.heroColorScheme?.brightness === 'dark' ? 'light' : 'dark', + }) + } + > + + + {profile?.heroColorScheme?.brightness === 'dark' ? 'Dark Mode' : 'Light Mode'} + + + + + {extendedFields.length > 0 && extendedFields.map(([key, value]) => { diff --git a/src/app/hooks/useSableCosmetics.ts b/src/app/hooks/useSableCosmetics.ts index 463d0be27..ad956fdca 100644 --- a/src/app/hooks/useSableCosmetics.ts +++ b/src/app/hooks/useSableCosmetics.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import type { IContent } from '$types/matrix-sdk'; import type { Room } from '$types/matrix-sdk'; import { usePowerLevels } from './usePowerLevels'; import { useRoomCreators } from './useRoomCreators'; @@ -8,7 +9,7 @@ import { usePowerLevelTags } from './usePowerLevelTags'; import { useTheme } from './useTheme'; import { useUserProfile } from './useUserProfile'; -export function useSableCosmetics(userId: string, room: Room) { +export function useSableCosmetics(userId: string, room: Room, isUserHero?: boolean) { const theme = useTheme(); const profile = useUserProfile(userId, room); @@ -23,17 +24,27 @@ export function useSableCosmetics(userId: string, room: Room) { return useMemo(() => { if (!room || !userId) return { color: undefined, font: undefined }; - let finalColor = profile.resolvedColor; + let finalColor = isUserHero ? profile.heroNameColor : profile.resolvedColor; if (!finalColor) { const memberPowerTag = getPowerTag(userId); finalColor = memberPowerTag?.color ? accessibleTagColors?.get(memberPowerTag.color) : undefined; } - - return { + const resolvedCosmetics: IContent = { color: finalColor, font: profile.resolvedFont, }; - }, [room, userId, profile.resolvedColor, profile.resolvedFont, getPowerTag, accessibleTagColors]); + + return resolvedCosmetics; + }, [ + room, + userId, + isUserHero, + profile.heroNameColor, + profile.resolvedColor, + profile.resolvedFont, + getPowerTag, + accessibleTagColors, + ]); } diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts index 633d8d38a..04b9f5803 100644 --- a/src/app/hooks/useUserProfile.ts +++ b/src/app/hooks/useUserProfile.ts @@ -9,6 +9,7 @@ import { profilesCacheAtom } from '$state/userRoomProfile'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; import type { MSC1767Text } from '$types/matrix/common'; +import { areColorsTooSimilar, shadeColor } from '$utils/shadeColor'; import type { PronounSet } from '$utils/pronouns'; import { useMatrixClient } from './useMatrixClient'; import { ThemeKind, useActiveTheme } from './useTheme'; @@ -31,6 +32,7 @@ export type UserProfile = { nameColor?: string; nameColorDark?: string; nameColorLight?: string; + heroColorScheme?: Record; isCat?: boolean; hasCats?: boolean; extended?: Record; @@ -53,6 +55,7 @@ const normalizeInfo = (info: Record): UserProfile => { 'moe.sable.app.name_color', 'moe.sable.app.name_color_dark_theme', 'moe.sable.app.name_color_light_theme', + 'chat.commet.profile_color_scheme', 'kitty.meow.has_cats', 'kitty.meow.is_cat', ]); @@ -78,6 +81,7 @@ const normalizeInfo = (info: Record): UserProfile => { nameColor: info['moe.sable.app.name_color'] as string | undefined, nameColorDark: info['moe.sable.app.name_color_dark_theme'] as string | undefined, nameColorLight: info['moe.sable.app.name_color_light_theme'] as string | undefined, + heroColorScheme: info['chat.commet.profile_color_scheme'] as Record | undefined, isCat: info['kitty.meow.is_cat'] === true, hasCats: info['kitty.meow.has_cats'] === true, extended, @@ -102,6 +106,9 @@ export const useUserProfile = ( resolvedColor?: string; resolvedFont?: string; resolvedPronouns?: PronounSet[]; + heroColor?: string; + heroNameColor?: string; + heroBrightness?: string; } => { const mx = useMatrixClient(); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); @@ -184,7 +191,6 @@ export const useUserProfile = ( Array.isArray(localFontEvent) ? localFontEvent[0] : localFontEvent )?.getContent()?.font; } - const localPronounEvent = state?.getStateEvents( CustomStateEvent.RoomCosmeticsPronouns as string, userId @@ -221,6 +227,7 @@ export const useUserProfile = ( )?.getContent()?.pronouns; } } + const validGlobalVal = isValidHex(data?.nameColor); const validGlobalValDark = isValidHex(data?.nameColorDark); const validGlobalValLight = isValidHex(data?.nameColorLight); @@ -262,12 +269,28 @@ export const useUserProfile = ( const resolvedPronouns = localPronouns || spacePronouns || data?.pronouns; + const validHeroColor = isValidHex(data?.heroColorScheme?.color); + const heroBrightness = data?.heroColorScheme?.brightness; + const testUserHeroColor = shadeColor(validHeroColor, heroBrightness === 'dark' ? -80 : 80); + + const heroNameColor = + ((renderGlobalColors || userId === mx.getUserId()) && + heroBrightness === 'light' && + !areColorsTooSimilar(testUserHeroColor, validGlobalValLight) && + validGlobalValLight) || + (heroBrightness === 'dark' && + !areColorsTooSimilar(testUserHeroColor, validGlobalValDark) && + validGlobalValDark) || + resolvedColor; return { ...data, resolvedColor, resolvedFont, resolvedPronouns, pronouns: resolvedPronouns, + heroColor: validHeroColor, + heroBrightness, + heroNameColor, }; }, [ cached, diff --git a/src/app/utils/shadeColor.ts b/src/app/utils/shadeColor.ts new file mode 100644 index 000000000..725c3e96c --- /dev/null +++ b/src/app/utils/shadeColor.ts @@ -0,0 +1,35 @@ +export function shadeColor(initialColor?: string, percent?: number) { + if (!initialColor || initialColor[0] !== '#' || initialColor.length !== 7 || !percent) + return undefined; + const ratio = 1 + percent / 100; + + // Get hex value, convert it to number, multiply it by the desired amount, then clamp it + let R = Math.min(parseInt(initialColor.substring(1, 3), 16) * ratio, 255); + let G = Math.min(parseInt(initialColor.substring(3, 5), 16) * ratio, 255); + let B = Math.min(parseInt(initialColor.substring(5, 7), 16) * ratio, 255); + + if (R <= 8 && G <= 8 && B <= 8 && percent > 0) { + R = R <= 8 ? Math.max(178 - R, 0) : R; + G = G <= 8 ? Math.max(178 - G, 0) : G; + B = B <= 8 ? Math.max(178 - B, 0) : B; + } + + const RR = Math.floor(R).toString(16).padStart(2, '0'); + const GG = Math.floor(G).toString(16).padStart(2, '0'); + const BB = Math.floor(B).toString(16).padStart(2, '0'); + + return `#${RR}${GG}${BB}`; +} + +export function areColorsTooSimilar(colorA?: string, colorB?: string) { + if (!colorA || !colorB) return false; + + const aR = parseInt(colorA.substring(1, 3), 16); + const aG = parseInt(colorA.substring(3, 5), 16); + const aB = parseInt(colorA.substring(5, 7), 16); + const bR = parseInt(colorB.substring(1, 3), 16); + const bG = parseInt(colorB.substring(3, 5), 16); + const bB = parseInt(colorB.substring(5, 7), 16); + + return Math.abs(aR - bR) < 32 && Math.abs(aG - bG) < 32 && Math.abs(aB - bB) < 32; +}