Files
iptvnator/apps/web/src/m3-theme.scss
T
4grayandClaude Fable 5 a6186a46c8 feat(dashboard): subscription-expiry warning badge on source cards (#1342)
* feat(dashboard): warn on source cards when a portal subscription expires soon

Dashboard source cards now carry a passive expiry chip: amber "Expires in
N d" within 7 days of the subscription lapsing, error-toned "Expired" once
it has. Account details stay behind the card's ⋮ → Account info.

Xtream expirations ride on the playlist switcher's cached PortalStatusService
check — checkPortalStatusDetails() now surfaces the parsed exp_date from the
same round-trip, so the dashboard adds no extra portal calls. Stalker
expirations come from the stalkerAccountInfo snapshot persisted at import;
it lives in the playlist payload (meta rows carry payload: null), so each
Stalker source costs one full-playlist read memoized on the playlist's
update timestamp.

New i18n keys added to all 19 locales via the i18n-fill merger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): address review feedback on expiry badges

- Recompute expiry badges on a minute tick so a dashboard left open
  crosses day-countdown and expiration boundaries (Greptile P1 / Codex P2)
- Gate the expiry refresh on the recent-sources rail setting so hidden
  rails cost no portal checks or playlist reads (Codex P2)
- Move chip colors to theme-aware tokens in m3-theme.scss; both themes
  now hold >= 4.5:1 small-text contrast (light warn 5.3:1, light expired
  5.4:1, dark warn 7.4:1, dark expired 6.0:1) (Codex P2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): make expiry-badge labels depend on the language signal

sourceCards previously relied on getPlaylistProvider's indirect language
read; the translate.instant() labels now read languageTick explicitly
(mirroring trendingCards). Also shift the minute tick by one so the
interval's first 0 differs from initialValue — the signal equality check
was swallowing the first heartbeat, delaying it to two minutes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:24:16 +02:00

272 lines
9.9 KiB
SCSS

@use 'sass:map';
@use '@angular/material' as mat;
@use 'styles';
@include mat.elevation-classes();
@include mat.app-background();
// ─── Typography ─────────────────────────────────────────────────────────────
$app-font-stack:
"'DM Sans', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
$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);
// ── 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
);
// 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);
.dark-theme {
@include mat.all-component-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
);
// 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);
}
}
// ─── 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);
.mat-mdc-form-field {
--mdc-outlined-text-field-outline-color: var(--app-widget-header-border);
--mdc-outlined-text-field-hover-outline-color: var(--app-muted-color);
--mdc-outlined-text-field-focus-outline-color: var(--app-selection-color);
--mdc-outlined-text-field-container-shape: 10px;
--mdc-outlined-text-field-label-text-color: var(--app-muted-color);
--mdc-outlined-text-field-input-text-color: var(--app-heading-color);
--mdc-outlined-text-field-caret-color: var(--app-selection-color);
--mdc-outlined-text-field-label-text-size: 13px;
--mdc-outlined-text-field-input-text-size: 13.5px;
}
// 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;
}
.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);
}
}
}