diff --git a/opennow-stable/src/renderer/src/components/SettingsPage.tsx b/opennow-stable/src/renderer/src/components/SettingsPage.tsx index 15bc9698..7d733cbc 100644 --- a/opennow-stable/src/renderer/src/components/SettingsPage.tsx +++ b/opennow-stable/src/renderer/src/components/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { Globe, Check, Search, X, Loader, Zap, Mic, FileDown, Wifi, Trash2, Heart, Users, ExternalLink } from "lucide-react"; +import { Globe, Check, Search, X, Loader, Zap, Mic, FileDown, Wifi, Trash2, Heart, Users, ExternalLink, Monitor, Keyboard } from "lucide-react"; import { useState, useCallback, useMemo, useEffect, useRef } from "react"; import type { JSX } from "react"; @@ -37,6 +37,8 @@ interface SettingsPageProps { type ThanksLoadState = "idle" | "loading" | "loaded" | "error"; +type SettingsSectionId = "stream" | "game" | "audio" | "input" | "interface" | "thanks"; + const codecOptions: VideoCodec[] = [...USER_FACING_VIDEO_CODEC_OPTIONS]; const accelerationOptions: { value: VideoAccelerationPreference; label: string }[] = [ @@ -327,7 +329,7 @@ function saveCachedEntitledResolutions(cache: EntitledResolutionsCache): void { export function SettingsPage({ settings, regions, onSettingChange, codecResults, codecTesting, onRunCodecTest }: SettingsPageProps): JSX.Element { const [savedIndicator, setSavedIndicator] = useState(false); - const [activeTab, setActiveTab] = useState<"preferences" | "thanks">("preferences"); + const [activeSection, setActiveSection] = useState("stream"); const [thanksData, setThanksData] = useState(null); const [thanksLoadState, setThanksLoadState] = useState("idle"); const [thanksFetchError, setThanksFetchError] = useState(null); @@ -427,6 +429,11 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, const [gameLanguageDropdownOpen, setGameLanguageDropdownOpen] = useState(false); const gameLanguageDropdownRef = useRef(null); + const [resolutionDropdownOpen, setResolutionDropdownOpen] = useState(false); + const resolutionDropdownRef = useRef(null); + const [settingsSearch, setSettingsSearch] = useState(""); + const [codecAdvancedOpen, setCodecAdvancedOpen] = useState(false); + // Dynamic entitled resolutions from MES API const [entitledResolutions, setEntitledResolutions] = useState([]); const [subscriptionLoading, setSubscriptionLoading] = useState(true); @@ -513,6 +520,18 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, [entitledResolutions, settings.resolution, hasDynamic] ); + const selectedResolutionLabel = useMemo(() => { + if (hasDynamic) { + for (const group of resolutionGroups) { + const found = group.resolutions.find(r => r.value === settings.resolution); + if (found) return found.label; + } + return settings.resolution || "Select"; + } + const found = STATIC_RESOLUTION_PRESETS.find(r => r.value === settings.resolution); + return found ? found.label : settings.resolution || "Select"; + }, [settings.resolution, hasDynamic, resolutionGroups]); + const handleChange = useCallback( (key: K, value: Settings[K]) => { onSettingChange(key, value); @@ -714,6 +733,9 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, if (gameLanguageDropdownRef.current && !gameLanguageDropdownRef.current.contains(target)) { setGameLanguageDropdownOpen(false); } + if (resolutionDropdownRef.current && !resolutionDropdownRef.current.contains(target)) { + setResolutionDropdownOpen(false); + } }; document.addEventListener("mousedown", handlePointerDown); @@ -802,7 +824,7 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, }, []); useEffect(() => { - if (activeTab !== "thanks") { + if (activeSection !== "thanks") { thanksRequestIdRef.current += 1; setThanksLoadState((current) => (current === "loading" || current === "error" ? "idle" : current)); setThanksFetchError(null); @@ -850,7 +872,7 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, setThanksLoadState("error"); }, ); - }, [activeTab, thanksData, thanksLoadState]); + }, [activeSection, thanksData, thanksLoadState]); const renderPersonLink = useCallback((person: ThankYouContributor | ThankYouSupporter, content: JSX.Element) => { if (!person.profileUrl) { @@ -1006,6 +1028,13 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, ); + const showAll = settingsSearch.length > 0; + const showStream = activeSection === "stream" || showAll; + const showGame = activeSection === "game" || showAll; + const showAudio = activeSection === "audio" || showAll; + const showInput = activeSection === "input" || showAll; + const showInterface = activeSection === "interface" || showAll; + return (
@@ -1016,37 +1045,64 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults,
-
- - -
- - {activeTab === "preferences" ? ( -
- {/* ── Region ────────────────────────────────────── */} -
-
-

Region

-
-
- {/* Region selector with search */} -
+
+ + {/* ── Sidebar ───────────────────────────────────────── */} + + + {/* ── Content ───────────────────────────────────────── */} +
+ {activeSection === "thanks" ? ( + thanksTabContent + ) : ( + <> + {/* ═══ STREAM ════════════════════════════════════ */} + {showStream && ( + <> + {/* ── Region ── */} +
+ {showAll &&
Stream
} +
+

Region

+
+
+
)} -
-
-
+
+ + - {/* ── Game ───────────────────────────────────────── */} -
-
-

Game

-
-
- {/* Game Language */} -
- -
- - {gameLanguageDropdownOpen && ( -
- {gameLanguageOptions.map((option) => ( + {/* ── Video ── */} +
+ {showAll &&
Stream
} +
+

Video

+
+
+ {/* Aspect Ratio — static chips */} +
+ +
+ {STATIC_ASPECT_RATIO_PRESETS.map((preset) => ( ))}
- )} -
-
-
-
- - {/* ── Video ──────────────────────────────────────── */} -
-
-

Video

-
- -
- {/* Aspect Ratio — static chips */} -
- -
- {STATIC_ASPECT_RATIO_PRESETS.map((preset) => ( - - ))} -
-
- - {/* Resolution — dynamic or static chips */} -
- - - {hasDynamic ? ( -
- {resolutionGroups.map((group) => ( -
- {group.category} -
- {group.resolutions.map((res) => ( - - ))} -
-
- ))}
- ) : ( -
- {STATIC_RESOLUTION_PRESETS.map((preset) => ( + + {/* Resolution — grouped dropdown */} +
+ +
- ))} + {resolutionDropdownOpen && ( +
+ {(hasDynamic ? resolutionGroups : [{ category: "All", resolutions: STATIC_RESOLUTION_PRESETS.map(p => ({ ...p, width: 0, height: 0 })) }]).map(group => ( +
+
{group.category}
+ {group.resolutions.map(res => ( + + ))} +
+ ))} +
+ )} +
- )} -
- {/* FPS — dynamic or static chips */} -
- -
- {(hasDynamic ? dynamicFpsOptions.map((v) => ({ value: v })) : STATIC_FPS_PRESETS).map( - (preset) => ( - - ) - )} -
-
+ {/* FPS — dynamic or static chips */} +
+ +
+ {(hasDynamic ? dynamicFpsOptions.map((v) => ({ value: v })) : STATIC_FPS_PRESETS).map((preset) => ( + + ))} +
+
- {/* Codec */} -
- -
- {codecOptions.map((codec) => { - const badgeState = getCodecDecodeBadgeState(codec, codecResults, codecTesting); - return ( - - ); - })} -
-
- -
- -
- {accelerationOptions.map((option) => ( - - ))} -
- Applies after app restart. -
+ {codec} + {badgeState && ( + + {badgeState === "gpu" ? "GPU" : badgeState === "cpu" ? "CPU" : "Testing…"} + + )} + + ); + })} +
+
-
- -
- {accelerationOptions.map((option) => ( - - ))} -
- Applies after app restart. -
+
+ +
+ {accelerationOptions.map((option) => ( + + ))} +
+ Applies after app restart. +
- {/* Color Quality */} -
- -
- {colorQualityOptions.map((opt) => { - const needsHevc = colorQualityRequiresHevc(opt.value); - return ( - - ); - })} -
- {colorQualityRequiresHevc(settings.colorQuality) && settings.codec === "H264" && ( - This mode requires H265 or AV1. Codec will be auto-switched. - )} -
+
+ +
+ {accelerationOptions.map((option) => ( + + ))} +
+ Applies after app restart. +
- {/* Bitrate slider */} -
-
- - {settings.maxBitrateMbps} Mbps -
- handleChange("maxBitrateMbps", parseInt(e.target.value, 10))} - /> -
+ {/* Color Quality */} +
+ +
+ {colorQualityOptions.map((opt) => { + const needsHevc = colorQualityRequiresHevc(opt.value); + return ( + + ); + })} +
+ {colorQualityRequiresHevc(settings.colorQuality) && settings.codec === "H264" && ( + This mode requires H265 or AV1. Codec will be auto-switched. + )} +
-
-
- -
+
-
-
-
+
- -
+
+
+ + +
+ + Request Cloud G-Sync (VRR) on newly created sessions. Smooths frame pacing on variable frame rate streams. Requires a VRR-capable display. The service may ignore this request depending on your subscription tier. + +
+ + - {/* ── Codec Diagnostics ──────────────────────────── */} -
-
-

Codec Diagnostics

-
-
-
- + {/* ── Codec Diagnostics (advanced disclosure) ── */} +
-
- - {codecTestOpen && codecResults && ( -
- {codecResults.map((result) => ( -
-
- {result.codec} - - {result.webrtcSupported ? "WebRTC Ready" : "Not in WebRTC"} - -
- -
- {/* Decode row */} -
- Decode - - {result.decodeSupported - ? result.hwAccelerated - ? "GPU" - : "CPU" - : "No"} - - {result.decodeVia} -
- - {/* Encode row */} -
- Encode - - {result.encodeSupported - ? result.encodeHwAccelerated - ? "GPU" - : "CPU" - : "No"} - - {result.encodeVia} -
-
- - {/* Profiles */} - {result.profiles.length > 0 && ( -
- Profiles: -
- {result.profiles.map((p, i) => ( - {p} - ))} -
-
- )} + {codecAdvancedOpen && ( +
+ {showAll &&
Stream
} +
+

Codec Diagnostics

- ))} -
- )} -
-
- - {/* ── Audio / Microphone ───────────────────────── */} -
-
-

Audio

-
-
- {/* Microphone Mode */} -
- -
- - {microphoneModeDropdownOpen && ( -
- {microphoneModeOptions.map((option) => ( +
+
+ - ))} +
+ {codecTestOpen && codecResults && ( +
+ {codecResults.map((result) => ( +
+
+ {result.codec} + + {result.webrtcSupported ? "WebRTC Ready" : "Not in WebRTC"} + +
+
+
+ Decode + + {result.decodeSupported ? (result.hwAccelerated ? "GPU" : "CPU") : "No"} + + {result.decodeVia} +
+
+ Encode + + {result.encodeSupported ? (result.encodeHwAccelerated ? "GPU" : "CPU") : "No"} + + {result.encodeVia} +
+
+ {result.profiles.length > 0 && ( +
+ Profiles: +
+ {result.profiles.map((p, i) => ( + {p} + ))} +
+
+ )} +
+ ))} +
+ )}
- )} -
+
+ )} - - {/* Microphone Device (only shown when mic is enabled) */} - {settings.microphoneMode !== "disabled" && ( + + )} + + {/* ═══ GAME ══════════════════════════════════════ */} + {showGame && ( +
+ {showAll &&
Game
} +
+

Game

+
+
-
-
+
+ + {gameLanguageDropdownOpen && ( +
+ {gameLanguageOptions.map((option) => ( + + ))} +
+ )} +
+
+
+
+ )} + + {/* ═══ AUDIO ══════════════════════════════════════ */} + {showAudio && ( +
+ {showAll &&
Audio
} +
+

Audio

+
+
+
+ +
- {microphoneDeviceDropdownOpen && ( -
+ {microphoneModeDropdownOpen && ( +
+ {microphoneModeOptions.map((option) => ( + + ))} +
+ )} +
+
+ + {settings.microphoneMode !== "disabled" && ( +
+ +
+
- {microphoneDevices.map((device, index) => ( + {microphoneDeviceDropdownOpen && ( +
+ + {microphoneDevices.map((device, index) => ( + + ))} +
+ )} +
+ {microphonePermissionError && ( + {microphonePermissionError} + )} + {microphoneDevices.length === 0 && !microphonePermissionError && ( + No microphone devices found + )} +
+
+ )} +
+
+ )} + + {/* ═══ INPUT ═══════════════════════════════════════ */} + {showInput && ( +
+ {showAll &&
Input
} +
+

Input

+
+
+
+ + +
+ +
+ +
+ + {keyboardLayoutDropdownOpen && ( +
+ {keyboardLayoutOptions.map((option) => ( ))}
)}
- {microphonePermissionError && ( - {microphonePermissionError} - )} - {microphoneDevices.length === 0 && !microphonePermissionError && ( - No microphone devices found - )}
-
- )} - -
- {/* ── Input ──────────────────────────────────────── */} -
-
-

Input

-
-
-
- - -
+ {/* Mouse Sensitivity */} +
+
+ + {settings.mouseSensitivity.toFixed(2)}x +
+
+ handleChange("mouseSensitivity", parseFloat(e.target.value))} + /> + { + const v = parseFloat(e.target.value || "0"); + if (Number.isFinite(v)) handleChange("mouseSensitivity", Math.max(0.1, Math.min(4, v))); + }} + /> +
+ Multiplier applied to mouse movement (1.00 = default) +
-
- -
- - {keyboardLayoutDropdownOpen && ( -
- {keyboardLayoutOptions.map((option) => ( +
+
+ + {Math.round(settings.mouseAcceleration)}% +
+
+ handleChange("mouseAcceleration", Math.max(1, Math.min(150, Math.round(Number(e.target.value) || 1))))} + /> + { + const v = Number(e.target.value || "1"); + if (Number.isFinite(v)) { + handleChange("mouseAcceleration", Math.max(1, Math.min(150, Math.round(v)))); + } + }} + /> +
+ Dynamic turn boost strength (1% = off-like, 150% = strongest). +
+ + {/* Shortcuts */} +
+
+ +
+ Editable - ))} +
- )} -
-
- {/* Mouse Sensitivity */} -
-
- - {settings.mouseSensitivity.toFixed(2)}x -
-
- handleChange("mouseSensitivity", parseFloat(e.target.value))} - /> - { - const v = parseFloat(e.target.value || "0"); - if (Number.isFinite(v)) handleChange("mouseSensitivity", Math.max(0.1, Math.min(4, v))); - }} - /> -
- Multiplier applied to mouse movement (1.00 = default) -
+
+ -
-
- - {Math.round(settings.mouseAcceleration)}% -
-
- handleChange("mouseAcceleration", Math.max(1, Math.min(150, Math.round(Number(e.target.value) || 1))))} - /> - { - const v = Number(e.target.value || "1"); - if (Number.isFinite(v)) { - handleChange("mouseAcceleration", Math.max(1, Math.min(150, Math.round(v)))); - } - }} - /> -
- Dynamic turn boost strength (1% = off-like, 150% = strongest). -
+ -
-
- -
- Editable - -
-
+ -
- + - + - + - + +
- + {(toggleStatsError || togglePointerLockError || stopStreamError || toggleAntiAfkError || toggleMicrophoneError || screenshotError) && ( + + Invalid shortcut. Use {shortcutExamples} + + )} - - + {!toggleStatsError && !togglePointerLockError && !stopStreamError && !toggleAntiAfkError && !toggleMicrophoneError && !screenshotError && ( + + {shortcutExamples}. Stop: {formatShortcutForDisplay(settings.shortcutStopStream, isMac)}. Mic: {formatShortcutForDisplay(settings.shortcutToggleMicrophone, isMac)}. Screenshot: {formatShortcutForDisplay(settings.shortcutScreenshot, isMac)}. + + )} +
+
+ )} + + {/* ═══ INTERFACE ══════════════════════════════════ */} + {showInterface && ( + <> + {/* ── Appearance ── */} +
+ {showAll &&
Interface
} +
+

Appearance

+
+
+ {/* 4-toggle grid */} +
+
+ + +
- {(toggleStatsError || togglePointerLockError || stopStreamError || toggleAntiAfkError || toggleMicrophoneError || screenshotError) && ( - - Invalid shortcut. Use {shortcutExamples} - - )} - - {!toggleStatsError && !togglePointerLockError && !stopStreamError && !toggleAntiAfkError && !toggleMicrophoneError && !screenshotError && ( - - {shortcutExamples}. Stop: {formatShortcutForDisplay(settings.shortcutStopStream, isMac)}. Mic: {formatShortcutForDisplay(settings.shortcutToggleMicrophone, isMac)}. ScreensShot: {formatShortcutForDisplay(settings.shortcutScreenshot, isMac)}. - - )} -
-
-
- - {/* ── Appearance ─────────────────────────────────── */} -
-
-

Appearance

-
-
-
- - -
- -
- - -
- -
- - -
+
+ + +
-
- - -
+
+ + +
- {settings.controllerMode && ( -
-
- -
- +
+ +
+ {/* Controller Mode */}
+ {settings.controllerMode && ( +
+
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ )} + + {/* Session Counter */}
-
- - +
+
+ + + {!settings.sessionCounterEnabled + ? "Disabled" + : settings.sessionClockShowEveryMinutes === 0 + ? "Off" + : `Every ${settings.sessionClockShowEveryMinutes} min`} + +
+ handleChange("sessionClockShowEveryMinutes", parseInt(e.target.value, 10))} + disabled={!settings.sessionCounterEnabled} + /> + + How often the session timer pops back up while streaming (0 disables repeats). +
-
- )} - -
- - -
-
-
- - - {!settings.sessionCounterEnabled - ? "Disabled" - : settings.sessionClockShowEveryMinutes === 0 - ? "Off" - : `Every ${settings.sessionClockShowEveryMinutes} min`} - -
- handleChange("sessionClockShowEveryMinutes", parseInt(e.target.value, 10))} - disabled={!settings.sessionCounterEnabled} - /> - - How often the session timer pops back up while streaming (0 disables repeats). - -
+
+
+ + + {settings.sessionCounterEnabled ? `${settings.sessionClockShowDurationSeconds}s` : "Disabled"} + +
+ handleChange("sessionClockShowDurationSeconds", parseInt(e.target.value, 10))} + disabled={!settings.sessionCounterEnabled} + /> + + How long the session timer stays visible each time it appears. + +
-
-
- - - {settings.sessionCounterEnabled ? `${settings.sessionClockShowDurationSeconds}s` : "Disabled"} - +
+ + Disabling the session elapsed counter stops the live elapsed timer from rendering at all. Remaining playtime indicators stay unchanged. + +
- handleChange("sessionClockShowDurationSeconds", parseInt(e.target.value, 10))} - disabled={!settings.sessionCounterEnabled} - /> - - How long the session timer stays visible each time it appears. - -
- -
- - Disabling the session elapsed counter stops the live elapsed timer from rendering at all. Remaining playtime indicators stay unchanged. - -
- -
- - -
-
-
- - {/* ── Miscellaneous ──────────────────────────────── */} -
-
-

