Files
iptvnator/apps/web/src/app/settings/settings.component.scss
T
4grayandClaude Fable 5.1 1f036d2a8e feat(settings): grouped navigation, cards and switches from the settings concept
Implements the Claude Design "Settings concept" handoff.

Navigation: the sidebar is a 20px "Settings" title over App, Library, Devices
and Data groups, with About pinned to the footer beside the installed version
and an update badge. The active item is a soft fill without a border. Esc
leaves settings like the header Back. Reset is no longer a page: its one
destructive action is the last card of "Backup & data".

Pages: a title and one-line subtitle, then rows grouped into titled cards
with one right-aligned control column. Selects are compact without floating
labels, checkboxes are switches, segmented controls are pills, and
descriptions are capped at 56ch. "Show subtitles" moves to Playback, the
Embedded MPV note becomes a callout under the player select (only while it
is selected), and the TMDB attribution becomes the About footer.

EPG: Add and Refresh all sit in the Sources header, the empty state has its
own Add, the format examples are one line, Clear EPG data is its own row and
the offset is a stepper. About: version, update state and channel share one
card; the nightly warning is a callout shown only while Nightly is selected,
with a shortcut to the backup page; the support buttons are neutral with
coloured icons.

Save model: the save bar stays (design option B) and now floats over the
content column, counts the staged changes, answers Cmd/Ctrl+S, and marks
pages with staged edits in the sidebar. A saved change that waits for a
restart shows a dismissible notice.

Rows stack under 600px of pane width (the page is a size container, so the
persistent sidebar is accounted for). Page-specific styles moved into the
About, EPG, Backup and Remote control section stylesheets to stay within the
component style budget. New SETTINGS keys are translated in all 19 locales
and NAV_RESET is removed; brand names and loanwords that stay English are in
the identical-value baseline.

E2E: Material slide toggles report aria-checked after the next render, so
both suites toggle through a setSwitch helper; the settings nav is a
navigation landmark, so the Dashboard rail link is scoped to the rail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 18:10:59 +02:00

608 lines
14 KiB
SCSS

