mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
UI-24 from the UI consistency audit.
- No-artwork slides paint their gradient in CSS from the slide hue: a light
tint in the light theme, unchanged near-black in the dark one. The dark
gradient under the light page-coloured scrim read as a grey slab.
- The side scrim holds 88% of the page colour up to the slide's right edge
(inset + min(560px, 55%)), so the end of a full slide no longer sits on
about 45%.
- Narrow layout (container <= 720px): a full-bleed 90% scrim behind the text
block, a scrim-coloured text shadow, and an entrance without a fade so
that scrim never flashes the art on a rotation.
- --hero-body is 85% of the heading colour (was 72%).
- Light --app-rating-color #a16207 -> #7a4a00: measured 3.36:1 on the chip
over artwork, now 5.10:1. The details pages share the chip and token.
- Buttons cap at the slide width and end long labels in an ellipsis.
- The page gets one visually hidden h1 ("Dashboard"); slide titles are h2.
- One live region outside the re-created slide announces slide changes;
progress bars are named and VOD ones read "N% watched"; dots are 24px.
dashboard-hero-legibility.e2e.ts replaces every image with a checkerboard
and measures each piece of slide text from the screen in both themes, wide
and narrow, for backdrop, poster, no-artwork and live slides. On master the
worst cases were 2.35:1 (body text) and 2.65:1 (pills).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
363 lines
14 KiB
SCSS
363 lines
14 KiB
SCSS
@use 'sass:map';
|
|
@use '@angular/material' as mat;
|
|
@use 'styles';
|
|
|
|
@include mat.elevation-classes();
|
|
@include mat.app-background();
|
|
|
|
// ─── Typography ─────────────────────────────────────────────────────────────
|
|
// The stack is declared once on html; every Material typography token and the
|
|
// typography hierarchy read it through the variable. Inlined, the 103-char
|
|
// stack was copied into ~90 declarations (about 7 KB of the initial CSS).
|
|
$app-font-stack-value:
|
|
"'DM Sans', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
|
$app-font-stack: var(--app-font-family);
|
|
|
|
html {
|
|
--app-font-family: #{$app-font-stack-value};
|
|
}
|
|
|
|
$my-typography: mat.m2-define-typography-config(
|
|
$font-family: $app-font-stack,
|
|
);
|
|
@include mat.typography-hierarchy($my-typography);
|
|
|
|
// ─── M3 Themes ───────────────────────────────────────────────────────────────
|
|
// One primary color across the app. Azure is the closest M3 prebuilt to the
|
|
// product's "active/selection" blue (#2f7bff / #78adff), so checkboxes, radio
|
|
// buttons, raised CTAs, etc. now read as the same blue as rail selection.
|
|
//
|
|
// Cyan stays on tertiary so the EPG "now" indicator (which uses Material
|
|
// tertiary surfaces) keeps its existing visual role.
|
|
$light-theme: mat.define-theme(
|
|
(
|
|
color: (
|
|
theme-type: light,
|
|
primary: mat.$azure-palette,
|
|
tertiary: mat.$cyan-palette,
|
|
),
|
|
typography: (
|
|
plain-family: $app-font-stack,
|
|
brand-family: $app-font-stack,
|
|
),
|
|
)
|
|
);
|
|
|
|
$dark-theme: mat.define-theme(
|
|
(
|
|
color: (
|
|
theme-type: dark,
|
|
primary: mat.$azure-palette,
|
|
tertiary: mat.$cyan-palette,
|
|
),
|
|
typography: (
|
|
plain-family: $app-font-stack,
|
|
brand-family: $app-font-stack,
|
|
),
|
|
)
|
|
);
|
|
|
|
// ─── Apply Themes ────────────────────────────────────────────────────────────
|
|
html {
|
|
@include mat.all-component-themes($light-theme);
|
|
|
|
// The theme is built with the legacy `define-theme` config, whose
|
|
// component mixins emit resolved component tokens but never declare the
|
|
// `--mat-sys-*` system variables. App styles reference those variables
|
|
// directly, so declare them explicitly for each theme context; without
|
|
// this every such reference silently resolves to nothing.
|
|
@include mat.system-level-colors($light-theme);
|
|
@include mat.system-level-typography($light-theme);
|
|
@include mat.system-level-elevation($light-theme);
|
|
@include mat.system-level-shape($light-theme);
|
|
@include mat.system-level-state($light-theme);
|
|
|
|
// ── Light theme design tokens ─────────────────────────────────────────
|
|
--app-shell-bg: #eef0f3;
|
|
--app-rail-bg: #eef0f3;
|
|
--app-header-bg: #eef0f3;
|
|
--app-content-bg: #f5f6f8;
|
|
--app-rail-border: transparent;
|
|
--app-on-surface: #1d1b1e;
|
|
--app-hover-overlay: rgba(0, 0, 0, 0.06);
|
|
--app-search-bg: rgba(255, 255, 255, 0.7);
|
|
--app-search-border: rgba(0, 0, 0, 0.1);
|
|
--app-selection-color: #2f7bff;
|
|
--app-selection-on-color: #eef4ff;
|
|
--app-selection-surface: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 14%,
|
|
transparent
|
|
);
|
|
--app-selection-surface-strong: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 22%,
|
|
transparent
|
|
);
|
|
--app-selection-border: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 52%,
|
|
transparent
|
|
);
|
|
--app-selection-glow: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 34%,
|
|
transparent
|
|
);
|
|
// Watch progress in app chrome (rail cards, catalog capsules, the hero).
|
|
// Over video the player's fixed `--pc-progress` applies instead.
|
|
--app-progress-color: var(--app-selection-color);
|
|
|
|
// Live / on-air accent (broadcast indicator)
|
|
--app-live-color: #d12c2c;
|
|
|
|
// Content area
|
|
--app-content-shadow: -2px 4px 12px rgba(0, 0, 0, 0.06);
|
|
|
|
// Widget surfaces — no borders, layered backgrounds
|
|
--app-widget-bg: #ffffff;
|
|
--app-widget-header-bg: #fafbfc;
|
|
--app-widget-border: transparent;
|
|
--app-widget-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.04);
|
|
--app-widget-header-border: rgba(0, 0, 0, 0.06);
|
|
--app-card-hover-bg: #f0f2f5;
|
|
--app-separator: rgba(0, 0, 0, 0.14);
|
|
|
|
// Typography — subdued, not pure black
|
|
--app-heading-color: #1a1d23;
|
|
--app-body-color: #4a5060;
|
|
--app-muted-color: #8891a4;
|
|
--app-eyebrow-color: #7c8495;
|
|
|
|
// Provider accent colors
|
|
--app-provider-xtream: #2f7bff;
|
|
--app-provider-stalker: #10b981;
|
|
--app-provider-m3u: #d97706;
|
|
|
|
// Subscription-expiry chips (dashboard source cards). Text/surface pairs
|
|
// are picked per theme to keep >= 4.5:1 small-text contrast against the
|
|
// composited chip background.
|
|
--app-expiry-warn-text: #8a5300;
|
|
--app-expiry-warn-surface: color-mix(in srgb, #d97706 16%, transparent);
|
|
--app-expiry-expired-text: #b3261e;
|
|
--app-expiry-expired-surface: color-mix(in srgb, #f44336 14%, transparent);
|
|
|
|
// Hero
|
|
--app-accent-gradient: linear-gradient(
|
|
135deg,
|
|
rgba(47, 123, 255, 0.08) 0%,
|
|
rgba(92, 214, 255, 0.05) 100%
|
|
);
|
|
--app-hero-surface: rgba(47, 123, 255, 0.04);
|
|
|
|
// Detail / hero call to action. The dark theme's light primary (#f2f4f8
|
|
// on #0a0e14) inverts to a dark button on the light theme, so it reads as
|
|
// the one solid CTA on either page background.
|
|
--app-cta-bg: #1a1d23;
|
|
--app-cta-fg: #f5f6f8;
|
|
--app-cta-hover-bg: #2a2f3a;
|
|
--app-cta-meta-fg: rgba(245, 246, 248, 0.68);
|
|
// Star rating chip: a deep amber, since the chip's 12px text needs
|
|
// 4.5:1 over hero artwork under the light scrim (#a16207 measured 3.4:1).
|
|
--app-rating-color: #7a4a00;
|
|
|
|
.dark-theme {
|
|
@include mat.all-component-colors($dark-theme);
|
|
// Only the color roles differ per theme: the elevation shadows are
|
|
// the same black in both, so html's declarations already apply here.
|
|
@include mat.system-level-colors($dark-theme);
|
|
|
|
// ── Dark graphite theme ────────────────────────────────────────────
|
|
// Inspired by both Slack (sidebar distinction) and GitButler (data density)
|
|
// Key principle: clearly distinct surface layers using graphite steps
|
|
// Darkest → lightest: #0d0f12 → #141720 → #1c2030 → #222840
|
|
|
|
// App shell & navigation
|
|
--app-shell-bg: #0d0f12;
|
|
--app-rail-bg: #111318;
|
|
--app-header-bg: #111318;
|
|
--app-content-bg: #161a22;
|
|
--app-rail-border: transparent;
|
|
--app-on-surface: #e6e1e5;
|
|
--app-hover-overlay: rgba(255, 255, 255, 0.08);
|
|
--app-search-bg: rgba(255, 255, 255, 0.05);
|
|
--app-search-border: rgba(255, 255, 255, 0.09);
|
|
--app-selection-color: #78adff;
|
|
--app-selection-on-color: #071225;
|
|
--app-selection-surface: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 18%,
|
|
transparent
|
|
);
|
|
--app-selection-surface-strong: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 28%,
|
|
transparent
|
|
);
|
|
--app-selection-border: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 62%,
|
|
transparent
|
|
);
|
|
--app-selection-glow: color-mix(
|
|
in srgb,
|
|
var(--app-selection-color) 44%,
|
|
transparent
|
|
);
|
|
// Re-declared so it resolves against this context's selection colour
|
|
// rather than inheriting the light value computed on `html`.
|
|
--app-progress-color: var(--app-selection-color);
|
|
|
|
// Live / on-air accent (broadcast indicator)
|
|
--app-live-color: #ff8f8f;
|
|
|
|
// Content area
|
|
--app-content-shadow: -4px 8px 20px rgba(0, 0, 0, 0.18);
|
|
|
|
// Widget surfaces — NO border, surface contrast does the work
|
|
// Widgets sit on content-bg (#161a22), so widget-bg is notably lighter
|
|
--app-widget-bg: #1e2330;
|
|
--app-widget-header-bg: #232840;
|
|
--app-widget-border: transparent;
|
|
--app-widget-shadow: none;
|
|
--app-widget-header-border: rgba(255, 255, 255, 0.05);
|
|
--app-card-hover-bg: #252b3a;
|
|
--app-separator: rgba(255, 255, 255, 0.14);
|
|
|
|
// Typography — NOT pure white. Warm graphite-tinted whites
|
|
--app-heading-color: #d8dce8; // soft off-white, not #fff
|
|
--app-body-color: #8b93a8; // muted blue-grey
|
|
--app-muted-color: #5c6478; // dimmed
|
|
--app-eyebrow-color: #6b7494; // eyebrow labels
|
|
|
|
// Provider accents (slightly brighter for dark bg)
|
|
--app-provider-xtream: #78adff;
|
|
--app-provider-stalker: #34d399;
|
|
--app-provider-m3u: #fbbf24;
|
|
|
|
// Subscription-expiry chips — brighter text for the dark surfaces;
|
|
// expired matches the --app-live-color accent family.
|
|
--app-expiry-warn-text: #fbbf24;
|
|
--app-expiry-warn-surface: color-mix(in srgb, #fbbf24 16%, transparent);
|
|
--app-expiry-expired-text: #ff8f8f;
|
|
--app-expiry-expired-surface: color-mix(
|
|
in srgb,
|
|
#ff8f8f 16%,
|
|
transparent
|
|
);
|
|
|
|
// Hero section
|
|
--app-accent-gradient: linear-gradient(
|
|
135deg,
|
|
rgba(120, 173, 255, 0.12) 0%,
|
|
rgba(92, 214, 255, 0.07) 100%
|
|
);
|
|
--app-hero-surface: rgba(30, 35, 48, 0.9);
|
|
|
|
// Detail / hero call to action (light primary) and rating accent
|
|
--app-cta-bg: #f2f4f8;
|
|
--app-cta-fg: #0a0e14;
|
|
--app-cta-hover-bg: #ffffff;
|
|
--app-cta-meta-fg: #5a6274;
|
|
--app-rating-color: #ffcf5c;
|
|
}
|
|
}
|
|
|
|
// ─── Global refined outlined inputs ──────────────────────────────────────────
|
|
// Applied app-wide AFTER mat.all-component-themes so these overrides win the
|
|
// cascade. Pairs with MAT_FORM_FIELD_DEFAULT_OPTIONS (outline + dynamic) set
|
|
// in app.config.ts.
|
|
$app-refined-input-density: mat.define-theme((
|
|
density: (scale: -2),
|
|
));
|
|
|
|
html {
|
|
@include mat.form-field-density($app-refined-input-density);
|
|
|
|
// Declared on the field itself (not on html) so the var(--app-*) values
|
|
// resolve inside .dark-theme instead of being inherited as light values.
|
|
// The resting and hover outlines keep the theme's outline roles: the app
|
|
// hairline tokens fall far under the 3:1 boundary contrast a field needs.
|
|
// Labels use the body token, which clears 4.5:1 in both themes.
|
|
// Text sizes stay on the theme defaults: this theme leaves the floated
|
|
// label's size token undeclared, so it inherits the field's text size and
|
|
// scales it to 75%; a 13px field would float a 10px label.
|
|
.mat-mdc-form-field {
|
|
@include mat.form-field-overrides(
|
|
(
|
|
outlined-focus-outline-color: var(--app-selection-color),
|
|
outlined-container-shape: 10px,
|
|
outlined-label-text-color: var(--app-body-color),
|
|
outlined-hover-label-text-color: var(--app-body-color),
|
|
outlined-focus-label-text-color: var(--app-selection-color),
|
|
outlined-input-text-color: var(--app-heading-color),
|
|
outlined-caret-color: var(--app-selection-color),
|
|
)
|
|
);
|
|
}
|
|
|
|
// Inputs sit slightly above the surrounding surface. color-mix is inlined
|
|
// here (not via a custom property on html) so var(--app-widget-bg)
|
|
// resolves at the wrapper — inside .dark-theme — rather than being
|
|
// locked to the light-mode value at html-level inheritance.
|
|
.mat-form-field-appearance-outline .mat-mdc-text-field-wrapper {
|
|
background: color-mix(in srgb, var(--app-widget-bg) 85%, white 7%);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.mat-mdc-form-field .mat-mdc-input-element,
|
|
.mat-mdc-form-field .mat-mdc-select-value {
|
|
font-size: 13.5px;
|
|
}
|
|
|
|
// ─── Destructive actions ───────────────────────────────────────────────
|
|
// Material only emits `.mat-warn` button colors for M2 themes, so the
|
|
// `warn` color input is a no-op with this M3 theme. Buttons that remove or
|
|
// discard user data opt in with this class instead; the error tokens are
|
|
// declared per theme context above, so light and dark each get their own
|
|
// error/on-error pair.
|
|
.app-destructive-button {
|
|
@include mat.button-overrides(
|
|
(
|
|
filled-container-color: var(--mat-sys-error),
|
|
filled-label-text-color: var(--mat-sys-on-error),
|
|
filled-state-layer-color: var(--mat-sys-on-error),
|
|
filled-ripple-color:
|
|
color-mix(in srgb, var(--mat-sys-on-error) 12%, transparent),
|
|
text-label-text-color: var(--mat-sys-error),
|
|
text-state-layer-color: var(--mat-sys-error),
|
|
text-ripple-color:
|
|
color-mix(in srgb, var(--mat-sys-error) 12%, transparent),
|
|
outlined-label-text-color: var(--mat-sys-error),
|
|
outlined-outline-color: var(--mat-sys-error),
|
|
outlined-state-layer-color: var(--mat-sys-error),
|
|
outlined-ripple-color:
|
|
color-mix(in srgb, var(--mat-sys-error) 12%, transparent),
|
|
)
|
|
);
|
|
@include mat.icon-button-overrides(
|
|
(
|
|
icon-color: var(--mat-sys-error),
|
|
state-layer-color: var(--mat-sys-error),
|
|
ripple-color:
|
|
color-mix(in srgb, var(--mat-sys-error) 12%, transparent),
|
|
)
|
|
);
|
|
}
|
|
|
|
.mat-mdc-form-field-subscript-wrapper {
|
|
font-size: 11px;
|
|
letter-spacing: 0.01em;
|
|
margin-top: 2px;
|
|
|
|
.mat-mdc-form-field-hint-wrapper,
|
|
.mat-mdc-form-field-error-wrapper {
|
|
padding: 0 12px;
|
|
}
|
|
|
|
.mat-mdc-form-field-hint {
|
|
color: var(--app-muted-color);
|
|
}
|
|
}
|
|
}
|