diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html index a23a5308b..314e151e4 100644 --- a/apps/web/src/app/settings/settings.component.html +++ b/apps/web/src/app/settings/settings.component.html @@ -14,18 +14,20 @@ class="settings-layout" novalidate > + @if (!isDialog) { -
+ -
-

- {{ 'SETTINGS.TITLE' | translate }} -

-

{{ 'SETTINGS.GENERAL_SUBTITLE' | translate }}

-
-
+ {{ 'SETTINGS.TITLE' | translate }} + }
diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index 5fcd5c475..4c6c0db57 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -235,50 +235,59 @@ } } +// Compact segmented control — replaces the previous 3 buttons each ~78px +// tall. Per the UX audit, "Light/Dark/System" was reading as the most +// important setting on the page because of its sheer footprint. Now the +// row reads at the same weight as the other compact rows (Language, +// Startup behavior, etc.), restoring the visual hierarchy. .theme-switcher { - width: min(100%, 540px); - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; + width: min(100%, 360px); + display: inline-flex; + padding: 3px; + gap: 2px; + background: color-mix( + in srgb, + var(--mat-sys-surface-container-low) 80%, + transparent + ); + border: 1px solid + color-mix(in srgb, var(--mat-sys-outline-variant) 60%, transparent); + border-radius: 10px; } .theme-switcher__option { + flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center; - gap: 8px; - min-height: 54px; - padding: 12px 14px; - border: 1px solid - color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent); - border-radius: 16px; - background: color-mix( - in srgb, - var(--mat-sys-surface-container-low) 88%, - transparent - ); + gap: 6px; + min-height: 30px; + padding: 4px 12px; + border: 0; + border-radius: 7px; + background: transparent; color: var(--mat-sys-on-surface-variant); font: inherit; - font-size: 0.9rem; - font-weight: 600; - letter-spacing: -0.01em; + font-size: 0.82rem; + font-weight: 500; + letter-spacing: 0; cursor: pointer; transition: - border-color 0.18s ease, - background 0.18s ease, - box-shadow 0.18s ease, - color 0.18s ease; + background 0.16s ease, + color 0.16s ease, + box-shadow 0.16s ease; mat-icon { - width: 18px; - height: 18px; - font-size: 18px; + width: 16px; + height: 16px; + font-size: 16px; + line-height: 16px; } &:hover { background: color-mix( in srgb, - var(--mat-sys-surface-container-high) 86%, + var(--mat-sys-surface-container-highest) 70%, transparent ); color: var(--mat-sys-on-surface); @@ -286,31 +295,19 @@ &:focus-visible { outline: none; - box-shadow: 0 0 0 3px - color-mix(in srgb, var(--app-selection-color) 18%, transparent); + box-shadow: 0 0 0 2px + color-mix(in srgb, var(--app-selection-color) 50%, transparent); } } .theme-switcher__option--selected { - border-color: var(--settings-group-active-ring); - background: linear-gradient( - 135deg, - color-mix( - in srgb, - var(--app-selection-surface-strong) 72%, - var(--mat-sys-surface-container-high) - ), - color-mix( - in srgb, - var(--app-selection-surface) 72%, - var(--mat-sys-surface-container) - ) - ); + background: var(--app-widget-bg, var(--mat-sys-surface)); color: var(--app-selection-color); + font-weight: 600; box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.18), inset 0 0 0 1px - color-mix(in srgb, var(--app-selection-border) 54%, transparent), - 0 10px 18px -22px var(--settings-group-active-glow); + color-mix(in srgb, var(--app-selection-border) 36%, transparent); } .epg-sources { @@ -708,21 +705,31 @@ ); } +// Sticky-footer Save bar — full content width so it never overlaps form +// rows. The previous chip-style bar (border-radius:18px, margin-left:auto) +// floated in the bottom-right corner and clipped fields underneath; the +// UX audit called this out as the worst of three available patterns. +// Now the bar acts as a proper footer: spans the column, separator line +// on top, sits flush with the bottom edge while still sticking on scroll. .action-bar { position: sticky; bottom: calc( var(--settings-action-bar-offset) + var(--settings-safe-bottom) ); - margin-left: auto; + margin-left: 0; + margin-right: 0; z-index: 100; display: flex; justify-content: flex-end; align-items: center; gap: 12px; flex-wrap: wrap; - padding: 10px 14px; - border: 1px solid var(--settings-action-bar-border); - border-radius: 18px; + padding: 12px 18px; + border-top: 1px solid var(--settings-action-bar-border); + border-left: 0; + border-right: 0; + border-bottom: 0; + border-radius: 0; background: var(--settings-action-bar-bg); backdrop-filter: blur(12px); box-shadow: var(--settings-action-bar-shadow); @@ -735,16 +742,21 @@ box-shadow: none; border-radius: 16px; padding: 12px; + border-top: 1px solid var(--settings-action-bar-border); justify-content: space-between; } .settings-save-button.mat-mdc-button-base { - background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); - color: #fff; + // Use the unified blue primary (--app-selection-color) instead of the + // old hand-rolled #3b82f6 gradient that didn't match the rest of the + // app's CTAs after the color unification commit. + background: var(--app-selection-color); + color: var(--app-selection-on-color, #fff); font-weight: 500; padding: 0 20px; border-radius: 10px; - box-shadow: 0 4px 14px rgba(59, 130, 246, 0.28); + box-shadow: 0 4px 14px + color-mix(in srgb, var(--app-selection-color) 28%, transparent); transition: transform 0.2s ease, box-shadow 0.2s ease, @@ -756,7 +768,8 @@ &:hover:not([disabled]) { transform: translateY(-1px); - box-shadow: 0 6px 18px rgba(59, 130, 246, 0.38); + box-shadow: 0 6px 18px + color-mix(in srgb, var(--app-selection-color) 38%, transparent); } &:active:not([disabled]) { diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 6fc137424..cd2980df2 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "السمة الداكنة", - "LIGHT_THEME": "السمة الفاتحة", - "SYSTEM_THEME": "سمة النظام" + "DARK_THEME": "داكن", + "LIGHT_THEME": "فاتح", + "SYSTEM_THEME": "النظام" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index b5a8c3cf0..35ae7c48a 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "المظهر الداكن", - "LIGHT_THEME": "المظهر الفاتح", - "SYSTEM_THEME": "مظهر النظام" + "DARK_THEME": "داكن", + "LIGHT_THEME": "فاتح", + "SYSTEM_THEME": "النظام" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index b06d6758a..5921f839a 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Цёмная тэма", - "LIGHT_THEME": "Светлая тэма", - "SYSTEM_THEME": "Сістэмная тэма" + "DARK_THEME": "Цёмная", + "LIGHT_THEME": "Светлая", + "SYSTEM_THEME": "Сістэмная" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 58afa845a..69ec3d7ec 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Σκούρο θέμα", - "LIGHT_THEME": "Ανοιχτόχρωμο θέμα", - "SYSTEM_THEME": "Θέμα συστήματος" + "DARK_THEME": "Σκούρο", + "LIGHT_THEME": "Ανοιχτό", + "SYSTEM_THEME": "Σύστημα" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 7f4b17947..f708e035c 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Dark theme", - "LIGHT_THEME": "Light theme", - "SYSTEM_THEME": "System theme" + "DARK_THEME": "Dark", + "LIGHT_THEME": "Light", + "SYSTEM_THEME": "System" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index c43bd4d08..3de1b6ec4 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Tema oscuro", - "LIGHT_THEME": "Tema claro", - "SYSTEM_THEME": "Tema del sistema" + "DARK_THEME": "Oscuro", + "LIGHT_THEME": "Claro", + "SYSTEM_THEME": "Sistema" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 6b573bf0e..97b37ee9d 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Thème foncé", - "LIGHT_THEME": "Thème clair", - "SYSTEM_THEME": "Thème du système" + "DARK_THEME": "Sombre", + "LIGHT_THEME": "Clair", + "SYSTEM_THEME": "Système" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index fbefedf2a..ed86645e7 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Tema scuro", - "LIGHT_THEME": "Tema chiaro", - "SYSTEM_THEME": "Tema di sistema" + "DARK_THEME": "Scuro", + "LIGHT_THEME": "Chiaro", + "SYSTEM_THEME": "Sistema" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 8159150ef..9ea348d08 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "ダークテーマ", - "LIGHT_THEME": "ライトテーマ", - "SYSTEM_THEME": "システムテーマ" + "DARK_THEME": "ダーク", + "LIGHT_THEME": "ライト", + "SYSTEM_THEME": "システム" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 49fa7ac26..98212cdc1 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "어두운 테마", - "LIGHT_THEME": "밝은 테마", - "SYSTEM_THEME": "시스템 테마" + "DARK_THEME": "어둡게", + "LIGHT_THEME": "밝게", + "SYSTEM_THEME": "시스템" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index d5e31f633..4c7d2a961 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Donker thema", - "LIGHT_THEME": "Licht thema", - "SYSTEM_THEME": "Systeemthema" + "DARK_THEME": "Donker", + "LIGHT_THEME": "Licht", + "SYSTEM_THEME": "Systeem" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index d9067313d..fe97670d9 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Ciemny motyw", - "LIGHT_THEME": "Jasny motyw", - "SYSTEM_THEME": "Motyw systemowy" + "DARK_THEME": "Ciemny", + "LIGHT_THEME": "Jasny", + "SYSTEM_THEME": "Systemowy" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 415663cfb..433e20503 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Tema escuro", - "LIGHT_THEME": "Tema claro", - "SYSTEM_THEME": "Tema do sistema" + "DARK_THEME": "Escuro", + "LIGHT_THEME": "Claro", + "SYSTEM_THEME": "Sistema" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index dd7e840ef..d2bcaa0ff 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Тёмная тема", - "LIGHT_THEME": "Светлая тема", - "SYSTEM_THEME": "Системная тема" + "DARK_THEME": "Тёмная", + "LIGHT_THEME": "Светлая", + "SYSTEM_THEME": "Системная" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index a7d4e824e..3e732d5d6 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -375,9 +375,9 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "Koyu tema", - "LIGHT_THEME": "Açık tema", - "SYSTEM_THEME": "Sistem teması" + "DARK_THEME": "Koyu", + "LIGHT_THEME": "Açık", + "SYSTEM_THEME": "Sistem" }, "PLAYBACK_DIAGNOSTICS": { "UNSUPPORTED_CONTAINER": { diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index b1f1798d3..5380c69ae 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -375,8 +375,8 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "暗黑模式", - "LIGHT_THEME": "明亮模式", + "DARK_THEME": "深色", + "LIGHT_THEME": "浅色", "SYSTEM_THEME": "跟随系统" }, "PLAYBACK_DIAGNOSTICS": { diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index c0aa33b49..7a25f94d0 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -375,8 +375,8 @@ "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { - "DARK_THEME": "深色主題", - "LIGHT_THEME": "亮色主題", + "DARK_THEME": "深色", + "LIGHT_THEME": "淺色", "SYSTEM_THEME": "跟隨系統" }, "PLAYBACK_DIAGNOSTICS": { diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index f1ec3dcfb..dc0e6ecb5 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -203,3 +203,20 @@ textarea, .mat-mdc-snack-bar-container.settings-snackbar { margin-bottom: calc(76px + env(safe-area-inset-bottom, 0px)); } + +// WCAG-friendly visually-hidden helper — content stays in the +// accessibility tree and DOM query results but occupies no layout box. +// Used where a visible heading would duplicate adjacent navigation +// context (e.g. Settings page title that the left rail already +// announces). +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html index 01afaf1cd..ed8593c74 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html @@ -1,18 +1,24 @@ -
- +
+ + @if (!isSettingsRoute()) { + + } @if (!hasNoPlaylists()) { } -
- - @if (headerShortcut(); as action) { + @if (!isSettingsRoute()) { +
- } - @if (headerBulkAction(); as bulk) { - - } -
+ @if (headerShortcut(); as action) { + + } + @if (headerBulkAction(); as bulk) { + + } +
+ } @if (isElectron()) {
diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts index f9fe6befa..19b2e85d3 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts @@ -48,6 +48,15 @@ export class WorkspaceShellHeaderComponent { readonly hasNoPlaylists = input(false); readonly isDownloadsView = input(false); readonly hasActiveDownloads = input(false); + /** + * When true the playlist switcher + the "+ Add source" / refresh / + * bulk-action buttons are hidden — those controls scope to a + * playlist, but Settings is a global page, so leaving them visible + * implies (falsely) that switching the playlist changes which + * settings you're editing. Driven from the shell facade's existing + * isSettingsRoute computed. + */ + readonly isSettingsRoute = input(false); readonly searchChanged = output(); readonly searchSubmitted = output(); diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html index eb8015a2e..bedc6afd2 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html @@ -39,6 +39,7 @@ [hasNoPlaylists]="facade.hasNoPlaylists()" [isDownloadsView]="facade.isDownloadsView()" [hasActiveDownloads]="facade.hasActiveDownloads()" + [isSettingsRoute]="facade.isSettingsRoute()" (refreshPlaylistRequested)="facade.refreshCurrentPlaylist()" (searchChanged)="facade.onSearchInput($event)" (searchSubmitted)="facade.onSearchEnter($event)" diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts index ac7d67980..98ef07231 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts @@ -57,6 +57,7 @@ class MockWorkspaceShellHeaderComponent { readonly hasNoPlaylists = input(false); readonly isDownloadsView = input(false); readonly hasActiveDownloads = input(false); + readonly isSettingsRoute = input(false); readonly headerBulkAction = input(null); readonly searchChanged = output(); readonly searchSubmitted = output();