* feat(ui): turn the phone context panel into an off-canvas drawer
On ≤640px viewports the workspace context panel (categories, filters,
settings sections, collection filters) no longer stacks above the route
content capped at 30vh — it is a hidden-by-default drawer that slides in
from the left over a backdrop, opened via a new header toggle
(phone-only, CSS-gated) and closed by selection, backdrop tap, Escape,
or any navigation.
State lives in the new WorkspaceShellContextDrawerService provided by
the shell component; panels close it explicitly after selections that
do not navigate (Stalker ITV/radio categories, settings sections,
sources filters, collection filters), since NavigationEnd alone cannot
cover those. Desktop behavior is untouched, including the
ResizableDirective inline width.
Closes the drawer follow-up deferred from #1100 / PR #1326.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): make the phone context drawer modal for keyboard users
Addresses Greptile P1 and Codex P2 review feedback on #1332:
- CdkTrapFocus on the sidebar captures focus into the drawer on open and
contains it while the drawer is modal; the shell restores focus to the
header toggle on close, since the closed drawer is visibility: hidden
and focus left inside it would silently drop to <body>.
- The drawer service closes the drawer when the viewport leaves the
phone breakpoint (matchMedia), so the trap can never hold the in-flow
desktop sidebar after a resize.
- The toggle's tooltip and aria-label are now variant-aware — categories
on portal routes, filters on sources/collection routes, settings
sections on the settings route — instead of a fixed 'Categories &
filters' that misdescribed two of the three; the two generic i18n keys
are replaced by six variant keys across all 19 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): remove background content from the a11y tree while the drawer is open
Round-2 review feedback on #1332 (Greptile P1, Codex P2):
- The rail, header, route content and playback footer are marked inert
while the phone drawer is open — CdkTrapFocus constrains Tab focus,
but a screen reader's virtual cursor could still reach and activate
the visually obscured controls behind the backdrop.
- The drawer panel itself is the trap's initial focus target
(tabindex=-1 + cdkFocusInitial), so focus capture still works when a
category list is loading, empty, or failed and renders no focusable
rows.
- Focus restore on close is deferred one tick: the toggle lives in the
inert header, and focus() on a still-inert element is silently
ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): gate global shortcuts and Escape behind the open phone drawer
Round-3 review feedback on #1332 (Codex P2s):
- The shell consumes Escape while the drawer is open: downstream Escape
consumers (the portal detail shell's inline player close, the shared
controls shortcuts) check defaultPrevented, so one keypress no longer
closes both the drawer and the obscured playback surface.
- inert does not silence document-level keydown listeners, so players
opt out themselves while inside an inert region: ControlsShortcuts
gains an optional hostElement handler and ignores every shortcut
(including Escape) when that host has an inert ancestor, and the radio
audio player applies the same check to its volume/mute keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): close control, Cmd+F gate, and Embedded MPV inert guard for the drawer
Round-4 review feedback on #1332 (Greptile P1, Codex P2s):
- The drawer carries its own phone-only close button: touch
screen-reader users have no hardware Escape and cannot reach the inert
header toggle or the aria-hidden backdrop, so the trapped surface must
offer dismissal itself — even when a category list is loading or
empty and renders no actionable entries.
- Ctrl/Cmd+F no longer opens global search while the drawer is modal;
the shortcut would have navigated and focused an input inside the
inert header.
- EmbeddedMpvShortcuts (native-view legacy dock) gains the same
hostElement/inert-ancestor guard as the shared controls shortcuts, so
the obscured player cannot react to Space/arrows/M/Escape behind the
drawer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): round-5 drawer feedback + update phone-layout e2e for the drawer
Merges master (#1333 landed mobile-layout.e2e.ts pinning the #1326
stacked-panel behavior this PR replaces) and updates that spec to pin
the drawer contract instead: panel hidden by default with full-width
content, header toggle opens it over a backdrop, category selection and
backdrop tap close it. Verified locally on Chromium, Firefox and WebKit
(12/12). The spec's getByTestId calls needed plain [data-test-id=...]
locators — the web-e2e Playwright config never mapped testIdAttribute.
Also addresses Codex round-5 P2s:
- Focus restore now reports whether the toggle received focus; when a
drawer selection navigated to a route without a context panel (toggle
gone), focus falls back to the route content instead of dropping to
<body>.
- The Xtream and Stalker live layouts' Ctrl/Cmd+B sidebar shortcut opts
out while their host sits inside an inert region, matching the other
document-level listeners.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): suppress command palette and shortcuts dialog behind the open drawer
Greptile round-6 finding on #1332: the document-level Ctrl/Cmd+K
handler in WorkspaceShellFacade and the '?' help-key handler in
WorkspaceKeyboardShortcutsService still opened their dialogs while the
phone context drawer was modal, stacking a second focus-trapped surface
on top of it. Both now check the drawer service (injected optionally,
same shell-component providers) and stay quiet while it is open, like
the Ctrl/Cmd+F global-search gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): round-7 drawer feedback — Cmd+R gate and native MPV surface hiding
Addresses the two Codex round-7 P2s on #1332:
- WorkspaceShellContextDrawerService moves to @iptvnator/workspace/shell/util
and becomes root-provided, so AppComponent's document-level Ctrl/Cmd+R
global-recent shortcut can observe the modal drawer without pulling the
lazy shell chunk into the eager bundle. Cmd+R is now suppressed while
the drawer is open, like Cmd+F/Cmd+K/'?'.
- The shell registers the open drawer with a new
EmbeddedMpvOverlayVisibilityService.acquireExternalModalSurface() API:
the native-view video surface is composited outside DOM stacking and
would paint straight over the drawer regardless of z-index. The service
treats registered external modal surfaces exactly like open Material
dialogs.
- The service's recompute no longer reads overlayActive back before
setting it: signals already skip notification on equal values, and that
hidden read registered overlayActive as a dependency of any reactive
context calling into the service — the shell's acquire/release effect
looped forever on exactly that (caught by a live browser probe; the
unit suite mocked the service). The effect also wraps the acquire in
untracked() for caller-side hygiene.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): expose the phone drawer as a named modal dialog
Round-8 review feedback on #1332 (Codex P2s):
- While open, the drawer carries role=dialog, aria-modal=true, and a
variant-appropriate accessible name (categories / filters / settings
sections) — assistive technology now hears that a named modal surface
opened instead of an unnamed complementary landmark. Closed (and the
always-visible desktop sidebar) stays a plain landmark.
- The UI-guidelines drawer section no longer claims the drawer service
is component-provided; it is root-provided from workspace/shell/util
since the round-7 move, and the stale claim could have led a future
change to re-scope it and silently break the AppComponent shortcut
gate and the Embedded MPV overlay observer. Matching code comments
updated everywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): round-9 — gate M3U player keys behind the drawer, raise drawer stacking
Greptile round-9 P1 + Codex round-9 P2 on #1332:
- The M3U video player's document-level digit-key channel switching and
Ctrl/Cmd+B sidebar toggle now apply the same inert-ancestor guard as
every other routed-content key listener. A codebase sweep confirms
this closes the class: every document-level key listener on routed
content is now either gated by the shell (Escape, Cmd+F/K/R, '?') or
opts out via closest('[inert]'); the guidelines now require the guard
for any new listener.
- The drawer moves from z-index 99/98 to 951/950: above the settings
action bar (100) and the root EPG/update panels (900/901), which
inert removes from interaction but not from paint order — below the
CDK overlay container (1000), since dialogs opened from inside the
drawer (Manage categories) must stack on top of it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(lint): resolve module-boundary and prefer-inject errors
Retag workspace-shell-util as type:data-access to match its injectable
services that depend on @iptvnator/services, and convert
RemoteControlService to inject(HttpClient).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(lint): enforce max-lines 400 with generated baseline
Add a max-lines ESLint error (hard cap 400 raw lines per TypeScript
file) per the repo file-size rule. The 134 pre-existing offenders are
baselined in tools/eslint/max-lines-baseline.mjs, regenerable via
generate-max-lines-baseline.mjs; the list should only shrink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(ci): enforce lint on PRs and guard coverage policy drift
- Add a Lint job to ci.yml running nx run-many -t lint --all, so
module-boundary tags, legacy-alias bans, and max-lines gate merges.
- Fix the root lint script (was linting only electron-backend).
- Add tools/coverage/check-coverage-policy.mjs: fails CI when a project
with a test target is missing from coverage-policy.json; wired into
coverage:ci as coverage:policy:check.
- Run Tier B/C unit tests in CI without coverage (list derived from the
policy), so website/packaging/remote-control tests run on PRs.
- Replace the hand-picked 16-project test:unit:ci list with --all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: document CI lint enforcement and coverage policy guard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): address bot review feedback on policy guard and baseline generator
- Drive Tier B/C validation from each policy entry's validationCommand
(falling back to nx test), skipping projects with an e2e target since
the E2E workflow already runs them (Codex).
- Fail when a Tier A entry has no test target (Greptile, adapted:
checking all entries against test targets would false-positive on the
intentionally spec-less e2e/mock-server tiers).
- Guard against missing JSON array in nx show projects output (Greptile).
- Scan .tsx files in the max-lines baseline generator to match the
ESLint rule's file patterns (Greptile).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Introduced a loading state in the source stats widget with a spinner.
- Updated styles for the loading state in the SCSS file.
- Created unit tests for the source stats widget to verify loading behavior.
- Enhanced the source stats widget component to include a progress spinner.
- Added a new service for managing workspace startup preferences.
- Implemented logic to determine the initial workspace path based on user settings.
- Updated workspace shell header to include a refresh playlist button.
- Refactored workspace shell rail component to support dynamic brand link and tooltip.
- Enhanced workspace shell facade to manage playlist refresh actions.
- Improved loading overlay UI in the workspace shell component.
Entire-Checkpoint: 271bd0162027
- Introduced `jest.config.ts` for testing setup.
- Added `workspace-search-capability.ts` to define search capabilities.
- Created `workspace-shell-route.utils.ts` for parsing and handling workspace routes.
- Implemented tests for route utilities in `workspace-shell-route.utils.spec.ts`.
- Updated `index.ts` to export new navigation utilities.
- Configured TypeScript for testing with `tsconfig.spec.json`.
- Set up test environment in `test-setup.ts`.
Entire-Checkpoint: 130fa99dcbb4