Miscellaneous

-
-
- {/* Export Logs */} -
- - -
- -
- - -
-
-
- - - ) : ( - thanksTabContent - )} + + {/* ── Miscellaneous ── */} +
+ {showAll &&
Interface
} +
+

Miscellaneous

+
+
+
+ + +
+
+ + +
+
+
+ + )} + + )} + + ); } diff --git a/opennow-stable/src/renderer/src/styles.css b/opennow-stable/src/renderer/src/styles.css index 4f369405..e6269696 100644 --- a/opennow-stable/src/renderer/src/styles.css +++ b/opennow-stable/src/renderer/src/styles.css @@ -1904,7 +1904,7 @@ button.game-card-store-chip.active:hover { SETTINGS PAGE ====================================================== */ .settings-page { - max-width: 760px; + max-width: 980px; margin: 0 auto; display: flex; flex-direction: column; @@ -2178,6 +2178,231 @@ button.game-card-store-chip.active:hover { gap: 14px; } +/* ── Settings two-panel layout ───────────────────────── */ +.settings-layout { + display: flex; + gap: 0; + min-height: 0; + flex: 1; + border: 1px solid var(--panel-border); + border-radius: var(--r-md); + overflow: hidden; +} + +.settings-sidebar { + width: 188px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 0; + background: var(--bg-a); + border-right: 1px solid var(--panel-border); + padding: 10px 8px; +} + +.settings-search-wrap { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 10px; + background: var(--chip); + border: 1px solid var(--panel-border); + border-radius: 6px; + margin-bottom: 8px; +} + +.settings-search-icon { + color: var(--ink-muted); + flex-shrink: 0; +} + +.settings-search-input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: var(--ink); + font-size: 0.82rem; + font-family: inherit; + min-width: 0; +} + +.settings-search-input::placeholder { + color: var(--ink-muted); +} + +.settings-search-clear { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--ink-muted); + cursor: pointer; + padding: 2px; + border-radius: 3px; + transition: color var(--t-fast); +} + +.settings-search-clear:hover { + color: var(--ink); +} + +.settings-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.settings-nav-item { + display: flex; + align-items: center; + gap: 9px; + padding: 9px 12px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--ink-soft); + font-size: 0.88rem; + font-weight: 500; + font-family: inherit; + cursor: pointer; + text-align: left; + transition: background var(--t-fast), color var(--t-fast); + white-space: nowrap; +} + +.settings-nav-item svg { + flex-shrink: 0; + color: var(--ink-muted); + transition: color var(--t-fast); +} + +.settings-nav-item:hover { + background: var(--chip); + color: var(--ink); +} + +.settings-nav-item:hover svg { + color: var(--ink-soft); +} + +.settings-nav-item.active { + background: var(--accent-surface); + color: var(--accent); + font-weight: 600; +} + +.settings-nav-item.active svg { + color: var(--accent); +} + +.settings-content { + flex: 1; + min-width: 0; + overflow-y: auto; + padding: 16px 18px; + display: flex; + flex-direction: column; + gap: 14px; +} + +/* Advanced disclosure */ +.settings-advanced-wrap { + display: flex; + flex-direction: column; + gap: 10px; +} + +.settings-advanced-toggle { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + background: var(--bg-a); + border: 1px solid var(--panel-border); + border-radius: var(--r-sm); + color: var(--ink-soft); + font-size: 0.83rem; + font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast); +} + +.settings-advanced-toggle:hover { + color: var(--ink); + border-color: rgba(255,255,255,0.1); + background: var(--chip); +} + +.settings-advanced-chevron { + margin-left: auto; + color: var(--ink-muted); + transition: transform var(--t-fast); +} + +.settings-advanced-chevron.flipped { + transform: rotate(180deg); +} + +/* Resolution grouped dropdown */ +.settings-dropdown-menu--grouped { + max-height: 300px; +} + +.settings-dropdown-group { + display: flex; + flex-direction: column; +} + +.settings-dropdown-group-label { + padding: 8px 14px 4px; + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--ink-muted); + background: rgba(255,255,255,0.03); + border-top: 1px solid var(--panel-border); +} + +.settings-dropdown-group:first-child .settings-dropdown-group-label { + border-top: none; +} + +/* Toggle two-column grid */ +.settings-toggle-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 16px; +} + +.settings-toggle-grid .settings-row { + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + padding: 12px; + background: var(--chip); + border: 1px solid var(--panel-border); + border-radius: 8px; +} + +.settings-toggle-grid .settings-label { + flex-shrink: 1; +} + +/* Search result context label */ +.settings-section-context { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--accent); + margin-bottom: 4px; +} + .settings-section { background: var(--card); border: 1px solid var(--panel-border); @@ -2415,14 +2640,15 @@ button.game-card-store-chip.active:hover { .settings-shortcut-grid { display: grid; + grid-template-columns: 1fr 1fr; gap: 8px; } .settings-shortcut-row { display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; + flex-direction: column; + align-items: stretch; + gap: 4px; } .settings-shortcut-label { @@ -2431,8 +2657,9 @@ button.game-card-store-chip.active:hover { } .settings-shortcut-input { - min-width: 185px; - text-align: right; + min-width: unset; + width: 100%; + text-align: left; } .settings-shortcut-input--static {