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="max-w-5xl mx-auto">
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<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">
|
<div className="space-y-6">
|
||||||
|
|
||||||
{/* Profile */}
|
{/* Profile */}
|
||||||
@ -300,140 +300,6 @@ export default function SettingsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 */}
|
{/* Calendar */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -529,6 +395,138 @@ export default function SettingsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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) */}
|
{/* Integrations (ntfy push notifications) */}
|
||||||
<NtfySettingsSection settings={settings} updateSettings={updateSettings} />
|
<NtfySettingsSection settings={settings} updateSettings={updateSettings} />
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user