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