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 { useAtom } from 'jotai';
import { Grid, SearchBar, SearchContext, SearchContextManager } from '@giphy/react-components';
import { IGif } from '@giphy/js-types';
import { Box, color, config } from 'folds';
import { useElementSizeObserver } from '../hooks/useElementSizeObserver';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import { addRecentGif, RecentGif, recentGifsAtom } from '../state/recentGifs';
@@ -146,8 +147,18 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
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 (
<Box direction="Column" style={{ width: PICKER_WIDTH_CSS }}>
<Box direction="Column" style={{ width: PICKER_WIDTH_CSS }} ref={containerRef}>
{lotusTerminal && (
<div
style={{
@@ -178,7 +189,7 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
<Grid
key={searchKey}
fetchGifs={fetchGifs}
width={PICKER_WIDTH - 16}
width={gridWidth}
columns={2}
gutter={4}
onGifClick={handleClick}
@@ -275,6 +275,7 @@ export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPack
key={key}
alignItems="Center"
gap="200"
wrap="Wrap"
style={{
padding: config.space.S200,
borderRadius: config.radii.R400,
+3
View File
@@ -338,6 +338,9 @@ export function CallControls({ callEmbed }: CallControlsProps) {
padding: '1rem 1.25rem',
zIndex: 100,
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)',
display: 'flex',
flexDirection: 'column',
+49 -53
View File
@@ -1898,27 +1898,27 @@ function Calls() {
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Ringtone Volume"
description="Volume of the incoming call ringtone."
after={
<Box direction="Row" alignItems="Center" gap="200" style={{ minWidth: '160px' }}>
<input
type="range"
min="0"
max="100"
step="5"
value={ringtoneVolume}
onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))}
aria-label="Ringtone volume"
style={{ flex: 1, accentColor: color.Primary.Main }}
/>
<Text size="T200" style={{ minWidth: '32px', textAlign: 'right' }}>
{ringtoneVolume}%
</Text>
</Box>
}
/>
<SettingTile title="Ringtone Volume" description="Volume of the incoming call ringtone." />
<Box
direction="Row"
alignItems="Center"
gap="200"
style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}
>
<input
type="range"
min="0"
max="100"
step="5"
value={ringtoneVolume}
onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))}
aria-label="Ringtone volume"
style={{ flex: 1, accentColor: color.Primary.Main }}
/>
<Text size="T200" style={{ minWidth: '32px', textAlign: 'right' }}>
{ringtoneVolume}%
</Text>
</Box>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
@@ -1990,26 +1990,28 @@ function Calls() {
}
/>
{soundboardEnabled && (
<SettingTile
title="Soundboard Volume"
after={
<Box alignItems="Center" gap="200" style={{ minWidth: toRem(180) }}>
<input
type="range"
min={0}
max={100}
step={5}
value={soundboardVolume}
onChange={(e) => setSoundboardVolume(parseInt(e.target.value, 10))}
style={{ flexGrow: 1 }}
aria-label="Soundboard volume"
/>
<Text size="T200" style={{ minWidth: toRem(36), textAlign: 'right' }}>
{soundboardVolume}%
</Text>
</Box>
}
/>
<>
<SettingTile title="Soundboard Volume" />
<Box
alignItems="Center"
gap="200"
style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}
>
<input
type="range"
min={0}
max={100}
step={5}
value={soundboardVolume}
onChange={(e) => setSoundboardVolume(parseInt(e.target.value, 10))}
style={{ flexGrow: 1 }}
aria-label="Soundboard volume"
/>
<Text size="T200" style={{ minWidth: toRem(36), textAlign: 'right' }}>
{soundboardVolume}%
</Text>
</Box>
</>
)}
</SequenceCard>
</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.'
}
after={
<select
aria-label="Translate messages into"
disabled={!translationSupported}
<SettingsSelect
value={selectedTargetLang}
onChange={(e) => setTranslateTargetLang(e.target.value)}
style={pickerInputStyle(color, config)}
>
{TRANSLATE_TARGET_LANGUAGES.map((l) => (
<option key={l.code} value={l.code}>
{l.name}
</option>
))}
</select>
onChange={(v) => setTranslateTargetLang(v)}
disabled={!translationSupported}
aria-label="Translate messages into"
options={TRANSLATE_TARGET_LANGUAGES.map((l) => ({ value: l.code, label: l.name }))}
/>
}
/>
{translationSupported && (