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