fix(mobile): native settings controls + soundboard/gif/call polish (M5)

Mobile-audit batch 5. Desktop provably unchanged (two review passes).

- Translate-language control: raw <select> (crowded narrow tiles + broke under
  non-default themes) -> the folds-native SettingsSelect used by every other
  dropdown in the settings screen (native-cinny; keeps aria-label).
- Ringtone/Soundboard volume sliders: moved from the fixed-width tile `after`
  slot (which squeezed the title on phones) to a full-width slider in the tile
  body, matching the night-light slider pattern.
- Screenshare-confirm popover: clamp maxWidth to the viewport so it can't run
  past the screen edges on a phone (inert on desktop).
- In-call soundboard editor rows wrap on a narrow popout instead of crushing the
  clip-name field.
- GifPicker: feed the giphy Grid the measured container width (useElementSize
  Observer) instead of a fixed 296px, so it doesn't overflow a <312px phone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:38:01 -04:00
co-authored by Claude Opus 4.8
parent 4c298a36b4
commit 09415f95c0
4 changed files with 67 additions and 56 deletions
+14 -3
View File
@@ -1,9 +1,10 @@
import React, { useCallback } from 'react'; import React, { useCallback, useRef, useState } from 'react';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { Grid, SearchBar, SearchContext, SearchContextManager } from '@giphy/react-components'; import { Grid, SearchBar, SearchContext, SearchContextManager } from '@giphy/react-components';
import { IGif } from '@giphy/js-types'; import { IGif } from '@giphy/js-types';
import { Box, color, config } from 'folds'; import { Box, color, config } from 'folds';
import { useElementSizeObserver } from '../hooks/useElementSizeObserver';
import { useSetting } from '../state/hooks/settings'; import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings'; import { settingsAtom } from '../state/settings';
import { addRecentGif, RecentGif, recentGifsAtom } from '../state/recentGifs'; import { addRecentGif, RecentGif, recentGifsAtom } from '../state/recentGifs';
@@ -146,8 +147,18 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
const showRecents = recents.length > 0 && !(term ?? '').trim(); const showRecents = recents.length > 0 && !(term ?? '').trim();
// The container is min(312px, 100vw-16); feed the Grid the live pixel width
// (minus the inner 8px padding on each side) so it doesn't overflow a phone
// narrower than 312px with a fixed 296px grid.
const containerRef = useRef<HTMLDivElement>(null);
const [gridWidth, setGridWidth] = useState(PICKER_WIDTH - 16);
useElementSizeObserver(
useCallback(() => containerRef.current, []),
useCallback((w) => setGridWidth(Math.max(1, Math.floor(w) - 16)), []),
);
return ( return (
<Box direction="Column" style={{ width: PICKER_WIDTH_CSS }}> <Box direction="Column" style={{ width: PICKER_WIDTH_CSS }} ref={containerRef}>
{lotusTerminal && ( {lotusTerminal && (
<div <div
style={{ style={{
@@ -178,7 +189,7 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
<Grid <Grid
key={searchKey} key={searchKey}
fetchGifs={fetchGifs} fetchGifs={fetchGifs}
width={PICKER_WIDTH - 16} width={gridWidth}
columns={2} columns={2}
gutter={4} gutter={4}
onGifClick={handleClick} onGifClick={handleClick}
@@ -275,6 +275,7 @@ export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPack
key={key} key={key}
alignItems="Center" alignItems="Center"
gap="200" gap="200"
wrap="Wrap"
style={{ style={{
padding: config.space.S200, padding: config.space.S200,
borderRadius: config.radii.R400, borderRadius: config.radii.R400,
+3
View File
@@ -338,6 +338,9 @@ export function CallControls({ callEmbed }: CallControlsProps) {
padding: '1rem 1.25rem', padding: '1rem 1.25rem',
zIndex: 100, zIndex: 100,
minWidth: '260px', minWidth: '260px',
// Don't run past the screen edges on a narrow phone (centered via
// translateX(-50%)); clamp to the viewport minus a small margin.
maxWidth: `calc(100vw - 2 * ${config.space.S400})`,
boxShadow: '0 8px 32px rgba(0,0,0,0.35)', boxShadow: '0 8px 32px rgba(0,0,0,0.35)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
+49 -53
View File
@@ -1898,27 +1898,27 @@ function Calls() {
/> />
</SequenceCard> </SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile <SettingTile title="Ringtone Volume" description="Volume of the incoming call ringtone." />
title="Ringtone Volume" <Box
description="Volume of the incoming call ringtone." direction="Row"
after={ alignItems="Center"
<Box direction="Row" alignItems="Center" gap="200" style={{ minWidth: '160px' }}> gap="200"
<input style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}
type="range" >
min="0" <input
max="100" type="range"
step="5" min="0"
value={ringtoneVolume} max="100"
onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))} step="5"
aria-label="Ringtone volume" value={ringtoneVolume}
style={{ flex: 1, accentColor: color.Primary.Main }} onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))}
/> aria-label="Ringtone volume"
<Text size="T200" style={{ minWidth: '32px', textAlign: 'right' }}> style={{ flex: 1, accentColor: color.Primary.Main }}
{ringtoneVolume}% />
</Text> <Text size="T200" style={{ minWidth: '32px', textAlign: 'right' }}>
</Box> {ringtoneVolume}%
} </Text>
/> </Box>
</SequenceCard> </SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile <SettingTile
@@ -1990,26 +1990,28 @@ function Calls() {
} }
/> />
{soundboardEnabled && ( {soundboardEnabled && (
<SettingTile <>
title="Soundboard Volume" <SettingTile title="Soundboard Volume" />
after={ <Box
<Box alignItems="Center" gap="200" style={{ minWidth: toRem(180) }}> alignItems="Center"
<input gap="200"
type="range" style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}
min={0} >
max={100} <input
step={5} type="range"
value={soundboardVolume} min={0}
onChange={(e) => setSoundboardVolume(parseInt(e.target.value, 10))} max={100}
style={{ flexGrow: 1 }} step={5}
aria-label="Soundboard volume" value={soundboardVolume}
/> onChange={(e) => setSoundboardVolume(parseInt(e.target.value, 10))}
<Text size="T200" style={{ minWidth: toRem(36), textAlign: 'right' }}> style={{ flexGrow: 1 }}
{soundboardVolume}% aria-label="Soundboard volume"
</Text> />
</Box> <Text size="T200" style={{ minWidth: toRem(36), textAlign: 'right' }}>
} {soundboardVolume}%
/> </Text>
</Box>
</>
)} )}
</SequenceCard> </SequenceCard>
</Box> </Box>
@@ -2454,19 +2456,13 @@ function Messages() {
: 'On-device translation isnt available in this browser. Use a Chromium desktop browser (Chrome/Edge 138+) or the Lotus desktop app.' : 'On-device translation isnt available in this browser. Use a Chromium desktop browser (Chrome/Edge 138+) or the Lotus desktop app.'
} }
after={ after={
<select <SettingsSelect
aria-label="Translate messages into"
disabled={!translationSupported}
value={selectedTargetLang} value={selectedTargetLang}
onChange={(e) => setTranslateTargetLang(e.target.value)} onChange={(v) => setTranslateTargetLang(v)}
style={pickerInputStyle(color, config)} disabled={!translationSupported}
> aria-label="Translate messages into"
{TRANSLATE_TARGET_LANGUAGES.map((l) => ( options={TRANSLATE_TARGET_LANGUAGES.map((l) => ({ value: l.code, label: l.name }))}
<option key={l.code} value={l.code}> />
{l.name}
</option>
))}
</select>
} }
/> />
{translationSupported && ( {translationSupported && (