From 0d6192f20e0dcc0647e5da640a09a89ff9bc7279 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 30 Jul 2026 12:50:48 +0200 Subject: [PATCH] docs(skills): tighten validated guidance --- .codex/skills/iptvnator-theme-style/SKILL.md | 4 +++- .codex/skills/stalker-portal/SKILL.md | 20 ++++++++++--------- docs/architecture/iptvnator-ui-guidelines.md | 8 ++++++++ docs/architecture/stalker-portal.md | 11 +++++----- .../src/lib/stalker-vod.utils.spec.ts | 20 +++++++++++++++++++ .../data-access/src/lib/stalker-vod.utils.ts | 5 +---- libs/ui/styles/_content-grid.scss | 6 ++++-- libs/ui/styles/_portal-layout.scss | 6 ++++-- libs/ui/styles/_portal-sidebar.scss | 6 ++++-- 9 files changed, 61 insertions(+), 25 deletions(-) diff --git a/.codex/skills/iptvnator-theme-style/SKILL.md b/.codex/skills/iptvnator-theme-style/SKILL.md index 55cd394e5..d986bcc45 100644 --- a/.codex/skills/iptvnator-theme-style/SKILL.md +++ b/.codex/skills/iptvnator-theme-style/SKILL.md @@ -40,7 +40,9 @@ consumers currently use relative `@use` paths to the needed partial. of copied SCSS. - In an Electron drag region, every interactive descendant—buttons, links, inputs, overlays, and resize handles—must explicitly use - `app-region: no-drag`. + `app-region: no-drag`. The shared directive-generated `.resize-handle` does + not set this centrally yet; consumers in drag regions must cover it + themselves and must not assume the generated handle opts out. - A shared change must be checked across M3U, Xtream, Stalker, workspace, portal catalog/shared UI, and unified collections where relevant. diff --git a/.codex/skills/stalker-portal/SKILL.md b/.codex/skills/stalker-portal/SKILL.md index 7b97a1bd0..832b833b7 100644 --- a/.codex/skills/stalker-portal/SKILL.md +++ b/.codex/skills/stalker-portal/SKILL.md @@ -24,16 +24,18 @@ must remain provider-neutral. ## Series Contract -Inside Stalker portal code, `isStalkerSeriesFlag()` is the sole flag -interpretation and accepts exactly `true`, `1`, and `'1'`. The activity -normalizer in shared interfaces remains dependency-neutral and preserves the -same closed set for favorites/recent and dashboard classification. Preserve -all three modes: regular `/series`, VOD with embedded `series[]`, and lazy -Ministra VOD `is_series`. +Inside Stalker portal code, `isStalkerSeriesFlag()` is the canonical predicate +and accepts exactly `true`, `1`, and `'1'`. `normalizeStalkerSeriesFlag()` +delegates to it and produces the normalized positive marker `true` or +`undefined`. The activity normalizer in shared interfaces keeps its +dependency-neutral equivalent for favorites/recent and dashboard +classification. Preserve all three modes: regular `/series`, VOD with embedded +`series[]`, and lazy Ministra VOD `is_series`. -Favorites/recent preserve the raw flag and VOD origin so reopening still uses -the correct lazy or embedded mode. Keep quick-start translation parameters and -the naturally ordered season fallback when `season_number` is absent. +Favorites/recent preserve the normalized positive marker and VOD origin so +reopening still uses the correct lazy or embedded mode. Keep quick-start +translation parameters and the naturally ordered season fallback when +`season_number` is absent. Lazy episodes use a deterministic tracking ID scoped by parent series, provider episode, season key, and episode number. `legacyTrackingId` is only a diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index bf08dce48..5b9be58a7 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -116,6 +116,14 @@ Do not copy the full detail-view stylesheet into feature libraries. Add shared layout changes to the mixin, and keep provider-specific differences explicit in the wrapper file that includes it. +## Electron Drag Regions + +Every interactive descendant of a drag region—including buttons, links, +inputs, overlays, and resize handles—requires `app-region: no-drag`. The shared +directive-generated `.resize-handle` does not set this centrally yet. Until +that debt is fixed, consumers in drag regions must cover the handle themselves +and must not assume it already opts out. + ## Channel List Item The shared row should be reused instead of rebuilding channel markup per view. diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index e238a09ce..5b5351365 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -268,11 +268,12 @@ list: Stalker has multiple real-world data shapes. The current implementation supports all three: Within Stalker portal data access and feature code, -`isStalkerSeriesFlag()` is the sole interpretation of `is_series`. The -dependency-neutral activity normalizer in `libs/shared/interfaces` preserves -the same closed set for dashboard records: boolean `true`, numeric `1`, or -string `'1'`. Unsupported values do not by themselves classify a VOD item as a -series. +`isStalkerSeriesFlag()` is the canonical predicate for `is_series`. +`normalizeStalkerSeriesFlag()` delegates to it and produces the normalized +positive marker `true` or `undefined`. The activity normalizer in +`libs/shared/interfaces` keeps its dependency-neutral equivalent for dashboard +records. Both accept the same closed set: boolean `true`, numeric `1`, or string +`'1'`. Unsupported values do not by themselves classify a VOD item as a series. 1. Regular Series (`/series`): diff --git a/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts b/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts index c37614014..cda2d7219 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts @@ -5,11 +5,31 @@ import { createStalkerInfo, createStalkerInlineDetailState, createStalkerDetailViewState, + isStalkerSeriesFlag, normalizeStalkerFavoriteItem, + normalizeStalkerSeriesFlag, toggleStalkerVodFavorite, } from './stalker-vod.utils'; describe('stalker-vod.utils regressions', () => { + describe('Stalker series flag contract', () => { + it.each([true, 1, '1'])( + 'accepts %p and normalizes it to the positive marker', + (value) => { + expect(isStalkerSeriesFlag(value)).toBe(true); + expect(normalizeStalkerSeriesFlag(value)).toBe(true); + } + ); + + it.each([false, 0, '0', 'true', null, undefined, {}, []])( + 'rejects unsupported value %p', + (value) => { + expect(isStalkerSeriesFlag(value)).toBe(false); + expect(normalizeStalkerSeriesFlag(value)).toBeUndefined(); + } + ); + }); + it('routes embedded series[] items to series view state', () => { const state = createStalkerDetailViewState( { diff --git a/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts b/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts index b70c28e65..bd0ae96ae 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts @@ -54,10 +54,7 @@ export function isStalkerSeriesFlag(value: unknown): boolean { export function normalizeStalkerSeriesFlag( value: unknown ): StalkerSeriesFlag | undefined { - if (value === true || value === 1 || value === '1') { - return true; - } - return undefined; + return isStalkerSeriesFlag(value) ? true : undefined; } export function isStalkerSeriesItem(item: { diff --git a/libs/ui/styles/_content-grid.scss b/libs/ui/styles/_content-grid.scss index 096df6fb9..8906db632 100644 --- a/libs/ui/styles/_content-grid.scss +++ b/libs/ui/styles/_content-grid.scss @@ -1,7 +1,9 @@ // Shared content grid and card styles // -// Usage (after adding libs/ui/styles to stylePreprocessorOptions.includePaths): -// @use 'content-grid' as grid; +// 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; diff --git a/libs/ui/styles/_portal-layout.scss b/libs/ui/styles/_portal-layout.scss index f2338e805..5075f3333 100644 --- a/libs/ui/styles/_portal-layout.scss +++ b/libs/ui/styles/_portal-layout.scss @@ -4,8 +4,10 @@ // Include this mixin in a component's SCSS to get the shared flex host+sidebar // skeleton, then add only the component-specific rules underneath. // -// Usage (after adding libs/ui/styles to stylePreprocessorOptions.includePaths): -// @use 'portal-layout' as portal; +// Choose the relative @use path from each consuming stylesheet. Current portal +// live layouts use the example below; other consumers may need a different +// depth. +// @use '../../../../../../ui/styles/portal-layout' as portal; // @include portal.live-layout; @mixin live-layout { diff --git a/libs/ui/styles/_portal-sidebar.scss b/libs/ui/styles/_portal-sidebar.scss index 44813b427..3902d5d02 100644 --- a/libs/ui/styles/_portal-sidebar.scss +++ b/libs/ui/styles/_portal-sidebar.scss @@ -1,8 +1,10 @@ // ─── Shared sidebar + sidebar-header styles ─────────────────────────────────── // Plain CSS rules (not a mixin) used for M3U/legacy sidebar layouts. // -// Usage (after adding libs/ui/styles to stylePreprocessorOptions.includePaths): -// @use 'portal-sidebar'; (no alias needed — no exported members to call) +// Choose the relative @use path from each consuming stylesheet. Current portal +// live layouts use the example below; other consumers may need a different +// depth. No alias is needed because this partial exports no callable members. +// @use '../../../../../../ui/styles/portal-sidebar'; .sidebar { // Width is controlled by the resizable directive