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',
+21 -25
View File
@@ -1898,11 +1898,13 @@ 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"
style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}
>
<input <input
type="range" type="range"
min="0" min="0"
@@ -1917,8 +1919,6 @@ function Calls() {
{ringtoneVolume}% {ringtoneVolume}%
</Text> </Text>
</Box> </Box>
}
/>
</SequenceCard> </SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile <SettingTile
@@ -1990,10 +1990,13 @@ 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"
gap="200"
style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}
>
<input <input
type="range" type="range"
min={0} min={0}
@@ -2008,8 +2011,7 @@ function Calls() {
{soundboardVolume}% {soundboardVolume}%
</Text> </Text>
</Box> </Box>
} </>
/>
)} )}
</SequenceCard> </SequenceCard>
</Box> </Box>
@@ -2454,19 +2456,13 @@ function Messages() {
: 'On-device translation isn’t available in this browser. Use a Chromium desktop browser (Chrome/Edge 138+) or the Lotus desktop app.' : 'On-device translation isn’t 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 && (