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:
@@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user