mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 10:56:43 -08:00
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>
608 lines
14 KiB
SCSS
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;
|
|
}
|
|
}
|