Rebalance settings page columns and inline lock-after input
Left column: Profile, Appearance, Calendar, Dashboard, Weather (prefs & display) Right column: Security, Authentication, Integrations (security & services) Also inlines the "Lock after [input] minutes" onto a single row. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7d6ac4d257
commit
ca1cd14ed1
@ -220,7 +220,7 @@ export default function SettingsPage() {
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
|
||||
{/* ── Left column: Profile, Appearance, Weather ── */}
|
||||
{/* ── Left column: Profile, Appearance, Calendar, Dashboard, Weather ── */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Profile */}
|
||||
@ -300,140 +300,6 @@ export default function SettingsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Weather */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-1.5 rounded-md bg-amber-500/10">
|
||||
<Cloud className="h-4 w-4 text-amber-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Weather</CardTitle>
|
||||
<CardDescription>Configure the weather widget on your dashboard</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label>Location</Label>
|
||||
{hasLocation ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-2 rounded-md border border-accent/30 bg-accent/10 px-3 py-1.5 text-sm text-foreground">
|
||||
<MapPin className="h-3.5 w-3.5 text-accent" />
|
||||
{settings?.weather_city || `${settings?.weather_lat}, ${settings?.weather_lon}`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLocationClear}
|
||||
className="inline-flex items-center justify-center rounded-md h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
title="Clear location"
|
||||
aria-label="Clear weather location"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div ref={searchRef} className="relative">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search for a city..."
|
||||
value={locationQuery}
|
||||
onChange={(e) => handleLocationInputChange(e.target.value)}
|
||||
onFocus={() => { if (locationResults.length > 0) setShowDropdown(true); }}
|
||||
className="pl-9 pr-9"
|
||||
/>
|
||||
{isSearching && (
|
||||
<Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
{showDropdown && (
|
||||
<div className="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg overflow-hidden">
|
||||
{locationResults.map((loc, i) => (
|
||||
<button
|
||||
key={`${loc.lat}-${loc.lon}-${i}`}
|
||||
type="button"
|
||||
onClick={() => handleLocationSelect(loc)}
|
||||
className="flex items-center gap-2.5 w-full px-3 py-2.5 text-sm text-left hover:bg-accent/10 transition-colors"
|
||||
>
|
||||
<MapPin className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<span>
|
||||
<span className="text-foreground font-medium">{loc.name}</span>
|
||||
{(loc.state || loc.country) && (
|
||||
<span className="text-muted-foreground">
|
||||
{loc.state ? `, ${loc.state}` : ''}{loc.country ? `, ${loc.country}` : ''}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Search and select your city for accurate weather data on the dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ── Right column: Security, Authentication, Calendar, Dashboard, Integrations ── */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Security (auto-lock) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-1.5 rounded-md bg-emerald-500/10">
|
||||
<Shield className="h-4 w-4 text-emerald-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Security</CardTitle>
|
||||
<CardDescription>Configure screen lock behavior</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Auto-lock</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically lock the screen after idle time
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoLockEnabled}
|
||||
onCheckedChange={handleAutoLockToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="auto_lock_minutes">Lock after</Label>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Input
|
||||
id="auto_lock_minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
value={autoLockMinutes}
|
||||
onChange={(e) => setAutoLockMinutes(e.target.value === '' ? '' : parseInt(e.target.value) || '')}
|
||||
onBlur={handleAutoLockMinutesSave}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleAutoLockMinutesSave(); }}
|
||||
className="w-24"
|
||||
disabled={!autoLockEnabled || isUpdating}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Authentication (TOTP + password change) */}
|
||||
<TotpSetupSection />
|
||||
|
||||
{/* Calendar */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@ -529,6 +395,138 @@ export default function SettingsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Weather */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-1.5 rounded-md bg-amber-500/10">
|
||||
<Cloud className="h-4 w-4 text-amber-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Weather</CardTitle>
|
||||
<CardDescription>Configure the weather widget on your dashboard</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label>Location</Label>
|
||||
{hasLocation ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-2 rounded-md border border-accent/30 bg-accent/10 px-3 py-1.5 text-sm text-foreground">
|
||||
<MapPin className="h-3.5 w-3.5 text-accent" />
|
||||
{settings?.weather_city || `${settings?.weather_lat}, ${settings?.weather_lon}`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLocationClear}
|
||||
className="inline-flex items-center justify-center rounded-md h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
title="Clear location"
|
||||
aria-label="Clear weather location"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div ref={searchRef} className="relative">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search for a city..."
|
||||
value={locationQuery}
|
||||
onChange={(e) => handleLocationInputChange(e.target.value)}
|
||||
onFocus={() => { if (locationResults.length > 0) setShowDropdown(true); }}
|
||||
className="pl-9 pr-9"
|
||||
/>
|
||||
{isSearching && (
|
||||
<Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
{showDropdown && (
|
||||
<div className="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg overflow-hidden">
|
||||
{locationResults.map((loc, i) => (
|
||||
<button
|
||||
key={`${loc.lat}-${loc.lon}-${i}`}
|
||||
type="button"
|
||||
onClick={() => handleLocationSelect(loc)}
|
||||
className="flex items-center gap-2.5 w-full px-3 py-2.5 text-sm text-left hover:bg-accent/10 transition-colors"
|
||||
>
|
||||
<MapPin className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<span>
|
||||
<span className="text-foreground font-medium">{loc.name}</span>
|
||||
{(loc.state || loc.country) && (
|
||||
<span className="text-muted-foreground">
|
||||
{loc.state ? `, ${loc.state}` : ''}{loc.country ? `, ${loc.country}` : ''}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Search and select your city for accurate weather data on the dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ── Right column: Security, Authentication, Integrations ── */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Security (auto-lock) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-1.5 rounded-md bg-emerald-500/10">
|
||||
<Shield className="h-4 w-4 text-emerald-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Security</CardTitle>
|
||||
<CardDescription>Configure screen lock behavior</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Auto-lock</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically lock the screen after idle time
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoLockEnabled}
|
||||
onCheckedChange={handleAutoLockToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="auto_lock_minutes" className="shrink-0">Lock after</Label>
|
||||
<Input
|
||||
id="auto_lock_minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
value={autoLockMinutes}
|
||||
onChange={(e) => setAutoLockMinutes(e.target.value === '' ? '' : parseInt(e.target.value) || '')}
|
||||
onBlur={handleAutoLockMinutesSave}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleAutoLockMinutesSave(); }}
|
||||
className="w-20"
|
||||
disabled={!autoLockEnabled || isUpdating}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground shrink-0">minutes</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Authentication (TOTP + password change) */}
|
||||
<TotpSetupSection />
|
||||
|
||||
{/* Integrations (ntfy push notifications) */}
|
||||
<NtfySettingsSection settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user