feat: enhance user profile customization with avatar border color and gradient options
- Added support for setting avatar border color and gradient in user profile settings. - Introduced new hooks for managing user profile styles and fetching metadata from avatars. - Implemented an AngleSelector component for selecting gradient direction. - Updated RoomNavItem to display parent space information for direct messages. - Improved caching mechanism for user banners and profile styles. - Refactored image metadata handling to include new profile style attributes.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { MouseEventHandler, forwardRef, useState } from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { Room, EventTimeline } from 'matrix-js-sdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -16,13 +17,15 @@ import {
|
||||
RectCords,
|
||||
Badge,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
} from 'folds';
|
||||
import { useFocusWithin, useHover } from 'react-aria';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
||||
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
|
||||
import { getDirectRoomAvatarUrl, getRoomAvatarUrl, guessPerfectParent, getOrphanParents } from '../../utils/room';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useRoomUnread } from '../../state/hooks/unread';
|
||||
@@ -36,7 +39,7 @@ import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
||||
import { TypingIndicator } from '../../components/typing-indicator';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
||||
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
|
||||
import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { getViaServers } from '../../plugins/via-servers';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
@@ -54,6 +57,9 @@ import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||
import { CallParticipantsIndicator } from './CallParticipantsIndicator';
|
||||
import { useCall } from '../call/useCall';
|
||||
import { CallType } from '../call/types';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
|
||||
type RoomNavItemMenuProps = {
|
||||
room: Room;
|
||||
@@ -273,7 +279,31 @@ export function RoomNavItem({
|
||||
const typingMember = useRoomTypingMember(room.roomId).filter(
|
||||
(receipt) => receipt.userId !== mx.getUserId()
|
||||
);
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
// Get parent space for DMs
|
||||
const parentSpaceInfo = (() => {
|
||||
if (!direct || !mDirects.has(room.roomId)) return undefined;
|
||||
|
||||
const orphanParents = getOrphanParents(roomToParents, room.roomId);
|
||||
if (orphanParents.length === 0) return undefined;
|
||||
|
||||
const parentSpaceId = guessPerfectParent(mx, room.roomId, orphanParents) ?? orphanParents[0];
|
||||
const parentSpace = mx.getRoom(parentSpaceId);
|
||||
if (!parentSpace) return undefined;
|
||||
|
||||
const avatarEvent = parentSpace.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(StateEvent.RoomAvatar, '');
|
||||
const avatarContent = avatarEvent?.getContent();
|
||||
const avatarMxc = avatarContent && typeof avatarContent.url === 'string' ? avatarContent.url : undefined;
|
||||
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
|
||||
|
||||
return {
|
||||
name: parentSpace.name,
|
||||
avatarUrl,
|
||||
roomId: parentSpace.roomId,
|
||||
};
|
||||
})();
|
||||
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
setMenuAnchor({
|
||||
@@ -353,43 +383,72 @@ export function RoomNavItem({
|
||||
</NavLink>
|
||||
{optionsVisible && (
|
||||
<NavItemOptions>
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
offset={menuAnchor?.width === 0 ? 0 : undefined}
|
||||
alignOffset={menuAnchor?.width === 0 ? 0 : -5}
|
||||
position="Bottom"
|
||||
align={menuAnchor?.width === 0 ? 'Start' : 'End'}
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => setMenuAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
<Box alignItems="Center" gap="100">
|
||||
{parentSpaceInfo && (
|
||||
<TooltipProvider
|
||||
position="Top"
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text size="T200">Space: {parentSpaceInfo.name}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<RoomNavItemMenu
|
||||
room={room}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
notificationMode={notificationMode}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleOpenMenu}
|
||||
aria-pressed={!!menuAnchor}
|
||||
variant="Background"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="300"
|
||||
>
|
||||
<Icon size="50" src={Icons.VerticalDots} />
|
||||
</IconButton>
|
||||
</PopOut>
|
||||
{(triggerRef) => (
|
||||
<Avatar ref={triggerRef} size="200" radii="Pill" style={{ flexShrink: 0, width: '16px', height: '16px' }}>
|
||||
<RoomAvatar
|
||||
roomId={parentSpaceInfo.roomId}
|
||||
src={parentSpaceInfo.avatarUrl}
|
||||
alt={parentSpaceInfo.name}
|
||||
renderFallback={() => (
|
||||
<Text size="Inherit">
|
||||
{nameInitials(parentSpaceInfo.name, 1)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{optionsVisible && (
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
offset={menuAnchor?.width === 0 ? 0 : undefined}
|
||||
alignOffset={menuAnchor?.width === 0 ? 0 : -5}
|
||||
position="Bottom"
|
||||
align={menuAnchor?.width === 0 ? 'Start' : 'End'}
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => setMenuAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<RoomNavItemMenu
|
||||
room={room}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
notificationMode={notificationMode}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleOpenMenu}
|
||||
aria-pressed={!!menuAnchor}
|
||||
variant="Background"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="300"
|
||||
>
|
||||
<Icon size="50" src={Icons.VerticalDots} />
|
||||
</IconButton>
|
||||
</PopOut>
|
||||
)}
|
||||
</Box>
|
||||
</NavItemOptions>
|
||||
)}
|
||||
</NavItem>
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
color,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { HexColorPicker, RgbaColorPicker, RgbaColor } from 'react-colorful';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
||||
@@ -48,8 +48,10 @@ import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||
import { useCapabilities } from '../../../hooks/useCapabilities';
|
||||
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
|
||||
import { AngleSelector } from '../../../components/AngleSelector';
|
||||
import { useUserColor, useOtherUserColor } from '../../../hooks/useUserColor';
|
||||
import { useUserBanner, useOtherUserBanner } from '../../../hooks/useUserBanner';
|
||||
import { useUserProfileStyle } from '../../../hooks/useUserProfileStyle';
|
||||
import { useUserPresence } from '../../../hooks/useUserPresence';
|
||||
import { AvatarPresence, PresenceBadge } from '../../../components/presence';
|
||||
import { BreakWord, LineClamp3 } from '../../../styles/Text.css';
|
||||
@@ -146,6 +148,134 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
||||
setSavingColor(false);
|
||||
};
|
||||
|
||||
// Profile style settings (border color and gradient)
|
||||
const [profileStyle, setProfileStyle, styleLoading] = useUserProfileStyle();
|
||||
// Initialize with transparent alpha so saved values show in preview until user edits
|
||||
const [localBorderColor, setLocalBorderColor] = useState<RgbaColor>({ r: 59, g: 130, b: 246, a: 0 });
|
||||
const [localGradientStart, setLocalGradientStart] = useState<RgbaColor>({ r: 0, g: 0, b: 0, a: 0 });
|
||||
const [localGradientStop, setLocalGradientStop] = useState<RgbaColor>({ r: 0, g: 0, b: 0, a: 0 });
|
||||
const [localGradientDirection, setLocalGradientDirection] = useState(180); // degrees (180 = top to bottom)
|
||||
const [savingStyle, setSavingStyle] = useState(false);
|
||||
const [styleError, setStyleError] = useState<string>();
|
||||
// Track if user has started editing (to show local values in preview)
|
||||
const [editingBorder, setEditingBorder] = useState(false);
|
||||
const [editingGradient, setEditingGradient] = useState(false);
|
||||
|
||||
// Helper to convert RGBA to hex with alpha (#RRGGBBAA)
|
||||
const rgbaToHex = (rgba: RgbaColor): string => {
|
||||
const r = rgba.r.toString(16).padStart(2, '0');
|
||||
const g = rgba.g.toString(16).padStart(2, '0');
|
||||
const b = rgba.b.toString(16).padStart(2, '0');
|
||||
const a = Math.round(rgba.a * 255).toString(16).padStart(2, '0');
|
||||
return `#${r}${g}${b}${a}`;
|
||||
};
|
||||
|
||||
// Helper to convert hex with alpha to RGBA
|
||||
const hexToRgba = (hex: string): RgbaColor => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex);
|
||||
if (result) {
|
||||
return {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
a: result[4] ? parseInt(result[4], 16) / 255 : 1,
|
||||
};
|
||||
}
|
||||
return { r: 59, g: 130, b: 246, a: 1 };
|
||||
};
|
||||
|
||||
// Helper to parse degrees from direction string (e.g., "180deg" -> 180)
|
||||
const parseDirectionDegrees = (direction: string): number => {
|
||||
const degMatch = direction.match(/^(\d+)deg$/i);
|
||||
if (degMatch) return parseInt(degMatch[1], 10);
|
||||
// Fallback for legacy "to X" format
|
||||
const keywordMap: Record<string, number> = {
|
||||
'to top': 0,
|
||||
'to top right': 45,
|
||||
'to right': 90,
|
||||
'to bottom right': 135,
|
||||
'to bottom': 180,
|
||||
'to bottom left': 225,
|
||||
'to left': 270,
|
||||
'to top left': 315,
|
||||
};
|
||||
return keywordMap[direction.toLowerCase()] ?? 180;
|
||||
};
|
||||
|
||||
// Sync local style state with loaded profile style
|
||||
useEffect(() => {
|
||||
if (profileStyle.avatarBorderColor) {
|
||||
setLocalBorderColor(hexToRgba(profileStyle.avatarBorderColor));
|
||||
}
|
||||
if (profileStyle.gradient) {
|
||||
setLocalGradientStart(hexToRgba(profileStyle.gradient.startColor));
|
||||
setLocalGradientStop(hexToRgba(profileStyle.gradient.stopColor));
|
||||
setLocalGradientDirection(parseDirectionDegrees(profileStyle.gradient.direction));
|
||||
}
|
||||
}, [profileStyle]);
|
||||
|
||||
const handleBorderColorSave = async () => {
|
||||
setSavingStyle(true);
|
||||
setStyleError(undefined);
|
||||
try {
|
||||
await setProfileStyle({ avatarBorderColor: rgbaToHex(localBorderColor) });
|
||||
await syncUserAvatar();
|
||||
setEditingBorder(false);
|
||||
} catch (e) {
|
||||
setStyleError('Failed to save border color');
|
||||
}
|
||||
setSavingStyle(false);
|
||||
};
|
||||
|
||||
const handleBorderColorRemove = async () => {
|
||||
setSavingStyle(true);
|
||||
setStyleError(undefined);
|
||||
try {
|
||||
await setProfileStyle({ avatarBorderColor: undefined });
|
||||
await syncUserAvatar();
|
||||
setEditingBorder(false);
|
||||
setLocalBorderColor({ r: 59, g: 130, b: 246, a: 0 });
|
||||
} catch (e) {
|
||||
setStyleError('Failed to remove border color');
|
||||
}
|
||||
setSavingStyle(false);
|
||||
};
|
||||
|
||||
const handleGradientSave = async () => {
|
||||
setSavingStyle(true);
|
||||
setStyleError(undefined);
|
||||
try {
|
||||
await setProfileStyle({
|
||||
gradient: {
|
||||
direction: `${localGradientDirection}deg`,
|
||||
startColor: rgbaToHex(localGradientStart),
|
||||
stopColor: rgbaToHex(localGradientStop),
|
||||
},
|
||||
});
|
||||
await syncUserAvatar();
|
||||
setEditingGradient(false);
|
||||
} catch (e) {
|
||||
setStyleError('Failed to save gradient');
|
||||
}
|
||||
setSavingStyle(false);
|
||||
};
|
||||
|
||||
const handleGradientRemove = async () => {
|
||||
setSavingStyle(true);
|
||||
setStyleError(undefined);
|
||||
try {
|
||||
await setProfileStyle({ gradient: undefined });
|
||||
await syncUserAvatar();
|
||||
setEditingGradient(false);
|
||||
setLocalGradientStart({ r: 0, g: 0, b: 0, a: 0 });
|
||||
setLocalGradientStop({ r: 0, g: 0, b: 0, a: 0 });
|
||||
setLocalGradientDirection(180);
|
||||
} catch (e) {
|
||||
setStyleError('Failed to remove gradient');
|
||||
}
|
||||
setSavingStyle(false);
|
||||
};
|
||||
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [editedName, setEditedName] = useState(profile.displayName || '');
|
||||
const [savingName, setSavingName] = useState(false);
|
||||
@@ -419,6 +549,20 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
// Build gradient CSS for preview (shows local editing values when user is editing,
|
||||
// otherwise falls back to saved profile style)
|
||||
const previewGradient = editingGradient
|
||||
? `linear-gradient(${localGradientDirection}deg, ${rgbaToHex(localGradientStart)}, ${rgbaToHex(localGradientStop)})`
|
||||
: profileStyle.gradient
|
||||
? `linear-gradient(${profileStyle.gradient.direction}, ${profileStyle.gradient.startColor}, ${profileStyle.gradient.stopColor})`
|
||||
: undefined;
|
||||
|
||||
// Build border color for preview (shows local editing value when user is editing,
|
||||
// otherwise falls back to saved profile style)
|
||||
const previewBorderColor = editingBorder
|
||||
? rgbaToHex(localBorderColor)
|
||||
: profileStyle.avatarBorderColor;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="300">
|
||||
<Box
|
||||
@@ -641,6 +785,7 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
||||
paddingLeft: config.space.S400,
|
||||
paddingRight: config.space.S400,
|
||||
paddingBottom: config.space.S400,
|
||||
background: previewGradient,
|
||||
}}
|
||||
>
|
||||
{/* Avatar */}
|
||||
@@ -661,7 +806,7 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
||||
as="button"
|
||||
onClick={handleAvatarClick}
|
||||
style={{
|
||||
backgroundColor: color.Surface.Container,
|
||||
background: previewGradient || color.Surface.Container,
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
cursor: 'pointer',
|
||||
@@ -678,7 +823,9 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
||||
<Avatar
|
||||
size="500"
|
||||
style={{
|
||||
outline: `${config.borderWidth.B600} solid ${color.Surface.Container}`,
|
||||
outline: previewBorderColor
|
||||
? `${toRem(4)} solid ${previewBorderColor}`
|
||||
: `${config.borderWidth.B600} solid ${color.Surface.Container}`,
|
||||
}}
|
||||
>
|
||||
<UserAvatar
|
||||
@@ -885,6 +1032,126 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Profile Style Settings */}
|
||||
<Box
|
||||
direction="Column"
|
||||
gap="200"
|
||||
style={{
|
||||
padding: config.space.S300,
|
||||
border: `${toRem(1)} solid ${color.Surface.ContainerLine}`,
|
||||
borderRadius: toRem(8),
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Text size="H6">Profile Style</Text>
|
||||
|
||||
{/* Avatar Border Color */}
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="T300">Avatar Border Color</Text>
|
||||
<Box gap="200" alignItems="Center" wrap="Wrap">
|
||||
<RgbaColorPicker color={localBorderColor} onChange={(c) => { setLocalBorderColor(c); setEditingBorder(true); }} />
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="T200" style={{ opacity: 0.7 }}>
|
||||
Preview: {rgbaToHex(localBorderColor)}
|
||||
</Text>
|
||||
<Box
|
||||
style={{
|
||||
width: toRem(48),
|
||||
height: toRem(48),
|
||||
borderRadius: '50%',
|
||||
border: `${toRem(4)} solid ${rgbaToHex(localBorderColor)}`,
|
||||
backgroundColor: color.Surface.Container,
|
||||
}}
|
||||
/>
|
||||
<Box gap="100">
|
||||
<Button
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={handleBorderColorSave}
|
||||
disabled={savingStyle}
|
||||
>
|
||||
<Text size="B300">Save</Text>
|
||||
</Button>
|
||||
{profileStyle.avatarBorderColor && (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={handleBorderColorRemove}
|
||||
disabled={savingStyle}
|
||||
>
|
||||
<Text size="B300">Remove</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Profile Gradient */}
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="T300">Profile Card Gradient</Text>
|
||||
<Box gap="200" alignItems="Start" wrap="Wrap">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="T200">Start Color</Text>
|
||||
<RgbaColorPicker color={localGradientStart} onChange={(c) => { setLocalGradientStart(c); setEditingGradient(true); }} />
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="T200">End Color</Text>
|
||||
<RgbaColorPicker color={localGradientStop} onChange={(c) => { setLocalGradientStop(c); setEditingGradient(true); }} />
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="T200">Direction</Text>
|
||||
<AngleSelector
|
||||
value={localGradientDirection}
|
||||
onChange={(deg) => { setLocalGradientDirection(deg); setEditingGradient(true); }}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
width: toRem(100),
|
||||
height: toRem(60),
|
||||
borderRadius: toRem(8),
|
||||
background: `linear-gradient(${localGradientDirection}deg, ${rgbaToHex(localGradientStart)}, ${rgbaToHex(localGradientStop)})`,
|
||||
border: `${toRem(1)} solid ${color.Surface.ContainerLine}`,
|
||||
}}
|
||||
/>
|
||||
<Box gap="100">
|
||||
<Button
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={handleGradientSave}
|
||||
disabled={savingStyle}
|
||||
>
|
||||
<Text size="B300">Save</Text>
|
||||
</Button>
|
||||
{profileStyle.gradient && (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={handleGradientRemove}
|
||||
disabled={savingStyle}
|
||||
>
|
||||
<Text size="B300">Remove</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{styleError && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>{styleError}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{uploadAtom && (
|
||||
<Box gap="200" direction="Column" style={{ width: '100%' }}>
|
||||
<CompactUploadCardRenderer
|
||||
|
||||
Reference in New Issue
Block a user