mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
* fix(ui): visible keyboard focus everywhere via a global focus-visible fallback The global stylesheet removed the outline from every input, button, textarea and `:focus`, so Tab users saw no focus on about 110 raw buttons: detail actions, season tabs, chips, title results, list rows. - Remove the outline only under `:focus:not(:focus-visible)` and draw a fallback ring on every other `:focus-visible` element. Both rules sit in `:where()`, so any component that styles its own focus still wins. - One recipe: `focus-ring-declarations` moves to `libs/ui/styles/_focus-ring.scss` and reads `--app-focus-ring`, declared per theme (light #1d63e0, dark #8cbaff) with at least 3:1 on every app surface and selection tint; `m3-theme.spec.ts` measures it. The card ring, the detail actions, the portal sidebar and both context panels use the mixin (the panels' selection-blue ring fell to 2.97:1 on the active item). - Material Tab stops (buttons, switches, button toggles, checkboxes) take the ring over their 12% focus state layer; menu items and options keep Material's highlight. - Surfaces over video (player controls, vendor chrome, fullscreen panels, Up Next rail) point the ring at the player's text colour. - The selected season tab drew its border with `outline`, which outranks the fallback; it is a spread shadow now. - Guard: `pnpm run styles:focus-visible:validate` (CI) rejects a global `outline: none` on an unscoped selector and a missing fallback. - Electron E2E `keyboard-focus-ring.e2e.ts` tabs through the detail actions and season tabs, a catalog grid with its refinement chips, the Sources list and Settings in both themes: one ring per stop, 3:1 where measurable, none after a click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): keep the player ring in the fullscreen panel; tighten the focus guard and E2E Local review round 1 (Codex P2, Greptile 3×P2): - The fullscreen channel panel carries `dark-theme`, whose context declares the theme ring again, so its own controls ringed in #8cbaff. Point the token back at the player's text colour on `.fullscreen-channel-panel .dark-theme`; the web series-playback E2E checks the close button's ring. - The guard took a container-scoped rule (`.panel :focus-visible`), a mixin body or a media query as the global fallback. The fallback is now the whole selector, outside any at-rule. - The keyboard-focus E2E now fails a ring that an `overflow: hidden` ancestor (up to the scroll container) cuts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): show focus where a component removes its outline; catch invisible rings in the guard GitHub review round on #1866 (Codex P2, Greptile P2): - A component rule that sets `outline: none` outranks the zero-specificity fallback. Re-audited every one: two hid a Tab stop with nothing else to show. The EPG list row (`role="button"`) now draws an inset ring, and the command palette underlines its search row while the field has focus. The others already show focus on the field's wrapper, on another element, or as a highlight inside an arrow-key composite (the "…" menu, the guide's search listbox); the guidelines now state the rule. - The guard read only a bare zero or `none` as a removal. It now reads a zero width, a `none`/`hidden` style or a transparent colour anywhere in the shorthand or longhands, so `outline: 0 solid transparent` is reported and `outline: 2px solid transparent` is no fallback. - The keyboard-focus E2E walks the live EPG list and the command palette too, and reads an inset or offset shadow line as a ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(player): keep --pc-text a literal palette colour Master's palette spec (#1854) reads `--pc-text` from the controls host as a literal; this branch had made it `#{palette.$text}`, failing "sets the timeline label on the dense glass" on the merge ref. The host declares the literal again, and a spec keeps the palette's `$text` (the focus ring token for surfaces beside the host) equal to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(epg): declare the ui-styles dependency of the list row's focus ring The EPG list row now `@use`s `libs/ui/styles/_focus-ring.scss`; Nx cannot infer a Sass import, so `ui-epg` declares `ui-styles` like the other consumers of the shared partials. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): a styleless outline shorthand hides focus too `outline: 2px` or `outline: red` resets the style to `none`, yet the guard counted either as a visible fallback. A shorthand without a drawn style (or a `var()` that may carry one) now counts as removing the outline, as do `initial` and `unset`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
4.5 KiB
SCSS
192 lines
4.5 KiB
SCSS
// Shared content grid and card styles
|
|
//
|
|
// Choose a relative @use path from each consuming stylesheet. A current
|
|
// portal-shared component uses its local forwarding module as shown below;
|
|
// direct consumers of this partial may need a different depth.
|
|
// @use '../../styles/content-grid' as grid;
|
|
// @include grid.content-grid;
|
|
// @include grid.content-card;
|
|
|
|
@use 'focus-ring';
|
|
|
|
@mixin content-grid(
|
|
$min-width: var(--cover-grid-min-width, 148px),
|
|
$gap: var(--cover-gap, 16px)
|
|
) {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(#{$min-width}, 1fr));
|
|
gap: $gap;
|
|
}
|
|
|
|
@mixin content-card {
|
|
cursor: pointer;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
|
|
&:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
|
}
|
|
}
|
|
|
|
@mixin thumbnail-wrapper {
|
|
position: relative;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
|
|
.poster {
|
|
width: 100%;
|
|
aspect-ratio: 2/3;
|
|
object-fit: cover;
|
|
display: block;
|
|
}
|
|
|
|
.poster-placeholder {
|
|
width: 100%;
|
|
aspect-ratio: 2/3;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(128, 128, 128, 0.1);
|
|
|
|
mat-icon {
|
|
font-size: 36px;
|
|
width: 36px;
|
|
height: 36px;
|
|
opacity: 0.25;
|
|
}
|
|
}
|
|
}
|
|
|
|
@mixin type-badge {
|
|
position: absolute;
|
|
top: 6px;
|
|
left: 6px;
|
|
padding: 3px 6px;
|
|
border-radius: 4px;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: #fff;
|
|
|
|
&.live {
|
|
color: #ef5350;
|
|
}
|
|
&.radio {
|
|
color: #ffa726;
|
|
}
|
|
&.movie {
|
|
color: #42a5f5;
|
|
}
|
|
&.series {
|
|
color: #66bb6a;
|
|
}
|
|
}
|
|
|
|
@mixin remove-button {
|
|
position: absolute;
|
|
top: 4px;
|
|
right: 4px;
|
|
z-index: 1;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
width: 28px;
|
|
height: 28px;
|
|
padding: 0;
|
|
|
|
&:hover {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
mat-icon {
|
|
color: white;
|
|
font-size: 18px;
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
}
|
|
|
|
// Hover/focus caption for the posters-only wall (Settings > General >
|
|
// "Show titles under covers" off). Placed inside the poster wrapper, which
|
|
// must be `position: relative` with `overflow: hidden`; the consumer reveals
|
|
// it on `:hover`/`:focus-visible` and pins it with `--pinned` whenever the
|
|
// cover cannot identify the item (default poster, placeholder).
|
|
@mixin cover-title-overlay {
|
|
position: absolute;
|
|
inset: auto 0 0 0;
|
|
box-sizing: border-box;
|
|
padding: 28px 8px 8px;
|
|
color: #fff;
|
|
font-size: 0.78rem;
|
|
font-weight: 500;
|
|
line-height: 1.3;
|
|
text-align: center;
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(0, 0, 0, 0) 0%,
|
|
rgba(0, 0, 0, 0.72) 50%,
|
|
rgba(0, 0, 0, 0.88) 100%
|
|
);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
// Entrance under 300ms, ease-out: the caption answers a hover, it is
|
|
// not a state change worth choreographing.
|
|
transition: opacity 150ms ease-out;
|
|
|
|
// The clamp lives on a child: `overflow: hidden` clips at the padding
|
|
// edge, so a clamped element with its own padding paints the third
|
|
// line into that padding.
|
|
.cover-title-overlay__text {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
word-break: break-word;
|
|
}
|
|
|
|
&.cover-title-overlay--pinned {
|
|
opacity: 1;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
// The app focus ring at the cards' 3px offset. Draw it on the element that
|
|
// owns `overflow: hidden` (or on one of its ancestors): an outline on a
|
|
// clipped descendant is cut on every edge.
|
|
@mixin card-focus-ring-declarations {
|
|
@include focus-ring.focus-ring-declarations($offset: 3px);
|
|
}
|
|
|
|
@mixin card-focus-ring {
|
|
&:focus-visible {
|
|
@include card-focus-ring-declarations;
|
|
}
|
|
}
|
|
|
|
@mixin card-info {
|
|
padding: 8px 10px;
|
|
|
|
h3 {
|
|
margin: 0;
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
line-height: 1.3;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.date-info {
|
|
margin: 4px 0 0;
|
|
font-size: 0.7rem;
|
|
opacity: 0.6;
|
|
}
|
|
}
|