ui: visual polish — animations, icons, and interaction improvements

- Spin animation on ⟳ delivery status during SENDING/ENCRYPTING states
- Pulsing ● dot on PTT LIVE badge (pttLivePulse keyframe)
- Read receipt pill: hover scale/opacity transition, symmetric padding
- PiP resize handles: larger dots (5px), wider hit area (24px), higher contrast
- ForwardMessageDialog: position:relative on scroll container, spinner overlay 0.35 opacity
- Boot sequence: 45ms interval (was 65ms), brighter ESC hint (0.55 opacity)
- Location button: Icons.Pin → Icons.SpaceGlobe (globe icon)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 23:52:58 -04:00
parent bf8fa85055
commit 97b335773b
9 changed files with 72 additions and 16 deletions
+9 -8
View File
@@ -737,15 +737,16 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
const s = corner.includes('s');
const e2 = corner.includes('e');
const dots = [
[2, 2],
[2, 7],
[7, 2],
[3, 3],
[3, 10],
[10, 3],
].map(([a, b]) => ({
position: 'absolute' as const,
width: 4,
height: 4,
width: 5,
height: 5,
borderRadius: '50%',
background: 'rgba(255,255,255,0.45)',
background: 'rgba(255,255,255,0.65)',
boxShadow: '0 0 3px rgba(0,0,0,0.4)',
[s ? 'bottom' : 'top']: a,
[e2 ? 'right' : 'left']: b,
}));
@@ -756,8 +757,8 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
onClick={(ev) => ev.stopPropagation()}
style={{
position: 'absolute',
width: '18px',
height: '18px',
width: '24px',
height: '24px',
[s ? 'bottom' : 'top']: 0,
[e2 ? 'right' : 'left']: 0,
cursor: `${corner}-resize`,
@@ -62,6 +62,7 @@ export function ReadReceiptAvatars({
onClick={() => setOpen(true)}
title={tooltipNames}
aria-label={tooltipNames}
className="receipt-pill-btn"
style={{
background: 'none',
border: 'none',
@@ -72,6 +73,16 @@ export function ReadReceiptAvatars({
display: 'flex',
alignItems: 'center',
gap: '4px',
opacity: 0.85,
transition: 'opacity 0.15s, transform 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'scale(1.04)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = '0.85';
e.currentTarget.style.transform = 'scale(1)';
}}
>
{/* Pill wrapper ensures visibility on any wallpaper/background */}
@@ -85,7 +96,7 @@ export function ReadReceiptAvatars({
border: lotusTerminal ? '1px solid rgba(0,212,255,0.30)' : '1px solid transparent',
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
borderRadius: '999px',
padding: '2px 6px 2px 2px',
padding: '2px 6px',
gap: '0px',
}}
>
+15 -1
View File
@@ -218,7 +218,21 @@ export function CallControls({ callEmbed }: CallControlsProps) {
fontFamily: 'JetBrains Mono, monospace',
}}
>
{pttActive ? '● LIVE' : `PTT — Hold ${pttKeyLabel}`}
{pttActive ? (
<>
<span
style={{
display: 'inline-block',
animation: 'pttLivePulse 900ms ease-in-out infinite',
}}
>
</span>
{' LIVE'}
</>
) : (
`PTT — Hold ${pttKeyLabel}`
)}
</Text>
</Box>
) : (
+1 -1
View File
@@ -879,7 +879,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
...
</Text>
) : (
<Icon src={Icons.Pin} size="100" />
<Icon src={Icons.SpaceGlobe} size="100" />
)}
</IconButton>
<VoiceMessageRecorder onSend={handleVoiceSend} />
@@ -162,7 +162,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
<Text size="T300"> Forwarded to {sentTo}</Text>
</Box>
) : (
<Box grow="Yes" style={{ minHeight: 0 }}>
<Box grow="Yes" style={{ minHeight: 0, position: 'relative' }}>
<Scroll size="300" hideTrack visibility="Hover">
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{filtered.slice(0, 60).map((room) => (
@@ -195,7 +195,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0,0,0,0.18)',
background: 'rgba(0,0,0,0.35)',
borderRadius: config.radii.R500,
}}
>
+10 -1
View File
@@ -58,6 +58,7 @@ import { MessageLayout, MessageSpacing } from '../../../state/settings';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
import * as css from './styles.css';
import { SendingSpinClass } from '../../../styles/Animations.css';
import { EventReaders } from '../../../components/event-readers';
import { ReadReceiptAvatars } from '../../../components/read-receipt-avatars';
import { useReadPositions } from '../ReadPositionsContext';
@@ -127,7 +128,15 @@ function DeliveryStatus({
: {}),
}}
>
{icon}
<span
className={
status === EventStatus.SENDING || status === EventStatus.ENCRYPTING
? SendingSpinClass
: undefined
}
>
{icon}
</span>
</Box>
);
}
+11
View File
@@ -1,6 +1,11 @@
import { keyframes, style } from '@vanilla-extract/css';
import { color, toRem } from 'folds';
const spin = keyframes({
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
});
const wobble = keyframes({
'0%': {
transform: 'translateX(0) rotateZ(0deg)',
@@ -45,3 +50,9 @@ export const CallAvatarAnimation = style({
animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`,
animationIterationCount: 'infinite',
});
export const SendingSpinClass = style({
display: 'inline-block',
animation: `${spin} 900ms linear infinite`,
transformOrigin: 'center',
});
+10
View File
@@ -156,6 +156,16 @@ audio:not([controls]) {
display: none !important;
}
@keyframes pttLivePulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.25;
}
}
/* Fix Firefox rendering lists that have empty items with those items collapsed in on eachother */
li p::before {
content: '';
+2 -2
View File
@@ -65,7 +65,7 @@ export function runLotusBootSequence(force = false): void {
'bottom:1.5rem',
'right:2rem',
'font-size:0.68rem',
'color:rgba(0,255,136,0.35)',
'color:rgba(0,255,136,0.55)',
"font-family:'JetBrains Mono','Courier New',monospace",
'letter-spacing:0.08em',
'user-select:none',
@@ -105,5 +105,5 @@ export function runLotusBootSequence(force = false): void {
text += `${BOOT_MESSAGES[i]}\n`;
pre.textContent = text;
i++;
}, 65);
}, 45);
}