From e5fcc494c74ce80d5f5ac007986063859dd3ab61 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Wed, 22 Apr 2026 15:52:10 +0300 Subject: [PATCH 01/19] basic color rendering --- src/app/components/user-profile/UserHero.tsx | 12 +++++-- .../user-profile/UserRoomProfile.tsx | 36 ++++++++++++++++--- src/app/hooks/useSableCosmetics.ts | 25 +++++++++---- src/app/hooks/useUserProfile.ts | 14 ++++++-- 4 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 0acdc3618..2fabad92a 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -28,6 +28,7 @@ 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 * as css from './styles.css'; type UserHeroProps = { @@ -69,8 +70,14 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs const status = presence?.status; const isExpandable = (status?.length ?? 0) > 70; + const fetchedProfile = useUserProfile(userId, undefined, undefined, true); + return ( - +
0 ? fetchedProfile @@ -451,8 +463,24 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly - - + + {userId !== myUserId && ( diff --git a/src/app/hooks/useSableCosmetics.ts b/src/app/hooks/useSableCosmetics.ts index b9b54c7e3..f68a1f7fd 100644 --- a/src/app/hooks/useSableCosmetics.ts +++ b/src/app/hooks/useSableCosmetics.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Room } from '$types/matrix-sdk'; +import { IContent, Room } from '$types/matrix-sdk'; import { usePowerLevels } from './usePowerLevels'; import { useRoomCreators } from './useRoomCreators'; import { useAccessiblePowerTagColors, useGetMemberPowerTag } from './useMemberPowerTag'; @@ -8,9 +8,9 @@ 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, isInHero?: boolean) { const theme = useTheme(); - const profile = useUserProfile(userId, room); + const profile = useUserProfile(userId, room, undefined, isInHero); const powerLevels = usePowerLevels(room); const creators = useRoomCreators(room); @@ -30,10 +30,23 @@ export function useSableCosmetics(userId: string, room: Room) { ? accessibleTagColors?.get(memberPowerTag.color) : undefined; } - - return { + const resolvedCosmetics: IContent = { color: finalColor, font: profile.resolvedFont, }; - }, [room, userId, profile.resolvedColor, profile.resolvedFont, getPowerTag, accessibleTagColors]); + if (isInHero && profile.heroColorScheme) { + resolvedCosmetics.heroColorScheme = profile.heroColorScheme; + } + + return resolvedCosmetics; + }, [ + room, + userId, + profile.resolvedColor, + profile.resolvedFont, + profile.heroColorScheme, + isInHero, + getPowerTag, + accessibleTagColors, + ]); } diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts index 4d78035ea..5e25e88ae 100644 --- a/src/app/hooks/useUserProfile.ts +++ b/src/app/hooks/useUserProfile.ts @@ -28,6 +28,7 @@ export type UserProfile = { nameColor?: string; nameColorDark?: string; nameColorLight?: string; + heroColorScheme?: Record; isCat?: boolean; hasCats?: boolean; extended?: Record; @@ -50,6 +51,7 @@ const normalizeInfo = (info: any): 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', ]); @@ -75,6 +77,7 @@ const normalizeInfo = (info: any): UserProfile => { nameColor: info['moe.sable.app.name_color'], nameColorDark: info['moe.sable.app.name_color_dark_theme'], nameColorLight: info['moe.sable.app.name_color_light_theme'], + heroColorScheme: info['chat.commet.profile_color_scheme'], isCat: info['kitty.meow.is_cat'] === true, hasCats: info['kitty.meow.has_cats'] === true, extended, @@ -94,7 +97,8 @@ const sanitizeFont = (f: string) => f.replaceAll(/[;{}<>]/g, '').slice(0, 32); export const useUserProfile = ( userId: string, room?: Room, - initialProfile?: Partial + initialProfile?: Partial, + isInUserHero?: boolean ): UserProfile & { resolvedColor?: string; resolvedFont?: string; @@ -181,7 +185,6 @@ export const useUserProfile = ( Array.isArray(localFontEvent) ? localFontEvent[0] : localFontEvent )?.getContent()?.font; } - const localPronounEvent = state?.getStateEvents( StateEvent.RoomCosmeticsPronouns as string, userId @@ -212,6 +215,12 @@ export const useUserProfile = ( const validGlobalVal = isValidHex(data?.nameColor); const validGlobalValDark = isValidHex(data?.nameColorDark); const validGlobalValLight = isValidHex(data?.nameColorLight); + console.log( + 'Hero Color Scheme: ', + data?.heroColorScheme?.color, + 'Is User Hero: ', + isInUserHero === true + ); const validGlobalGeneral = (renderGlobalColors || userId === mx.getUserId()) && !!validGlobalVal @@ -265,6 +274,7 @@ export const useUserProfile = ( room, renderRoomColors, renderRoomFonts, + isInUserHero, renderGlobalColors, themeKind, legacyUsernameColor, From b8e6b796c9adeb32d9bddb50d5f57446fdd2528d Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Wed, 22 Apr 2026 19:26:28 +0300 Subject: [PATCH 02/19] inner coloring --- .../user-profile/UserRoomProfile.tsx | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index bd5b9e23e..3b056e490 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -53,6 +53,7 @@ import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './ import { PowerChip } from './PowerChip'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; import { UserHero, UserHeroName } from './UserHero'; +import { text } from 'stream/consumers'; const KNOWN_KEYS = [ 'moe.sable.app.bio', @@ -73,12 +74,16 @@ type UserExtendedSectionProps = { profile: UserProfile; htmlReactParserOptions: HTMLReactParserOptions; linkifyOpts: LinkifyOpts; + cardColor?: string; + textColor?: string; }; function UserExtendedSection({ profile, htmlReactParserOptions, linkifyOpts, + cardColor, + textColor, }: Readonly) { const [showMisc, setShowMisc] = useState(false); const [miscDataIndex, setMiscDataIndex] = useState(-1); @@ -211,6 +216,7 @@ function UserExtendedSection({ justifyContent: 'flex-start', width: 'fit-content', textAlign: 'center', + color: textColor, }} > @@ -222,11 +228,11 @@ function UserExtendedSection({ {showMisc && miscSelector} ), - [miscSelector, miscDataIndex, showMisc, unknownFields] + [miscDataIndex, textColor, unknownFields, showMisc, miscSelector] ); - + console.log('text', textColor); return ( - + {(pronouns || localTime) && ( {pronouns && ( @@ -264,7 +270,7 @@ function UserExtendedSection({ visibility="Always" size="300" style={{ - backgroundColor: 'var(--sable-bg-container)', + backgroundColor: cardColor, borderRadius: config.radii.R400, maxHeight: '200px', marginTop: config.space.S0, @@ -291,7 +297,7 @@ function UserExtendedSection({
+ @@ -501,6 +538,8 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly {server && } From 67a2d7ccb3fb67194c98ce0671e7fd5ef3565f70 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Wed, 22 Apr 2026 19:49:17 +0300 Subject: [PATCH 03/19] introduced basic typechecking --- src/app/components/user-profile/UserHero.tsx | 4 ++-- .../user-profile/UserRoomProfile.tsx | 8 ++++---- src/app/hooks/useSableCosmetics.ts | 18 +++--------------- src/app/hooks/useUserProfile.ts | 17 ++++++++--------- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 2fabad92a..224a98d59 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -70,7 +70,7 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs const status = presence?.status; const isExpandable = (status?.length ?? 0) > 70; - const fetchedProfile = useUserProfile(userId, undefined, undefined, true); + const fetchedProfile = useUserProfile(userId); return ( 0 ? fetchedProfile @@ -459,6 +458,7 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly f.replaceAll(/[;{}<>]/g, '').slice(0, 32); export const useUserProfile = ( userId: string, room?: Room, - initialProfile?: Partial, - isInUserHero?: boolean + initialProfile?: Partial ): UserProfile & { resolvedColor?: string; resolvedFont?: string; resolvedPronouns?: any[]; + heroColor?: string; + heroBrightness?: string; } => { const mx = useMatrixClient(); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); @@ -212,15 +213,12 @@ export const useUserProfile = ( } } } + const validHeroColor = isValidHex(data?.heroColorScheme?.color); + const heroBrightness = data?.heroColorScheme?.brightness; + const validGlobalVal = isValidHex(data?.nameColor); const validGlobalValDark = isValidHex(data?.nameColorDark); const validGlobalValLight = isValidHex(data?.nameColorLight); - console.log( - 'Hero Color Scheme: ', - data?.heroColorScheme?.color, - 'Is User Hero: ', - isInUserHero === true - ); const validGlobalGeneral = (renderGlobalColors || userId === mx.getUserId()) && !!validGlobalVal @@ -265,6 +263,8 @@ export const useUserProfile = ( resolvedFont, resolvedPronouns, pronouns: resolvedPronouns, + heroColor: validHeroColor, + heroBrightness, }; }, [ cached, @@ -274,7 +274,6 @@ export const useUserProfile = ( room, renderRoomColors, renderRoomFonts, - isInUserHero, renderGlobalColors, themeKind, legacyUsernameColor, From 51d476672c6ad201740bc2164b9d353addb0b01f Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Wed, 22 Apr 2026 22:37:07 +0300 Subject: [PATCH 04/19] set the username color to respect the hero in brightness --- src/app/components/user-profile/UserHero.tsx | 15 +++++++++++- .../user-profile/UserRoomProfile.tsx | 23 ++----------------- src/app/hooks/useSableCosmetics.ts | 15 +++++++++--- src/app/hooks/useUserProfile.ts | 12 ++++++++-- src/app/utils/shadeColor.ts | 21 +++++++++++++++++ 5 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 src/app/utils/shadeColor.ts diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 224a98d59..3613978fb 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, @@ -29,6 +30,7 @@ import { AvatarPresence, PresenceBadge } from '$components/presence'; import { UserAvatar } from '$components/user-avatar'; import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze'; import { useUserProfile } from '$hooks/useUserProfile'; +import { shadeColor } from '$utils/shadeColor'; import * as css from './styles.css'; type UserHeroProps = { @@ -71,6 +73,15 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs 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' && '#FFFFFF') || + (fetchedBrightness === 'light' && '#000000') || + undefined; return ( @@ -203,7 +216,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 db085e6b3..29889b128 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -48,6 +48,7 @@ import { filterPronounsByLanguage } from '$utils/pronouns'; import { useSetting } from '$state/hooks/settings'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; import { TextViewerContent } from '$components/text-viewer'; +import { shadeColor } from '$utils/shadeColor'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { PowerChip } from './PowerChip'; @@ -229,7 +230,6 @@ function UserExtendedSection({ ), [miscDataIndex, textColor, unknownFields, showMisc, miscSelector] ); - console.log('text', textColor); return ( {(pronouns || localTime) && ( @@ -459,35 +459,16 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly diff --git a/src/app/hooks/useSableCosmetics.ts b/src/app/hooks/useSableCosmetics.ts index 1efc9057b..e757e7a69 100644 --- a/src/app/hooks/useSableCosmetics.ts +++ b/src/app/hooks/useSableCosmetics.ts @@ -8,7 +8,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,7 +23,7 @@ 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 @@ -36,5 +36,14 @@ export function useSableCosmetics(userId: string, room: Room) { }; return resolvedCosmetics; - }, [room, userId, profile.resolvedColor, profile.resolvedFont, getPowerTag, accessibleTagColors]); + }, [ + 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 4c0b7b66c..b80fa6fdf 100644 --- a/src/app/hooks/useUserProfile.ts +++ b/src/app/hooks/useUserProfile.ts @@ -103,6 +103,7 @@ export const useUserProfile = ( resolvedFont?: string; resolvedPronouns?: any[]; heroColor?: string; + heroNameColor?: string; heroBrightness?: string; } => { const mx = useMatrixClient(); @@ -213,8 +214,6 @@ export const useUserProfile = ( } } } - const validHeroColor = isValidHex(data?.heroColorScheme?.color); - const heroBrightness = data?.heroColorScheme?.brightness; const validGlobalVal = isValidHex(data?.nameColor); const validGlobalValDark = isValidHex(data?.nameColorDark); @@ -257,6 +256,14 @@ export const useUserProfile = ( const resolvedPronouns = localPronouns || spacePronouns || data?.pronouns; + const validHeroColor = isValidHex(data?.heroColorScheme?.color); + const heroBrightness = data?.heroColorScheme?.brightness; + const heroNameColor = + ((renderGlobalColors || userId === mx.getUserId()) && + heroBrightness === 'light' && + validGlobalValLight) || + (heroBrightness === 'dark' && validGlobalValDark) || + resolvedColor; return { ...data, resolvedColor, @@ -265,6 +272,7 @@ export const useUserProfile = ( 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..00dabe397 --- /dev/null +++ b/src/app/utils/shadeColor.ts @@ -0,0 +1,21 @@ +export function shadeColor(initialColor: string, percent: number) { + if (!initialColor || initialColor[0] !== '#' || initialColor.length !== 7) return undefined; + const ratio = 1 + percent / 100; + + // Get hex value, convert it to number, multiply it by the desired amount, then clamp it + const R = Math.floor( + Math.max(Math.min(parseInt(initialColor.substring(1, 3), 16) * ratio, 255), 0) + ); + const G = Math.floor( + Math.max(Math.min(parseInt(initialColor.substring(3, 5), 16) * ratio, 255), 0) + ); + const B = Math.floor( + Math.max(Math.min(parseInt(initialColor.substring(5, 7), 16) * ratio, 255), 0) + ); + + const RR = `${R < 16 ? '0' : ''}${R.toString(16)}`; + const GG = `${G < 16 ? '0' : ''}${G.toString(16)}`; + const BB = `${B < 16 ? '0' : ''}${B.toString(16)}`; + + return `#${RR}${GG}${BB}`; +} From e32376b6d557be42e8c7bd134a93d935d83f136f Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 00:38:52 +0300 Subject: [PATCH 05/19] make chips, and messages have the right formatting --- src/app/components/user-profile/PowerChip.tsx | 35 ++++- src/app/components/user-profile/UserChips.tsx | 146 ++++++++++++++---- .../user-profile/UserRoomProfile.tsx | 104 +++++++++++-- src/app/components/user-profile/styles.css.ts | 4 + 4 files changed, 242 insertions(+), 47 deletions(-) diff --git a/src/app/components/user-profile/PowerChip.tsx b/src/app/components/user-profile/PowerChip.tsx index d1e4dbfa2..ba6440b2c 100644 --- a/src/app/components/user-profile/PowerChip.tsx +++ b/src/app/components/user-profile/PowerChip.tsx @@ -43,6 +43,7 @@ import { useRoomPermissions } from '$hooks/useRoomPermissions'; import { useMemberPowerCompare } from '$hooks/useMemberPowerCompare'; import { CutoutCard } from '$components/cutout-card'; import { PowerColorBadge, PowerIcon } from '$components/power'; +import * as css from './styles.css'; type SelfDemoteAlertProps = { power: number; @@ -144,7 +145,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(); @@ -239,7 +252,11 @@ export function PowerChip({ userId }: { userId: string }) { {error && ( @@ -268,6 +285,7 @@ export function PowerChip({ userId }: { userId: string }) { radii="300" aria-disabled={changing || !canChangePowers || !canAssignPower} aria-pressed={selected} + style={{ backgroundColor: cardColor, color: textColor }} before={} after={ powerTagIconSrc ? ( @@ -285,13 +303,14 @@ export function PowerChip({ userId }: { userId: string }) { ); })} - -
+ +
{ if (room.isSpaceRoom()) { openSpaceSettings( @@ -317,8 +336,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 44432df22..01c12ef9a 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -29,7 +29,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'; @@ -50,10 +49,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(); @@ -84,9 +94,14 @@ export function ServerChip({ server }: { server: string }) { }} > -
+
Copy Server Explore Community
-
+
Open in Browser @@ -131,7 +149,6 @@ export function ServerChip({ server }: { server: string }) { } > {server} @@ -151,7 +174,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(); @@ -180,12 +215,12 @@ export function ShareChip({ userId }: { userId: string }) { }} > -
+
{ copyToClipboard(userId); setCopied(); @@ -195,10 +230,10 @@ export function ShareChip({ userId }: { userId: string }) { Copy User ID { copyToClipboard(getMatrixToUser(userId)); setCopied(); @@ -213,7 +248,7 @@ export function ShareChip({ userId }: { userId: string }) { } > Share @@ -239,7 +280,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); @@ -305,7 +358,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) { fill="None" size="300" radii="300" - style={{ paddingLeft: config.space.S100 }} + style={{ paddingLeft: config.space.S100, backgroundColor: cardColor, color: textColor }} onClick={() => { if (room.isSpaceRoom()) { navigateSpace(roomId); @@ -332,12 +385,17 @@ export function MutualRoomsChip({ userId }: { userId: string }) { )} /> ) : ( - + )} } > - + {room.name} @@ -367,6 +425,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) { display: 'flex', maxWidth: toRem(200), maxHeight: '80vh', + backgroundColor: innerColor, }} > @@ -374,7 +433,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) { {mutual.spaces.length > 0 && ( @@ -409,7 +468,6 @@ export function MutualRoomsChip({ userId }: { userId: string }) { } > } disabled={ @@ -417,8 +475,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'} @@ -445,7 +509,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); @@ -510,7 +586,7 @@ export function OptionsChip({ userId }: { userId: string }) { }} > -
+
{editingNick ? ( Save @@ -554,6 +631,7 @@ export function OptionsChip({ userId }: { userId: string }) { setNickname(userId, undefined); close(); }} + style={{ backgroundColor: cardColor, color: textColor }} > Clear @@ -568,6 +646,7 @@ export function OptionsChip({ userId }: { userId: string }) { radii="300" before={} onClick={() => setEditingNick(true)} + style={{ backgroundColor: cardColor, color: textColor }} > {currentNick ? 'Edit Nickname' : 'Set Nickname'} @@ -581,6 +660,7 @@ export function OptionsChip({ userId }: { userId: string }) { toggleIgnore(); close(); }} + style={{ backgroundColor: cardColor }} before={ ignoring ? ( @@ -590,14 +670,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/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 29889b128..9ca4e681a 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -74,6 +74,8 @@ type UserExtendedSectionProps = { profile: UserProfile; htmlReactParserOptions: HTMLReactParserOptions; linkifyOpts: LinkifyOpts; + backgroundColor?: string; + innerColor?: string; cardColor?: string; textColor?: string; }; @@ -82,6 +84,8 @@ function UserExtendedSection({ profile, htmlReactParserOptions, linkifyOpts, + backgroundColor, + innerColor, cardColor, textColor, }: Readonly) { @@ -175,13 +179,24 @@ function UserExtendedSection({ return null; } return ( - + handleMiscSelector(-1)} > @@ -192,8 +207,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} @@ -201,7 +215,7 @@ function UserExtendedSection({ ))} ); - }, [miscDataIndex, showMisc, unknownFields]); + }, [cardColor, innerColor, miscDataIndex, showMisc, textColor, unknownFields]); const miscHeader = useMemo( () => ( @@ -271,6 +285,9 @@ function UserExtendedSection({ style={{ backgroundColor: cardColor, borderRadius: config.radii.R400, + borderColor: backgroundColor, + borderStyle: 'solid', + borderWidth: '1px', maxHeight: '200px', marginTop: config.space.S0, overflowY: 'auto', @@ -299,6 +316,7 @@ function UserExtendedSection({ backgroundColor: cardColor, borderColor: 'var(--sable-surface-container-line)', borderRadius: config.radii.R400, + borderWidth: '1px', }} > {unknownFields.length > 1 && ( @@ -326,16 +344,23 @@ function UserExtendedSection({ {miscHeader} {unknownFields.length > 1 && ( )} - + } onClick={handleMessage} - style={{ marginLeft: 'auto' }} + style={{ + marginLeft: 'auto', + backgroundColor: cardColor, + borderColor: backgroundColor, + color: textColor, + borderStyle: 'solid', + borderWidth: '1px', + }} > Message @@ -519,15 +551,57 @@ 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..465f25924 100644 --- a/src/app/components/user-profile/styles.css.ts +++ b/src/app/components/user-profile/styles.css.ts @@ -78,3 +78,7 @@ export const UserHeroAvatarImg = style({ }, }, }); +export const UserHeroChip = style({ + borderStyle: 'solid', + borderWidth: '1px', +}); From b6b967bb30b321c29509144054e26d8ad17c7109 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 00:46:40 +0300 Subject: [PATCH 06/19] fix message button look --- src/app/components/user-profile/UserRoomProfile.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 9ca4e681a..733764227 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -536,9 +536,10 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly Date: Thu, 23 Apr 2026 01:39:37 +0300 Subject: [PATCH 07/19] add setting button --- src/app/features/settings/account/Profile.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index 8ee4d9b35..747149f31 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -627,6 +627,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]) => { From 7546c22d2f2386ea736e2ce2cf2adaa377b74db8 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 01:43:20 +0300 Subject: [PATCH 08/19] add changeset --- .changeset/add_color_heros.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add_color_heros.md diff --git a/.changeset/add_color_heros.md b/.changeset/add_color_heros.md new file mode 100644 index 000000000..f936fe519 --- /dev/null +++ b/.changeset/add_color_heros.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add styling background styling to user profile cards From 6a000a42c14b54ba50c30c18b37e49cd05ed04a5 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 01:44:16 +0300 Subject: [PATCH 09/19] fix wording --- .changeset/add_color_heros.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/add_color_heros.md b/.changeset/add_color_heros.md index f936fe519..52525613e 100644 --- a/.changeset/add_color_heros.md +++ b/.changeset/add_color_heros.md @@ -2,4 +2,4 @@ default: minor --- -Add styling background styling to user profile cards +Add background styling to user profile cards From 3446423ed9cefd79ecadaa7b22d47bc69da30707 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 02:17:19 +0300 Subject: [PATCH 10/19] edgecase of setting the color to #000 --- src/app/utils/shadeColor.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/app/utils/shadeColor.ts b/src/app/utils/shadeColor.ts index 00dabe397..b0b9ce59b 100644 --- a/src/app/utils/shadeColor.ts +++ b/src/app/utils/shadeColor.ts @@ -3,15 +3,13 @@ export function shadeColor(initialColor: string, percent: number) { const ratio = 1 + percent / 100; // Get hex value, convert it to number, multiply it by the desired amount, then clamp it - const R = Math.floor( - Math.max(Math.min(parseInt(initialColor.substring(1, 3), 16) * ratio, 255), 0) - ); - const G = Math.floor( - Math.max(Math.min(parseInt(initialColor.substring(3, 5), 16) * ratio, 255), 0) - ); - const B = Math.floor( - Math.max(Math.min(parseInt(initialColor.substring(5, 7), 16) * ratio, 255), 0) - ); + let R = Math.floor(Math.min(parseInt(initialColor.substring(1, 3), 16) * ratio, 255)); + let G = Math.floor(Math.min(parseInt(initialColor.substring(3, 5), 16) * ratio, 255)); + let B = Math.floor(Math.min(parseInt(initialColor.substring(5, 7), 16) * ratio, 255)); + + if (R <= 0 && percent > 0) R = Math.max(178 - R, 0); + if (G <= 0 && percent > 0) G = Math.max(178 - G, 0); + if (B <= 0 && percent > 0) B = Math.max(178 - B, 0); const RR = `${R < 16 ? '0' : ''}${R.toString(16)}`; const GG = `${G < 16 ? '0' : ''}${G.toString(16)}`; From 49e8d6f85a19e0d6e936a1523ce1781b8dba3822 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 02:41:31 +0300 Subject: [PATCH 11/19] more complex edgecasing maybe --- .../user-profile/UserRoomProfile.tsx | 8 +++++++- src/app/utils/shadeColor.ts | 20 ++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 733764227..9602b54b7 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -293,7 +293,13 @@ function UserExtendedSection({ overflowY: 'auto', }} > - + 0) R = Math.max(178 - R, 0); - if (G <= 0 && percent > 0) G = Math.max(178 - G, 0); - if (B <= 0 && percent > 0) B = Math.max(178 - B, 0); + if (R <= 0 && G <= 0 && B <= 0 && percent > 0) { + R = R <= 0 ? Math.max(178 - R, 0) : R; + G = G <= 0 ? Math.max(178 - G, 0) : G; + B = B <= 0 ? Math.max(178 - B, 0) : B; + } - const RR = `${R < 16 ? '0' : ''}${R.toString(16)}`; - const GG = `${G < 16 ? '0' : ''}${G.toString(16)}`; - const BB = `${B < 16 ? '0' : ''}${B.toString(16)}`; + 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}`; } From 3cfbb99ece5d4e2755eef51ce6be33d358badc56 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 03:05:51 +0300 Subject: [PATCH 12/19] add checking for bad choices --- src/app/components/user-profile/UserHero.tsx | 6 ++--- .../user-profile/UserRoomProfile.tsx | 6 ++--- src/app/utils/shadeColor.ts | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 3613978fb..16935912b 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -30,7 +30,7 @@ import { AvatarPresence, PresenceBadge } from '$components/presence'; import { UserAvatar } from '$components/user-avatar'; import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze'; import { useUserProfile } from '$hooks/useUserProfile'; -import { shadeColor } from '$utils/shadeColor'; +import { isTooBright, isTooDark, shadeColor } from '$utils/shadeColor'; import * as css from './styles.css'; type UserHeroProps = { @@ -79,8 +79,8 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs const cardColor = shadeColor(backgroundColor, isBackgroundDark ? -80 : 80) ?? standardColors.Background.Container; const textColor = - (fetchedBrightness === 'dark' && '#FFFFFF') || - (fetchedBrightness === 'light' && '#000000') || + ((fetchedBrightness === 'dark' || isTooDark(cardColor)) && '#FFFFFF') || + ((fetchedBrightness === 'light' || isTooBright(cardColor)) && '#000000') || undefined; return ( diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 9602b54b7..f312ada46 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -48,7 +48,7 @@ import { filterPronounsByLanguage } from '$utils/pronouns'; import { useSetting } from '$state/hooks/settings'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; import { TextViewerContent } from '$components/text-viewer'; -import { shadeColor } from '$utils/shadeColor'; +import { isTooBright, isTooDark, isTooExtreme, shadeColor } from '$utils/shadeColor'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { PowerChip } from './PowerChip'; @@ -497,8 +497,8 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly 0) { - R = R <= 0 ? Math.max(178 - R, 0) : R; - G = G <= 0 ? Math.max(178 - G, 0) : G; - B = B <= 0 ? Math.max(178 - B, 0) : B; + 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'); @@ -19,3 +19,19 @@ export function shadeColor(initialColor: string, percent: number) { return `#${RR}${GG}${BB}`; } + +export function isTooDark(color?: string) { + if (!color) return false; + const R = parseInt(color.substring(1, 3), 16); + const G = parseInt(color.substring(3, 5), 16); + const B = parseInt(color.substring(5, 7), 16); + return (R + G + B) / 3 < 64; +} + +export function isTooBright(color?: string) { + if (!color) return false; + const R = parseInt(color.substring(1, 3), 16); + const G = parseInt(color.substring(3, 5), 16); + const B = parseInt(color.substring(5, 7), 16); + return (R + G + B) / 3 > 192; +} From c44f0ff7ff3b94b9a9cf3b96c7db771422e0b25b Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 03:09:12 +0300 Subject: [PATCH 13/19] small linting fix --- src/app/components/user-profile/UserRoomProfile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index f312ada46..e963f367f 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -48,7 +48,7 @@ import { filterPronounsByLanguage } from '$utils/pronouns'; import { useSetting } from '$state/hooks/settings'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; import { TextViewerContent } from '$components/text-viewer'; -import { isTooBright, isTooDark, isTooExtreme, shadeColor } from '$utils/shadeColor'; +import { isTooBright, isTooDark, shadeColor } from '$utils/shadeColor'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { PowerChip } from './PowerChip'; From abba98f725f95bfe5779e122a6d9b66d03b62e2f Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 16:30:27 +0300 Subject: [PATCH 14/19] add checking for name colors and hovering buttons with good styling --- .../components/user-profile/CreatorChip.tsx | 25 ++++++-- src/app/components/user-profile/PowerChip.tsx | 2 + src/app/components/user-profile/UserChips.tsx | 58 +++++++++++++++---- .../user-profile/UserRoomProfile.tsx | 11 +++- src/app/components/user-profile/styles.css.ts | 12 ++++ src/app/hooks/useUserProfile.ts | 8 ++- src/app/utils/shadeColor.ts | 18 +++++- 7 files changed, 114 insertions(+), 20 deletions(-) diff --git a/src/app/components/user-profile/CreatorChip.tsx b/src/app/components/user-profile/CreatorChip.tsx index f517581cc..896909f78 100644 --- a/src/app/components/user-profile/CreatorChip.tsx +++ b/src/app/components/user-profile/CreatorChip.tsx @@ -14,8 +14,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(); @@ -51,10 +62,12 @@ export function CreatorChip() { }} > -
+
{ @@ -78,8 +91,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 ba6440b2c..5dbf3a61a 100644 --- a/src/app/components/user-profile/PowerChip.tsx +++ b/src/app/components/user-profile/PowerChip.tsx @@ -285,6 +285,7 @@ export function PowerChip({ radii="300" aria-disabled={changing || !canChangePowers || !canAssignPower} aria-pressed={selected} + className={css.UserHeroMenuItem} style={{ backgroundColor: cardColor, color: textColor }} before={} after={ @@ -310,6 +311,7 @@ export function PowerChip({ fill="None" size="300" radii="300" + className={css.UserHeroMenuItem} style={{ backgroundColor: cardColor, color: textColor }} onClick={() => { if (room.isSpaceRoom()) { diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx index 01c12ef9a..16106fec3 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -110,7 +110,11 @@ export function ServerChip({ setCopied(); close(); }} - style={{ backgroundColor: cardColor, color: textColor }} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > Copy Server @@ -122,7 +126,11 @@ export function ServerChip({ navigate(getExploreServerPath(server)); closeProfile(); }} - style={{ backgroundColor: cardColor, color: textColor }} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > Explore Community @@ -139,7 +147,11 @@ export function ServerChip({ window.open(`https://${server}`, '_blank'); close(); }} - style={{ backgroundColor: cardColor, color: textColor }} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > Open in Browser @@ -220,7 +232,11 @@ export function ShareChip({ fill="None" size="300" radii="300" - style={{ backgroundColor: cardColor, color: textColor }} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} onClick={() => { copyToClipboard(userId); setCopied(); @@ -233,7 +249,11 @@ export function ShareChip({ fill="None" size="300" radii="300" - style={{ backgroundColor: cardColor, color: textColor }} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} onClick={() => { copyToClipboard(getMatrixToUser(userId)); setCopied(); @@ -358,7 +378,12 @@ export function MutualRoomsChip({ fill="None" size="300" radii="300" - style={{ paddingLeft: config.space.S100, backgroundColor: cardColor, color: textColor }} + className={css.UserHeroMenuItem} + style={{ + paddingLeft: config.space.S100, + backgroundColor: cardColor, + color: textColor, + }} onClick={() => { if (room.isSpaceRoom()) { navigateSpace(roomId); @@ -591,7 +616,7 @@ export function OptionsChip({ Nickname Save @@ -627,11 +656,15 @@ export function OptionsChip({ radii="300" variant="Critical" fill="None" + className={css.UserHeroMenuItem} onClick={() => { setNickname(userId, undefined); close(); }} - style={{ backgroundColor: cardColor, color: textColor }} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > Clear @@ -646,7 +679,11 @@ export function OptionsChip({ radii="300" before={} onClick={() => setEditingNick(true)} - style={{ backgroundColor: cardColor, color: textColor }} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > {currentNick ? 'Edit Nickname' : 'Set Nickname'} @@ -660,6 +697,7 @@ export function OptionsChip({ toggleIgnore(); close(); }} + className={css.UserHeroMenuItem} style={{ backgroundColor: cardColor }} before={ ignoring ? ( diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index e963f367f..70b65b34b 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -54,6 +54,7 @@ import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './ import { PowerChip } from './PowerChip'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; import { UserHero, UserHeroName } from './UserHero'; +import * as css from './styles.css'; const KNOWN_KEYS = [ 'moe.sable.app.bio', @@ -540,14 +541,13 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly} onClick={handleMessage} + className={css.UserHeroChip} style={{ marginLeft: 'auto', backgroundColor: backgroundColor !== color.Surface.Container ? cardColor : undefined, borderColor: backgroundColor, color: backgroundColor !== color.Surface.Container ? textColor : undefined, - borderStyle: 'solid', - borderWidth: '1px', }} > Message @@ -581,7 +581,12 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly {creator ? ( - + ) : ( 192; } + +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) < 200 && Math.abs(aG - bG) < 200 && Math.abs(aB - bB) < 200; +} From 2e6418ddcd2d3fea7b1bdfadf4c2cc3939ab2ff7 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 16:45:27 +0300 Subject: [PATCH 15/19] fix checks --- .../components/user-profile/UserRoomProfile.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 622b824c6..2e3de9174 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -1,6 +1,16 @@ - - -import { Box, Button, color, 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'; @@ -239,7 +249,6 @@ function UserExtendedSection({ ), [miscSelector, miscDataIndex, selectedUnknownField, showMisc, unknownFields, textColor] - ); return ( From 102b1c0c4bdb64df5ef34823e29f446eda9d3feb Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 16:54:27 +0300 Subject: [PATCH 16/19] fix more checks :smile: --- src/app/hooks/useSableCosmetics.ts | 3 ++- src/app/hooks/useUserProfile.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/hooks/useSableCosmetics.ts b/src/app/hooks/useSableCosmetics.ts index e757e7a69..40c80d128 100644 --- a/src/app/hooks/useSableCosmetics.ts +++ b/src/app/hooks/useSableCosmetics.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; -import { IContent, Room } from '$types/matrix-sdk'; +import type { IContent } from '$types/matrix-sdk'; +import { Room } from '$types/matrix-sdk'; import { usePowerLevels } from './usePowerLevels'; import { useRoomCreators } from './useRoomCreators'; import { useAccessiblePowerTagColors, useGetMemberPowerTag } from './useMemberPowerTag'; diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts index f05c8b393..04b9f5803 100644 --- a/src/app/hooks/useUserProfile.ts +++ b/src/app/hooks/useUserProfile.ts @@ -8,7 +8,7 @@ import colorMXID from '$utils/colorMXID'; import { profilesCacheAtom } from '$state/userRoomProfile'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; -import { MSC1767Text } from '$types/matrix/common'; +import type { MSC1767Text } from '$types/matrix/common'; import { areColorsTooSimilar, shadeColor } from '$utils/shadeColor'; import type { PronounSet } from '$utils/pronouns'; import { useMatrixClient } from './useMatrixClient'; From 386c6f7efdceb66ef372abf6a712c2bf40123338 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 16:56:23 +0300 Subject: [PATCH 17/19] fix more checks :smile::smile: --- src/app/hooks/useSableCosmetics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hooks/useSableCosmetics.ts b/src/app/hooks/useSableCosmetics.ts index 40c80d128..ad956fdca 100644 --- a/src/app/hooks/useSableCosmetics.ts +++ b/src/app/hooks/useSableCosmetics.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import type { IContent } from '$types/matrix-sdk'; -import { Room } from '$types/matrix-sdk'; +import type { Room } from '$types/matrix-sdk'; import { usePowerLevels } from './usePowerLevels'; import { useRoomCreators } from './useRoomCreators'; import { useAccessiblePowerTagColors, useGetMemberPowerTag } from './useMemberPowerTag'; From e472830327f4c2348d23c11931bb2595ff9b477f Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 18:46:13 +0300 Subject: [PATCH 18/19] removed non-required functions --- src/app/components/user-profile/UserHero.tsx | 6 +++--- .../user-profile/UserRoomProfile.tsx | 6 +++--- src/app/utils/shadeColor.ts | 18 +----------------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index c1f754983..9dc9c9c50 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -30,7 +30,7 @@ import { AvatarPresence, PresenceBadge } from '$components/presence'; import { UserAvatar } from '$components/user-avatar'; import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze'; import { useUserProfile } from '$hooks/useUserProfile'; -import { isTooBright, isTooDark, shadeColor } from '$utils/shadeColor'; +import { shadeColor, areColorsTooSimilar } from '$utils/shadeColor'; import * as css from './styles.css'; type UserHeroProps = { @@ -79,8 +79,8 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs const cardColor = shadeColor(backgroundColor, isBackgroundDark ? -80 : 80) ?? standardColors.Background.Container; const textColor = - ((fetchedBrightness === 'dark' || isTooDark(cardColor)) && '#FFFFFF') || - ((fetchedBrightness === 'light' || isTooBright(cardColor)) && '#000000') || + ((fetchedBrightness === 'dark' || areColorsTooSimilar('#000000', cardColor)) && '#FFFFFF') || + ((fetchedBrightness === 'light' || areColorsTooSimilar('#FFFFFF',cardColor)) && '#000000') || undefined; return ( diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 2e3de9174..5bbf98a64 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -50,7 +50,7 @@ import { filterPronounsByLanguage } from '$utils/pronouns'; import { useSetting } from '$state/hooks/settings'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; import { TextViewerContent } from '$components/text-viewer'; -import { isTooBright, isTooDark, shadeColor } from '$utils/shadeColor'; +import { areColorsTooSimilar, shadeColor } from '$utils/shadeColor'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { PowerChip } from './PowerChip'; @@ -507,8 +507,8 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly 192; -} - export function areColorsTooSimilar(colorA?: string, colorB?: string) { if (!colorA || !colorB) return false; @@ -47,5 +31,5 @@ export function areColorsTooSimilar(colorA?: string, colorB?: string) { const bG = parseInt(colorB.substring(3, 5), 16); const bB = parseInt(colorB.substring(5, 7), 16); - return Math.abs(aR - bR) < 200 && Math.abs(aG - bG) < 200 && Math.abs(aB - bB) < 200; + return Math.abs(aR - bR) < 32 && Math.abs(aG - bG) < 32 && Math.abs(aB - bB) < 32; } From 2fc17dbe5dfe5b435a7b2c31db16d8001987d25e Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 23 Apr 2026 18:50:48 +0300 Subject: [PATCH 19/19] fixed formatting --- src/app/components/user-profile/UserHero.tsx | 2 +- src/app/components/user-profile/UserRoomProfile.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 9dc9c9c50..bb4acc103 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -80,7 +80,7 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs shadeColor(backgroundColor, isBackgroundDark ? -80 : 80) ?? standardColors.Background.Container; const textColor = ((fetchedBrightness === 'dark' || areColorsTooSimilar('#000000', cardColor)) && '#FFFFFF') || - ((fetchedBrightness === 'light' || areColorsTooSimilar('#FFFFFF',cardColor)) && '#000000') || + ((fetchedBrightness === 'light' || areColorsTooSimilar('#FFFFFF', cardColor)) && '#000000') || undefined; return ( diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 5bbf98a64..8fb3ac253 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -508,7 +508,7 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly