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 s = corner.includes('s');
const e2 = corner.includes('e'); const e2 = corner.includes('e');
const dots = [ const dots = [
[2, 2], [3, 3],
[2, 7], [3, 10],
[7, 2], [10, 3],
].map(([a, b]) => ({ ].map(([a, b]) => ({
position: 'absolute' as const, position: 'absolute' as const,
width: 4, width: 5,
height: 4, height: 5,
borderRadius: '50%', 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, [s ? 'bottom' : 'top']: a,
[e2 ? 'right' : 'left']: b, [e2 ? 'right' : 'left']: b,
})); }));
@@ -756,8 +757,8 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
onClick={(ev) => ev.stopPropagation()} onClick={(ev) => ev.stopPropagation()}
style={{ style={{
position: 'absolute', position: 'absolute',
width: '18px', width: '24px',
height: '18px', height: '24px',
[s ? 'bottom' : 'top']: 0, [s ? 'bottom' : 'top']: 0,
[e2 ? 'right' : 'left']: 0, [e2 ? 'right' : 'left']: 0,
cursor: `${corner}-resize`, cursor: `${corner}-resize`,
@@ -62,6 +62,7 @@ export function ReadReceiptAvatars({
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
title={tooltipNames} title={tooltipNames}
aria-label={tooltipNames} aria-label={tooltipNames}
className="receipt-pill-btn"
style={{ style={{
background: 'none', background: 'none',
border: 'none', border: 'none',
@@ -72,6 +73,16 @@ export function ReadReceiptAvatars({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', 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 */} {/* 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', 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', boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
borderRadius: '999px', borderRadius: '999px',
padding: '2px 6px 2px 2px', padding: '2px 6px',
gap: '0px', gap: '0px',
}} }}
> >
+15 -1
View File
@@ -218,7 +218,21 @@ export function CallControls({ callEmbed }: CallControlsProps) {
fontFamily: 'JetBrains Mono, monospace', 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> </Text>
</Box> </Box>
) : ( ) : (
+1 -1
View File
@@ -879,7 +879,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
... ...
</Text> </Text>
) : ( ) : (
<Icon src={Icons.Pin} size="100" /> <Icon src={Icons.SpaceGlobe} size="100" />
)} )}
</IconButton> </IconButton>
<VoiceMessageRecorder onSend={handleVoiceSend} /> <VoiceMessageRecorder onSend={handleVoiceSend} />
@@ -162,7 +162,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
<Text size="T300"> Forwarded to {sentTo}</Text> <Text size="T300"> Forwarded to {sentTo}</Text>
</Box> </Box>
) : ( ) : (
<Box grow="Yes" style={{ minHeight: 0 }}> <Box grow="Yes" style={{ minHeight: 0, position: 'relative' }}>
<Scroll size="300" hideTrack visibility="Hover"> <Scroll size="300" hideTrack visibility="Hover">
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
{filtered.slice(0, 60).map((room) => ( {filtered.slice(0, 60).map((room) => (
@@ -195,7 +195,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
style={{ style={{
position: 'absolute', position: 'absolute',
inset: 0, inset: 0,
background: 'rgba(0,0,0,0.18)', background: 'rgba(0,0,0,0.35)',
borderRadius: config.radii.R500, borderRadius: config.radii.R500,
}} }}
> >
+10 -1
View File
@@ -58,6 +58,7 @@ import { MessageLayout, MessageSpacing } from '../../../state/settings';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji'; import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
import * as css from './styles.css'; import * as css from './styles.css';
import { SendingSpinClass } from '../../../styles/Animations.css';
import { EventReaders } from '../../../components/event-readers'; import { EventReaders } from '../../../components/event-readers';
import { ReadReceiptAvatars } from '../../../components/read-receipt-avatars'; import { ReadReceiptAvatars } from '../../../components/read-receipt-avatars';
import { useReadPositions } from '../ReadPositionsContext'; import { useReadPositions } from '../ReadPositionsContext';
@@ -127,7 +128,15 @@ function DeliveryStatus({
: {}), : {}),
}} }}
> >
{icon} <span
className={
status === EventStatus.SENDING || status === EventStatus.ENCRYPTING
? SendingSpinClass
: undefined
}
>
{icon}
</span>
</Box> </Box>
); );
} }
+11
View File
@@ -1,6 +1,11 @@
import { keyframes, style } from '@vanilla-extract/css'; import { keyframes, style } from '@vanilla-extract/css';
import { color, toRem } from 'folds'; import { color, toRem } from 'folds';
const spin = keyframes({
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
});
const wobble = keyframes({ const wobble = keyframes({
'0%': { '0%': {
transform: 'translateX(0) rotateZ(0deg)', transform: 'translateX(0) rotateZ(0deg)',
@@ -45,3 +50,9 @@ export const CallAvatarAnimation = style({
animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`, animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`,
animationIterationCount: 'infinite', 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; 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 */ /* Fix Firefox rendering lists that have empty items with those items collapsed in on eachother */
li p::before { li p::before {
content: ''; content: '';
+2 -2
View File
@@ -65,7 +65,7 @@ export function runLotusBootSequence(force = false): void {
'bottom:1.5rem', 'bottom:1.5rem',
'right:2rem', 'right:2rem',
'font-size:0.68rem', '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", "font-family:'JetBrains Mono','Courier New',monospace",
'letter-spacing:0.08em', 'letter-spacing:0.08em',
'user-select:none', 'user-select:none',
@@ -105,5 +105,5 @@ export function runLotusBootSequence(force = false): void {
text += `${BOOT_MESSAGES[i]}\n`; text += `${BOOT_MESSAGES[i]}\n`;
pre.textContent = text; pre.textContent = text;
i++; i++;
}, 65); }, 45);
} }