fix(mobile): stack embed cards + secondary 44px touch targets (r2)
Mobile follow-ups round 2 (survey findings deferred from the mobile audit), reviewed by 2 agents on the staged diff (both SHIP). - URL-preview cards: the Twitch / Twitter / TikTok-fallback cards render their thumbnail/header BESIDE the content as direct children of the UrlPreview flex row, which squeezes both on a phone. Add `StackOnMobile` (@media max-width:750px -> flex-direction:column) scoped to those variants via cardClass. folds Box has no default `direction`, so the override wins uncontested; desktop (>750px) is unchanged. No-op for the single-column embed cards (MediaEmbedCard/TikTokEmbedCard). - 44px touch targets (MobileTouchTarget, @media max-width:750px) on the otherwise ~28px controls: embed-player Close/Collapse/Fullscreen/View-post buttons; image-viewer close/zoom/download; the read-receipt "seen by" pill. Deferred (rationale, not built): PiP resize handles + fullscreen button — enlarging four 24px corners to 44px would swallow a ~160px mobile PiP and block "Return to call"; presence dot is a non-interactive status indicator. Gates: tsc 0, eslint 0, prettier clean, 856/856 tests, build ok. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import * as css from './ImageViewer.css';
|
||||
import { useZoom } from '../../hooks/useZoom';
|
||||
import { usePan } from '../../hooks/usePan';
|
||||
import { downloadMedia } from '../../utils/matrix';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
|
||||
export type ImageViewerProps = {
|
||||
alt: string;
|
||||
@@ -35,7 +36,13 @@ export const ImageViewer = as<'div', ImageViewerProps>(
|
||||
>
|
||||
<Header className={css.ImageViewerHeader} size="400">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<IconButton size="300" radii="300" onClick={requestClose} aria-label="Close">
|
||||
<IconButton
|
||||
size="300"
|
||||
radii="300"
|
||||
className={MobileTouchTarget}
|
||||
onClick={requestClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon size="50" src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
<Text size="T300" truncate>
|
||||
@@ -48,12 +55,18 @@ export const ImageViewer = as<'div', ImageViewerProps>(
|
||||
outlined={zoom < 1}
|
||||
size="300"
|
||||
radii="Pill"
|
||||
className={MobileTouchTarget}
|
||||
onClick={zoomOut}
|
||||
aria-label="Zoom Out"
|
||||
>
|
||||
<Icon size="50" src={Icons.Minus} />
|
||||
</IconButton>
|
||||
<Chip variant="SurfaceVariant" radii="Pill" onClick={() => setZoom(zoom === 1 ? 2 : 1)}>
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => setZoom(zoom === 1 ? 2 : 1)}
|
||||
>
|
||||
<Text size="B300">{Math.round(zoom * 100)}%</Text>
|
||||
</Chip>
|
||||
<IconButton
|
||||
@@ -61,6 +74,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
|
||||
outlined={zoom > 1}
|
||||
size="300"
|
||||
radii="Pill"
|
||||
className={MobileTouchTarget}
|
||||
onClick={zoomIn}
|
||||
aria-label="Zoom In"
|
||||
>
|
||||
@@ -70,6 +84,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
|
||||
variant="Primary"
|
||||
onClick={handleDownload}
|
||||
radii="300"
|
||||
className={MobileTouchTarget}
|
||||
before={<Icon size="50" src={Icons.Download} />}
|
||||
>
|
||||
<Text size="B300">{t('Organisms.ImageViewer.download')}</Text>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { stopPropagation } from '../../utils/keyboard';
|
||||
import { useModalStyle } from '../../hooks/useModalStyle';
|
||||
import { useMemberAvatar } from '../../hooks/useMemberAvatar';
|
||||
import { useRoomMembersChange } from '../../hooks/useRoomMemberChange';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import * as css from './ReadReceiptAvatars.css';
|
||||
|
||||
const MAX_DISPLAY = 5;
|
||||
@@ -92,7 +93,7 @@ export function ReadReceiptAvatars({
|
||||
onClick={() => setOpen(true)}
|
||||
title={tooltipNames}
|
||||
aria-label={tooltipNames}
|
||||
className={css.ReceiptTrigger}
|
||||
className={`${css.ReceiptTrigger} ${MobileTouchTarget}`}
|
||||
>
|
||||
{/* Pill wrapper ensures visibility on any wallpaper/background */}
|
||||
<span
|
||||
|
||||
@@ -23,6 +23,19 @@ export const UrlPreviewWide = style({
|
||||
width: 'min(38rem, 94vw)',
|
||||
});
|
||||
|
||||
// The Twitch/Twitter/TikTok-fallback cards lay their header/thumbnail out BESIDE
|
||||
// the content as direct children of the UrlPreview flex row; on a phone that
|
||||
// squeezes both. Stack them vertically on narrow viewports only. `UrlPreview`'s
|
||||
// Box has no explicit direction (browser default row), so this override wins
|
||||
// with nothing to compete against, and desktop (>750px) is unchanged.
|
||||
export const StackOnMobile = style({
|
||||
'@media': {
|
||||
'(max-width: 750px)': {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const UrlPreviewImg = style([
|
||||
DefaultReset,
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
config,
|
||||
} from 'folds';
|
||||
import { ImageOverlay } from '../ImageOverlay';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
|
||||
@@ -726,6 +727,7 @@ function TwitterCard({
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="SurfaceVariant"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => setExpanded(false)}
|
||||
aria-label="Collapse post"
|
||||
>
|
||||
@@ -780,6 +782,7 @@ function TwitterCard({
|
||||
<Chip
|
||||
variant="Secondary"
|
||||
radii="Pill"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => setExpanded(true)}
|
||||
before={<Icon size="50" src={Icons.Play} />}
|
||||
>
|
||||
@@ -1249,6 +1252,7 @@ function MediaEmbedCard({
|
||||
<Chip
|
||||
variant="Secondary"
|
||||
radii="Pill"
|
||||
className={MobileTouchTarget}
|
||||
onClick={enterFullscreen}
|
||||
aria-label="Fullscreen"
|
||||
>
|
||||
@@ -1259,6 +1263,7 @@ function MediaEmbedCard({
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="SurfaceVariant"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => setPlaying(false)}
|
||||
aria-label="Close player"
|
||||
>
|
||||
@@ -1407,6 +1412,7 @@ function TikTokEmbedCard({ url, prev }: { url: string; prev: IPreviewUrlResponse
|
||||
<Chip
|
||||
variant="Secondary"
|
||||
radii="Pill"
|
||||
className={MobileTouchTarget}
|
||||
onClick={enterFullscreen}
|
||||
aria-label="Fullscreen"
|
||||
>
|
||||
@@ -1416,6 +1422,7 @@ function TikTokEmbedCard({ url, prev }: { url: string; prev: IPreviewUrlResponse
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="SurfaceVariant"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => setPlaying(false)}
|
||||
aria-label="Close player"
|
||||
>
|
||||
@@ -2069,7 +2076,14 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
||||
// player chrome / tweet content isn't cramped or clipped.
|
||||
const embed = parseMediaEmbed(url, window.location.hostname);
|
||||
const wide = !!embed || isTwitterTweet(url);
|
||||
const cardClass = wide ? previewCss.UrlPreviewWide : undefined;
|
||||
// Twitter/Twitch/TikTok(fallback) cards render header/thumbnail beside content
|
||||
// in the card flex row; stack them on phones (no-op for the single-column
|
||||
// embed cards). Desktop keeps the row layout.
|
||||
const stackOnMobile = isTwitter(url) || isTwitch(url) || isTikTok(url);
|
||||
const cardClass =
|
||||
[wide && previewCss.UrlPreviewWide, stackOnMobile && previewCss.StackOnMobile]
|
||||
.filter(Boolean)
|
||||
.join(' ') || undefined;
|
||||
|
||||
const renderContent = (prev: IPreviewUrlResponse): React.ReactNode => {
|
||||
// Embeddable media (YouTube/Vimeo/TikTok/Dailymotion/Streamable/Twitch/
|
||||
|
||||
Reference in New Issue
Block a user