mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
style(ui): ADR and one global kit layer for menus, select panels and destructive buttons (#1936)
* docs(ui): record the Angular Material role decision in the UI guidelines Angular Material stays the behaviour and accessibility layer; the look comes from the design kit through mat.*-overrides() on --app-* tokens in the shared partials, kit-only components build on CDK, and plain actions may become native buttons. Records the measured Material and CDK share of the initial bytes, why a replacement would not pay, and the v22 behaviour changes to keep covered in E2E. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * style(ui): one global kit layer for menus, select panels and destructive buttons - `<mat-menu class="app-menu">` gets the kit panel from one rule in styles.scss; the two lazy components drop their `::ng-deep` copies. - Select option panels follow the same panel: bordered, 10px radius, 6px padding, 36px rows at 13px, the chosen row on the list-selection tokens. - `.app-destructive-button` is the kit's red tint on the `--app-danger-*` tokens (tinted fill, red text, no ripple, 40% opacity when disabled) instead of a solid error fill, for filled, text, outlined and icon buttons. - The backdrop blur is scoped to the dark backdrop the CDK gives modal overlays; menus, selects and popovers open over a transparent backdrop that no longer matches the rule. - `mat.elevation-classes()` is dropped: nothing uses `.mat-elevation-z*`, and its 25 rules cost 4.4 KB of the initial stylesheet. The stack now measures 1,595,843 initial bytes (was 1,597,764). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(e2e): wait for focus to enter an opened menu before navigating it Material focuses a menu's first item after the next render, which the zoneless scheduler runs on a later task than the keydown that opened it. The panel is visible before that, so End sent as soon as it appeared still reached the trigger: on the Linux runner the Sources row-menu delete test then found "Remove playlist" inactive on every retry. `waitForMenuFocus` polls for the active element inside the open menu; the Sources keyboard and switcher keyboard tests use it before their first navigation key. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
88765c7bf0
commit
3b2af73baa
10 files changed
+253
-59
No files matched your search
@@ -0,0 +1,8 @@
|
||||
---
|
||||
type: fix
|
||||
area: ui
|
||||
---
|
||||
|
||||
Buttons that delete or discard data now show as a red tint with red text
|
||||
instead of a solid red block, in both themes. The option lists of the Settings
|
||||
dropdowns use the same compact, bordered panel as the app's menus.
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, Locator, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Wait until keyboard focus has entered the open `mat-menu`, and return it.
|
||||
*
|
||||
* Material moves focus to the first item after the next render, which the
|
||||
* zoneless scheduler runs on a later task than the keydown that opened the
|
||||
* menu. The panel is visible before that, so a navigation key sent as soon
|
||||
* as it appears still reaches the trigger and the menu never sees it.
|
||||
*/
|
||||
export async function waitForMenuFocus(page: Page): Promise<Locator> {
|
||||
const menu = page.getByRole('menu');
|
||||
await expect(menu).toBeVisible();
|
||||
await expect
|
||||
.poll(
|
||||
() => menu.evaluate((el) => el.contains(document.activeElement)),
|
||||
{ message: 'focus inside the open menu' }
|
||||
)
|
||||
.toBe(true);
|
||||
return menu;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
writeTemporaryM3uFile,
|
||||
xtreamMockServer,
|
||||
} from './electron-test-fixtures';
|
||||
import { waitForMenuFocus } from './menu-keyboard';
|
||||
import {
|
||||
routePlayableStreams,
|
||||
startAndConfirmPlayback,
|
||||
@@ -572,8 +573,7 @@ test.describe('Electron Playlist Switcher', () => {
|
||||
await expect(search).toBeFocused();
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('Shift+F10');
|
||||
const rowMenu = page.locator('.mat-mdc-menu-panel');
|
||||
await expect(rowMenu).toBeVisible();
|
||||
const rowMenu = await waitForMenuFocus(page);
|
||||
await page.keyboard.press('End');
|
||||
await expect(
|
||||
rowMenu.getByRole('menuitem', { name: /Remove/ })
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
waitForM3uCatalog,
|
||||
writeTemporaryM3uFile,
|
||||
} from './electron-test-fixtures';
|
||||
import { waitForMenuFocus } from './menu-keyboard';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A source row opens from the keyboard. Each row has its own Tab stop: the
|
||||
@@ -217,6 +218,7 @@ test.describe('Electron Sources keyboard', () => {
|
||||
name: 'Remove playlist',
|
||||
});
|
||||
await expect(remove).toBeVisible();
|
||||
await waitForMenuFocus(page);
|
||||
await page.keyboard.press('End');
|
||||
await expect(remove).toBeFocused();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
@@ -47,16 +47,27 @@ async function outlineContrast(
|
||||
}, fillSelector);
|
||||
}
|
||||
|
||||
/** Resolves a CSS color (including var() and color-mix()) to rgb()/rgba(). */
|
||||
async function resolveColor(page: Page, value: string): Promise<string> {
|
||||
return page.evaluate((css) => {
|
||||
const probe = document.createElement('div');
|
||||
probe.style.color = css;
|
||||
document.body.appendChild(probe);
|
||||
const color = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
return color;
|
||||
}, value);
|
||||
/**
|
||||
* Resolves a CSS color (including var() and color-mix()) the way the given
|
||||
* property serializes it, so it compares with a computed `color` or
|
||||
* `backgroundColor`.
|
||||
*/
|
||||
async function resolveColor(
|
||||
page: Page,
|
||||
value: string,
|
||||
property: 'color' | 'backgroundColor' = 'color'
|
||||
): Promise<string> {
|
||||
return page.evaluate(
|
||||
({ css, prop }) => {
|
||||
const probe = document.createElement('div');
|
||||
probe.style[prop] = css;
|
||||
document.body.appendChild(probe);
|
||||
const color = getComputedStyle(probe)[prop];
|
||||
probe.remove();
|
||||
return color;
|
||||
},
|
||||
{ css: value, prop: property }
|
||||
);
|
||||
}
|
||||
|
||||
async function systemVariable(page: Page, name: string): Promise<string> {
|
||||
@@ -200,7 +211,7 @@ test.describe('Theme tokens', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@theme @electron destructive actions name the action and use the error color', async ({
|
||||
test('@theme @electron destructive actions name the action and use the danger tint', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const app = await launchElectronApp(dataDir);
|
||||
@@ -211,18 +222,35 @@ test.describe('Theme tokens', () => {
|
||||
|
||||
for (const theme of ['light', 'dark'] as const) {
|
||||
await applyTheme(page, theme);
|
||||
const error = await resolveColor(page, 'var(--mat-sys-error)');
|
||||
const dangerText = await resolveColor(
|
||||
page,
|
||||
'var(--app-danger-text)'
|
||||
);
|
||||
const dangerSurface = await resolveColor(
|
||||
page,
|
||||
'var(--app-danger-surface)',
|
||||
'backgroundColor'
|
||||
);
|
||||
const trigger = page.getByRole('button', {
|
||||
name: 'Clear EPG data',
|
||||
exact: true,
|
||||
});
|
||||
// The `warn` color input was a no-op with M3: the trigger
|
||||
// rendered in the primary color.
|
||||
// rendered in the primary color. The kit draws a destructive
|
||||
// button as a red tint, never a solid error fill, and the
|
||||
// outlined trigger carries the same tint as a filled one.
|
||||
await expect
|
||||
.poll(() =>
|
||||
trigger.evaluate((el) => getComputedStyle(el).color)
|
||||
)
|
||||
.toBe(error);
|
||||
.toBe(dangerText);
|
||||
await expect
|
||||
.poll(() =>
|
||||
trigger.evaluate(
|
||||
(el) => getComputedStyle(el).backgroundColor
|
||||
)
|
||||
)
|
||||
.toBe(dangerSurface);
|
||||
await trigger.click();
|
||||
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
@@ -239,7 +267,12 @@ test.describe('Theme tokens', () => {
|
||||
(el) => getComputedStyle(el).backgroundColor
|
||||
)
|
||||
)
|
||||
.toBe(error);
|
||||
.toBe(dangerSurface);
|
||||
await expect
|
||||
.poll(() =>
|
||||
confirm.evaluate((el) => getComputedStyle(el).color)
|
||||
)
|
||||
.toBe(dangerText);
|
||||
|
||||
await dialog
|
||||
.getByRole('button', { name: 'Cancel', exact: true })
|
||||
|
||||
+88
-22
@@ -3,7 +3,8 @@
|
||||
@use '@angular/material' as mat;
|
||||
@use 'styles';
|
||||
|
||||
@include mat.elevation-classes();
|
||||
// No `mat.elevation-classes()`: nothing in the app uses the `.mat-elevation-z*`
|
||||
// utilities, and the 25 rules cost 4.4 KB of the initial stylesheet.
|
||||
@include mat.app-background();
|
||||
|
||||
// ─── Typography ─────────────────────────────────────────────────────────────
|
||||
@@ -407,39 +408,104 @@ html {
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
// A select stays inside a form; its option panel follows the kit menu:
|
||||
// a bordered 10px panel with 6px padding, 36px rows at 13px and the
|
||||
// chosen row on the list-selection tokens. The overrides sit on the
|
||||
// panel, which renders in the overlay container inside the theme
|
||||
// context, so the `--app-*` values resolve per theme. The selector is
|
||||
// doubled because Material sets the panel's padding and attached
|
||||
// corners from `div.mat-mdc-select-panel` and
|
||||
// `.mat-mdc-select-panel-above div.mat-mdc-select-panel`, injected after
|
||||
// the global stylesheet.
|
||||
div.mat-mdc-select-panel.mat-mdc-select-panel {
|
||||
@include mat.select-overrides(
|
||||
(
|
||||
panel-background-color: var(--app-widget-bg),
|
||||
container-elevation-shadow: 0 18px 40px rgba(0, 0, 0, 0.35),
|
||||
)
|
||||
);
|
||||
@include mat.option-overrides(
|
||||
(
|
||||
label-text-size: 13px,
|
||||
label-text-color: var(--app-heading-color),
|
||||
selected-state-label-text-color: var(--app-list-selected-fg),
|
||||
selected-state-layer-color: var(--app-list-selected-bg),
|
||||
hover-state-layer-color:
|
||||
color-mix(in srgb, var(--app-on-surface) 8%, transparent),
|
||||
focus-state-layer-color:
|
||||
color-mix(in srgb, var(--app-on-surface) 10%, transparent),
|
||||
)
|
||||
);
|
||||
padding: 6px;
|
||||
border: 1px solid var(--app-line-strong);
|
||||
border-radius: 10px;
|
||||
|
||||
.mat-mdc-option {
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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.
|
||||
// discard user data opt in with this class instead. The kit draws them as
|
||||
// a red tint (`--app-danger-*`, declared per theme context above), never
|
||||
// a solid error fill: hover changes the fill only, the ripple is off and
|
||||
// a disabled button is the same button at 40% opacity.
|
||||
.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),
|
||||
filled-container-color: var(--app-danger-surface),
|
||||
filled-disabled-container-color: var(--app-danger-surface),
|
||||
filled-label-text-color: var(--app-danger-text),
|
||||
filled-disabled-label-text-color: var(--app-danger-text),
|
||||
filled-state-layer-color: var(--app-danger-text),
|
||||
filled-hover-state-layer-opacity: 0.08,
|
||||
filled-focus-state-layer-opacity: 0.08,
|
||||
filled-pressed-state-layer-opacity: 0.12,
|
||||
filled-ripple-color: transparent,
|
||||
text-label-text-color: var(--app-danger-text),
|
||||
text-disabled-label-text-color: var(--app-danger-text),
|
||||
text-state-layer-color: var(--app-danger-text),
|
||||
text-hover-state-layer-opacity: 0.08,
|
||||
text-focus-state-layer-opacity: 0.08,
|
||||
text-pressed-state-layer-opacity: 0.12,
|
||||
text-ripple-color: transparent,
|
||||
outlined-label-text-color: var(--app-danger-text),
|
||||
outlined-disabled-label-text-color: var(--app-danger-text),
|
||||
outlined-outline-color: transparent,
|
||||
outlined-disabled-outline-color: transparent,
|
||||
outlined-state-layer-color: var(--app-danger-text),
|
||||
outlined-hover-state-layer-opacity: 0.08,
|
||||
outlined-focus-state-layer-opacity: 0.08,
|
||||
outlined-pressed-state-layer-opacity: 0.12,
|
||||
outlined-ripple-color: 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),
|
||||
icon-color: var(--app-danger-text),
|
||||
disabled-icon-color: var(--app-danger-text),
|
||||
state-layer-color: var(--app-danger-text),
|
||||
hover-state-layer-opacity: 0.08,
|
||||
focus-state-layer-opacity: 0.08,
|
||||
pressed-state-layer-opacity: 0.12,
|
||||
ripple-color: transparent,
|
||||
)
|
||||
);
|
||||
|
||||
// The kit has no outlined buttons: an outlined destructive button
|
||||
// carries the same tint as a filled one.
|
||||
&.mat-mdc-outlined-button {
|
||||
background-color: var(--app-danger-surface);
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&.mat-mdc-button-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-subscript-wrapper {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@use '@angular/material' as mat;
|
||||
@use '../../../libs/ui/components/src/lib/resizable/resizable.scss';
|
||||
@use '../../../libs/ui/styles/focus-ring';
|
||||
@use '../../../libs/ui/styles/app-controls' as controls;
|
||||
@use './settings-theme';
|
||||
@use './cover-size';
|
||||
|
||||
@@ -238,10 +239,20 @@ body.dark-theme {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.cdk-overlay-backdrop {
|
||||
// Modal overlays blur the page behind their backdrop. The CDK gives a dialog
|
||||
// the dark backdrop class by default; menus, selects and popovers open over
|
||||
// `cdk-overlay-transparent-backdrop`, which sits at opacity 0 and stays a
|
||||
// plain layer without a filter of its own.
|
||||
.cdk-overlay-dark-backdrop {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
// UI-kit menu panel: `<mat-menu class="app-menu">`. The panel renders in the
|
||||
// overlay container, outside every component view, so the rule is global.
|
||||
.mat-mdc-menu-panel.app-menu {
|
||||
@include controls.menu-panel;
|
||||
}
|
||||
|
||||
// Command palette dialog — the palette-shell renders its own background, border,
|
||||
// radius and shadow, so strip MDC's dialog surface chrome to avoid a darker
|
||||
// halo peeking out behind the rounded corners.
|
||||
|
||||
@@ -22,6 +22,54 @@ Use it when changing existing views or introducing new list-based UI in the work
|
||||
5. Scroll ownership must be explicit.
|
||||
Headers stay visible. Lists scroll. Do not let nested panes compete for scroll.
|
||||
|
||||
## Angular Material's Role
|
||||
|
||||
Decision of 2026-10-12. The repository has no ADR folder; decisions live in
|
||||
the subsystem's document.
|
||||
|
||||
**Decision.**
|
||||
|
||||
- Angular Material stays as the behaviour and accessibility layer: dialog,
|
||||
menu, select (inside forms only), form field and input, snack-bar, tooltip,
|
||||
slide-toggle, checkbox, progress and icon.
|
||||
- The look comes from the IPTVnator design kit, applied through
|
||||
`mat.*-overrides()` on the `--app-*` tokens in one place: the shared
|
||||
partials under `libs/ui/styles/` (the control mixins live in
|
||||
`_app-controls.scss`), not per-component `::ng-deep`.
|
||||
- A kit component that Material lacks is built on CDK primitives (overlay,
|
||||
a11y key managers), as the playlist switcher popover is.
|
||||
- A plain action button may become a native `<button>` with the kit mixins.
|
||||
- No wholesale replacement of Material. Re-evaluate Angular's headless
|
||||
`@angular/aria` (developer preview as of v21; not installed) for menu,
|
||||
listbox and combobox once it is stable.
|
||||
|
||||
**Why** (production build, `renderer.initialBytes` 1,597,763 bytes):
|
||||
|
||||
- Material's JavaScript is about 203 KB (12.7% of the initial bytes) and the
|
||||
CDK's about 126 KB (7.9%). Any replacement keeps the CDK, and the CDK is
|
||||
where the behaviour lives.
|
||||
- In the global CSS the `--mat-*` tokens are 96 KB raw but about 11 KB
|
||||
gzipped, and Material's own rules are 9 KB. 23.6 KB of the tokens belong to
|
||||
17 components the app never imports, because `mat.all-component-themes`
|
||||
emits every component.
|
||||
- The startup cost came from a few eager imports (the form-field defaults
|
||||
token in `app.config.ts`, the startup, update and EPG-progress components,
|
||||
the confirm-dialog service), not from Material itself. Those are gone (see
|
||||
Material off the startup path below): `renderer.initialBytes` went from
|
||||
1,597,763 to 1,345,807 with Material still in place.
|
||||
- Rewriting dialog focus trapping, menu keyboard handling, select listboxes
|
||||
and snack-bar live regions would cost months and accessibility risk for no
|
||||
visible gain.
|
||||
|
||||
**Consequences.**
|
||||
|
||||
- Material upgrades can change behaviour. Seen in v22: the menu panel has no
|
||||
max-height; a submenu is offset by the parent's first direct
|
||||
`mat-menu-item`, so a trigger that follows `role="group"` rows is wrapped
|
||||
in `<div role="none">`; overlay panes are `position: static` inside the
|
||||
bounding box.
|
||||
- Keep E2E coverage on menus and dialogs.
|
||||
|
||||
## Canonical References
|
||||
|
||||
- Channel row:
|
||||
@@ -78,8 +126,9 @@ in `apps/web/src/m3-theme.scss`):
|
||||
active source's row on Sources and in the switcher (see Selection Pattern)
|
||||
- `--app-line-strong` — popover and menu borders, a step above
|
||||
`--app-widget-header-border`
|
||||
- `--app-danger-text` / `--app-danger-surface` — a failing source's tag and
|
||||
destructive menu items; `--app-neutral-tag-surface` for a neutral tag
|
||||
- `--app-danger-text` / `--app-danger-surface` — a failing source's tag,
|
||||
destructive menu items and `.app-destructive-button`;
|
||||
`--app-neutral-tag-surface` for a neutral tag
|
||||
- `--app-font-mono` — URLs and keyboard hints (JetBrains Mono, 400/500)
|
||||
- `--app-popover-shadow` / `--app-popover-backdrop` — the switcher popover
|
||||
|
||||
@@ -243,6 +292,15 @@ selection blue on its own tint is 3.3:1) and the selection colour in dark.
|
||||
|
||||
## Single-Choice Menus And Status Chips
|
||||
|
||||
A `<mat-menu class="app-menu">` gets the kit panel from one global rule in
|
||||
`styles.scss` (`menu-panel` in `libs/ui/styles/_app-controls.scss`): a
|
||||
bordered 10px panel with 6px padding, 36px rows at 13px with 16px icons, the
|
||||
checked row on the list-selection tokens, a destructive row marked
|
||||
`app-menu-item--danger` after a divider, and a shortcut hint in a trailing
|
||||
`kbd.app-menu-item__kbd`. The panel renders in the overlay container, outside
|
||||
every component view, so a component never copies the mixin into its own
|
||||
stylesheet.
|
||||
|
||||
A `mat-menu` that picks one value, such as a sort order or a rating
|
||||
threshold, uses `appMenuItemRadio` and `appMenuItemRadioCheck` from
|
||||
`@iptvnator/ui/components`:
|
||||
@@ -1097,14 +1155,17 @@ per-theme tokens in `apps/web/src/_settings-theme.scss`.
|
||||
- **Destructive actions.** Material only emits `warn` button colors for M2
|
||||
themes, so the `color` input is a no-op here. A button that removes or
|
||||
discards user data uses the global `.app-destructive-button` class from
|
||||
`m3-theme.scss` (error/on-error tokens per theme, for filled, text,
|
||||
outlined and icon buttons), as the unsaved-changes dialog's Discard does.
|
||||
`m3-theme.scss`, as the unsaved-changes dialog's Discard does. It is the
|
||||
kit's red tint on the `--app-danger-*` tokens per theme (tinted fill and
|
||||
red text for filled and outlined buttons, red text for text and icon
|
||||
buttons), never a solid error fill; hover changes the fill only, and a
|
||||
disabled button is the same button at 40% opacity.
|
||||
Confirmations go through `DialogService.openConfirmDialog` with a
|
||||
translated verb as the required `confirmLabel` ("Remove playlist",
|
||||
"Clear") and `tone: 'destructive'` when the action loses data; the dismiss
|
||||
defaults to "Cancel". Never confirm with "Yes"/"No". When the verb itself
|
||||
is "Cancel …", pass `cancelLabel` "Close" so the two buttons do not read
|
||||
alike. `theme-tokens.e2e.ts` checks the label and the error fill in both
|
||||
alike. `theme-tokens.e2e.ts` checks the label and the danger tint in both
|
||||
themes.
|
||||
- **One control per action.** A page offers one control for a destructive
|
||||
action. Clearing a favorites or recently viewed tab goes through
|
||||
@@ -1123,6 +1184,12 @@ provide the defaults, because the token import there would load Material's
|
||||
form field and `@angular/forms` on startup. `pnpm run
|
||||
ui:form-field-defaults:validate` (in CI) reports a missing provider.
|
||||
|
||||
A `mat-select` belongs inside a form. Its option panel is styled once in
|
||||
`m3-theme.scss` to match the kit menu (a bordered 10px panel, 36px rows at
|
||||
13px, the chosen row on the list-selection tokens). A picker in a toolbar is
|
||||
not a select: it is a secondary button with a chevron that opens an
|
||||
`app-menu`.
|
||||
|
||||
The add-source forms (M3U URL, Xtream, Stalker) and the edit dialog share one
|
||||
vocabulary, so a field reads the same wherever it appears:
|
||||
|
||||
|
||||
@@ -357,10 +357,3 @@ app-source-tile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// UI-kit menu panel. The panel renders in the overlay container, so the
|
||||
// rule is global; it ships with the components that use `class="app-menu"`
|
||||
// rather than in the app's initial stylesheet.
|
||||
::ng-deep .mat-mdc-menu-panel.app-menu {
|
||||
@include controls.menu-panel;
|
||||
}
|
||||
-7
@@ -123,10 +123,3 @@
|
||||
.sort-failing-first__check--off {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// UI-kit menu panel. The panel renders in the overlay container, so the
|
||||
// rule is global; it ships with the components that use `class="app-menu"`
|
||||
// rather than in the app's initial stylesheet.
|
||||
::ng-deep .mat-mdc-menu-panel.app-menu {
|
||||
@include controls.menu-panel;
|
||||
}
|
||||
Reference in new issue
Block a user