// Settings page: one content column of titled cards. Every row is a label
// column plus one right-aligned control column, so selects, switches and
// segmented controls line up down the page. Tokens and the Material token
// overrides (compact switch, dense select) live in `_settings-theme.scss`.
.settings-page-host {
display: block;
}
.settings-container {
height: auto;
overflow: visible;
// Rows stack by the pane's width, not the viewport's: the sidebar keeps
// the page narrow on small desktop windows. This block is not the
// scroll owner, so inline-size containment costs it nothing.
container-type: inline-size;
}
.settings-layout {
position: relative;
display: flex;
flex-direction: column;
gap: 20px;
align-items: stretch;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
padding: 28px 40px calc(48px + var(--settings-safe-bottom));
}
.settings-content {
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
// ─── Page header ───────────────────────────────────────────────────────────
.settings-group {
display: flex;
flex-direction: column;
gap: 28px;
}
.settings-group__header {
h2 {
margin: 0;
font-size: 26px;
font-weight: 600;
letter-spacing: -0.3px;
line-height: 1.2;
color: var(--app-heading-color);
}
p {
margin: 6px 0 0;
color: var(--app-body-color);
font-size: 13.5px;
line-height: 1.45;
}
}
// ─── Sections and cards ────────────────────────────────────────────────────
.settings-section {
display: block;
}
.settings-section__header {
display: flex;
align-items: center;
gap: 10px;
margin: 0 2px 10px;
min-height: 32px;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--app-heading-color);
}
}
.settings-section__count {
padding: 1px 7px;
border-radius: 999px;
background: var(--settings-chip-bg);
color: var(--settings-chip-fg);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.settings-section__actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.settings-section__desc {
margin: -4px 2px 10px;
color: var(--settings-muted);
font-size: 12.5px;
line-height: 1.5;
max-width: 72ch;
}
.settings-card {
background: var(--settings-card-bg);
border: 1px solid var(--settings-card-border);
border-radius: 12px;
}
.settings-card--danger {
border-color: var(--settings-item-danger-border);
}
// ─── Rows ──────────────────────────────────────────────────────────────────
.setting-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 24px;
align-items: center;
min-height: 60px;
padding: 14px 16px;
position: relative;
}
.setting-item + .setting-item,
.setting-item__sub + .setting-item,
.setting-item + .setting-item__sub + .setting-item {
border-top: 1px solid var(--settings-item-border);
}
// Meta stacked over a full-width control (textareas, lists). Flex children
// do not grow from `justify-content`, so the field itself takes the row.
.setting-item--full-control {
grid-template-columns: 1fr;
gap: 12px;
.setting-item__control {
justify-content: stretch;
> .mat-mdc-form-field {
width: 100%;
}
}
}
.setting-item--list {
display: block;
padding: 0;
}
// A callout under the row it explains (player runtime note, nightly warning).
.setting-item__sub {
padding: 0 16px 14px;
}
.setting-item__meta {
min-width: 0;
h4 {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin: 0;
font-size: 13.5px;
font-weight: 500;
color: var(--app-heading-color);
}
p {
margin: 3px 0 0;
max-width: 56ch;
color: var(--settings-muted);
font-size: 12.5px;
line-height: 1.5;
text-wrap: pretty;
}
a {
color: var(--app-selection-color);
}
}
.setting-item__control {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
min-width: 0;
}
.setting-item__control--stack {
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.setting-item__toggle {
justify-content: flex-end;
}
// Row revealed from settings search or the command palette.
.setting-item[data-setting-id],
.settings-card[data-setting-id],
.settings-foot[data-setting-id] {
scroll-margin: 96px 0 120px;
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid
color-mix(in srgb, var(--app-selection-color) 60%, transparent);
outline-offset: 2px;
border-radius: 12px;
}
}
.setting-item--revealed {
animation: settings-reveal-fade 2.4s ease-out forwards;
}
@keyframes settings-reveal-fade {
0%,
60% {
background: color-mix(
in srgb,
var(--app-selection-color) 10%,
transparent
);
}
100% {
background: transparent;
}
}
@media (prefers-reduced-motion: reduce) {
.setting-item--revealed {
animation: none;
}
}
// ─── Controls ──────────────────────────────────────────────────────────────
// Compact select without a floating label: the row label names it.
.settings-select.mat-mdc-form-field {
width: 220px;
.mat-mdc-text-field-wrapper {
background: var(--settings-control-bg);
}
.mat-mdc-select-value,
.mat-mdc-input-element {
font-size: 13px;
}
}
.settings-text-field.mat-mdc-form-field {
width: min(100%, 340px);
}
.settings-chip {
display: inline-flex;
align-items: center;
padding: 1px 7px;
border-radius: 999px;
background: var(--settings-chip-bg);
color: var(--settings-chip-fg);
font-size: 10.5px;
font-weight: 500;
line-height: 1.5;
white-space: nowrap;
}
.settings-chip--success {
background: color-mix(in srgb, #4ade80 14%, transparent);
color: var(--app-provider-stalker);
}
.settings-chip--accent {
background: var(--app-selection-surface);
color: var(--app-selection-color);
}
.settings-chip--warn {
background: var(--app-expiry-warn-surface);
color: var(--app-expiry-warn-text);
}
.settings-callout {
display: flex;
gap: 10px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid var(--settings-callout-info-border);
background: var(--settings-callout-info-bg);
color: var(--settings-muted);
font-size: 12.5px;
line-height: 1.5;
> mat-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
font-size: 16px;
margin-top: 1px;
color: var(--app-selection-color);
}
b {
display: block;
margin-bottom: 2px;
color: var(--app-heading-color);
font-weight: 600;
}
a {
color: var(--app-selection-color);
}
}
.settings-callout--warn {
border-color: var(--settings-callout-warn-border);
background: var(--settings-callout-warn-bg);
> mat-icon {
color: var(--settings-callout-warn-icon);
}
}
.settings-callout__actions {
display: flex;
gap: 8px;
margin-top: 10px;
}
// Segmented control (theme, cover size, guide layout): a pill.
.theme-switcher {
display: inline-flex;
padding: 3px;
border: 1px solid var(--settings-control-border);
border-radius: 999px;
background: var(--settings-control-bg);
}
.theme-switcher__option {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--app-body-color);
font: inherit;
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
transition:
background 0.15s ease,
color 0.15s ease;
mat-icon {
width: 14px;
height: 14px;
font-size: 14px;
}
&:hover {
color: var(--app-heading-color);
}
&:focus-visible {
outline: 2px solid var(--app-selection-color);
outline-offset: 1px;
}
}
.theme-switcher__option--selected {
background: var(--app-selection-surface);
color: var(--app-selection-color);
}
// ─── Buttons in rows ───────────────────────────────────────────────────────
.inline-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
}
.button-spinner {
display: inline-block;
margin-right: 6px;
vertical-align: middle;
}
.inline-actions__label {
display: inline-flex;
align-items: center;
}
.inline-actions__button--busy {
background: var(--app-selection-surface);
}
// TMDB attribution / disclaimers under the last card (About, Metadata,
// Parental lock).
.settings-foot {
display: flex;
align-items: center;
gap: 10px;
color: var(--settings-muted);
font-size: 11.5px;
line-height: 1.6;
img {
height: 14px;
}
}
// ─── Restart notice ────────────────────────────────────────────────────────
.settings-restart-bar {
position: sticky;
top: 0;
z-index: 5;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px 8px 16px;
border: 1px solid var(--settings-card-border);
border-radius: 10px;
background: var(--settings-restart-bar-bg);
color: var(--app-body-color);
font-size: 12.5px;
mat-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
font-size: 16px;
color: var(--app-selection-color);
}
span {
flex: 1;
min-width: 0;
}
}
// ─── Save bar ──────────────────────────────────────────────────────────────
// Only present while the form is dirty. It floats over the content column
// (sticky, so it never covers the shell rail) and carries the change count.
.unsaved-bar {
position: sticky;
bottom: calc(16px + var(--settings-safe-bottom));
z-index: 100;
display: flex;
align-items: center;
gap: 12px;
width: min(100%, 720px);
margin: 0 auto;
padding: 10px 10px 10px 16px;
box-sizing: border-box;
border: 1px solid var(--settings-action-bar-border);
border-radius: 12px;
background: var(--settings-action-bar-bg);
box-shadow: var(--settings-action-bar-shadow);
animation: unsaved-bar-enter 0.2s cubic-bezier(0.16, 1, 0.3, 1);
@media (prefers-reduced-motion: reduce) {
animation: none;
}
}
@keyframes unsaved-bar-enter {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.unsaved-bar__dot {
flex-shrink: 0;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--app-selection-color);
}
.unsaved-bar__message {
flex: 1;
min-width: 0;
color: var(--app-heading-color);
font-size: 13px;
}
.unsaved-bar__actions {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.settings-save-button.mat-mdc-button-base {
font-weight: 600;
}
.settings-kbd {
margin-left: 8px;
padding: 1px 5px;
border-radius: 4px;
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10.5px;
font-weight: 500;
opacity: 0.8;
}
// ─── Narrow panes ──────────────────────────────────────────────────────────
// Below 600px of content width a row cannot hold its label beside a 220px
// control, so the control drops under the label.
@container (max-width: 600px) {
.settings-layout {
padding: 20px 16px calc(24px + var(--settings-safe-bottom));
}
.settings-group__header h2 {
font-size: 22px;
}
.setting-item {
grid-template-columns: 1fr;
gap: 12px;
}
.setting-item__control,
.setting-item__toggle {
justify-content: flex-start;
}
.setting-item__control--stack {
align-items: flex-start;
}
.settings-select.mat-mdc-form-field,
.settings-text-field.mat-mdc-form-field {
width: 100%;
}
// Translated option labels (ru, de) do not fit a one-line pill on a
// phone: the segments take the row and wrap when they must.
.theme-switcher {
width: 100%;
flex-wrap: wrap;
border-radius: 14px;
}
.theme-switcher__option {
flex: 1 1 auto;
min-width: 0;
}
.settings-section__header {
flex-wrap: wrap;
}
.settings-section__actions {
flex-basis: 100%;
margin-left: 0;
}
.unsaved-bar {
flex-wrap: wrap;
bottom: calc(8px + var(--settings-safe-bottom));
}
// Long translations ("Enregistrer les modifications") do not fit two
// buttons side by side on a phone: stack them, Save first.
.unsaved-bar__actions {
flex-basis: 100%;
flex-direction: column-reverse;
align-items: stretch;
margin-left: 0;
button {
width: 100%;
}
}
// A keyboard-only affordance has no place on a phone.
.settings-kbd {
display: none;
}
}