mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
fix: integrations settings squish on mobile (#812) + polish
PhotoProvidersSection: - Replace raw <input type=checkbox> with TREK's ToggleSwitch so the 'spiegeln zu Immich'-style options match the rest of the app. - Wrap action row in flex-wrap so the connected/disconnected badge drops to its own line on mobile instead of clipping. - Add a short 'Test' translation (memories.testShort) shown on mobile in place of 'Test connection' — 14 languages kept in sync. ToggleSwitch: - Explicit type='button' (never a form submitter), minWidth + flex- shrink:0 so the toggle doesn't get squished next to long labels, padding:0 so no inherited UA margin warps the inner circle. MapSettingsTab: - 'Mapbox' instead of 'Mapbox GL' on narrow screens — the provider card is too cramped on mobile for the full name. - Drop the 'Experimental' badge on mobile entirely; it overlapped the title at that width. Still shown on >=sm. DisplaySettingsTab: - Time format buttons show just '24h' / '12h' on mobile; the '(14:30)' / '(2:30 PM)' hint stays on >=sm. Test updated to match the role query since the label is now split across nodes.
This commit is contained in:
@@ -155,7 +155,9 @@ describe('DisplaySettingsTab', () => {
|
||||
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '12h' }), updateSetting });
|
||||
render(<DisplaySettingsTab />);
|
||||
await user.click(screen.getByText('24h (14:30)'));
|
||||
// The label is split across a text node ('24h') and a responsive span (' (14:30)').
|
||||
// Click the button that contains the 24h text instead of matching the full string.
|
||||
await user.click(screen.getByRole('button', { name: /24h/ }));
|
||||
expect(updateSetting).toHaveBeenCalledWith('time_format', '24h');
|
||||
});
|
||||
|
||||
|
||||
@@ -188,8 +188,8 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.timeFormat')}</label>
|
||||
<div className="flex gap-3">
|
||||
{[
|
||||
{ value: '24h', label: '24h (14:30)' },
|
||||
{ value: '12h', label: '12h (2:30 PM)' },
|
||||
{ value: '24h', short: '24h', example: '14:30' },
|
||||
{ value: '12h', short: '12h', example: '2:30 PM' },
|
||||
].map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
@@ -207,7 +207,8 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
{opt.short}
|
||||
<span className="hidden sm:inline">{` (${opt.example})`}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -240,14 +240,18 @@ export default function MapSettingsTab(): React.ReactElement {
|
||||
: 'border-slate-200 hover:border-slate-400 dark:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span className="absolute top-2 right-2 text-[9px] font-semibold tracking-wide uppercase px-1.5 py-[3px] rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300 leading-none">
|
||||
{t('settings.mapExperimental')}
|
||||
</span>
|
||||
<Box size={18} className="mt-0.5 flex-shrink-0 text-slate-700 dark:text-slate-300" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-900 dark:text-white">Mapbox GL</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-slate-900 dark:text-white">
|
||||
<span className="sm:hidden">Mapbox</span>
|
||||
<span className="hidden sm:inline">Mapbox GL</span>
|
||||
</div>
|
||||
<div className="hidden sm:block text-xs text-slate-500 mt-0.5">{t('settings.mapMapboxSubtitle')}</div>
|
||||
</div>
|
||||
{/* Experimental badge only on ≥sm; on mobile there's no room next to the title. */}
|
||||
<span className="hidden sm:inline-block absolute top-2 right-2 text-[9px] font-semibold tracking-wide uppercase px-1.5 py-[3px] rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300 leading-none">
|
||||
{t('settings.mapExperimental')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-2">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useToast } from '../../components/shared/Toast'
|
||||
import apiClient from '../../api/client'
|
||||
import { useAddonStore } from '../../store/addonStore'
|
||||
import Section from './Section'
|
||||
import ToggleSwitch from './ToggleSwitch'
|
||||
|
||||
interface ProviderField {
|
||||
key: string
|
||||
@@ -222,15 +223,13 @@ export default function PhotoProvidersSection(): React.ReactElement {
|
||||
{fields.map(field => (
|
||||
<div key={`${provider.id}-${field.key}`}>
|
||||
{field.input_type === 'checkbox' ? (
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={values[field.key] === 'true'}
|
||||
onChange={e => handleProviderFieldChange(provider.id, field.key, e.target.checked ? 'true' : 'false')}
|
||||
className="w-4 h-4 rounded border-slate-300 accent-slate-900"
|
||||
<div className="flex items-center gap-3">
|
||||
<ToggleSwitch
|
||||
on={values[field.key] === 'true'}
|
||||
onToggle={() => handleProviderFieldChange(provider.id, field.key, values[field.key] === 'true' ? 'false' : 'true')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-slate-700">{t(`memories.${field.label}`)}</span>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t(`memories.${field.label}`)}</label>
|
||||
@@ -248,7 +247,9 @@ export default function PhotoProvidersSection(): React.ReactElement {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Wraps on mobile so the connection badge drops to its own row
|
||||
instead of clipping off the side of the card. */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
onClick={() => handleSaveProvider(provider)}
|
||||
disabled={!canSave || !!saving[provider.id] || isProviderSaveDisabled(provider)}
|
||||
@@ -266,15 +267,17 @@ export default function PhotoProvidersSection(): React.ReactElement {
|
||||
{testing
|
||||
? <div className="w-4 h-4 border-2 border-slate-300 border-t-slate-700 rounded-full animate-spin" />
|
||||
: <Camera className="w-4 h-4" />}
|
||||
{t('memories.testConnection')}
|
||||
<span className="sm:hidden">{t('memories.testShort')}</span>
|
||||
<span className="hidden sm:inline">{t('memories.testConnection')}</span>
|
||||
</button>
|
||||
{/* On mobile the badge sits on its own row thanks to flex-wrap, so force a line break via basis-full. */}
|
||||
{connected ? (
|
||||
<span className="text-xs font-medium text-green-600 flex items-center gap-1">
|
||||
<span className="basis-full sm:basis-auto text-xs font-medium text-green-600 flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full" />
|
||||
{t('memories.connected')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs font-medium text-slate-400 flex items-center gap-1">
|
||||
<span className="basis-full sm:basis-auto text-xs font-medium text-slate-400 flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-slate-300 rounded-full" />
|
||||
{t('memories.disconnected')}
|
||||
</span>
|
||||
|
||||
@@ -2,9 +2,10 @@ import React from 'react'
|
||||
|
||||
export default function ToggleSwitch({ on, onToggle }: { on: boolean; onToggle: () => void }) {
|
||||
return (
|
||||
<button onClick={onToggle}
|
||||
<button type="button" onClick={onToggle}
|
||||
style={{
|
||||
position: 'relative', width: 44, height: 24, borderRadius: 12, border: 'none', cursor: 'pointer',
|
||||
position: 'relative', width: 44, height: 24, minWidth: 44, flexShrink: 0,
|
||||
borderRadius: 12, border: 'none', padding: 0, cursor: 'pointer',
|
||||
background: on ? 'var(--accent, #111827)' : 'var(--border-primary, #d1d5db)',
|
||||
transition: 'background 0.2s',
|
||||
}}>
|
||||
|
||||
Reference in New Issue
Block a user