diff --git a/.changes/workspace-layout-slide.md b/.changes/workspace-layout-slide.md new file mode 100644 index 000000000..1e59813ba --- /dev/null +++ b/.changes/workspace-layout-slide.md @@ -0,0 +1,9 @@ +--- +type: fix +area: workspace +--- + +Opening a source from the dashboard no longer makes the page jump: the content +slides aside for the category panel, and the header controls slide over for +the Back button. The category list's search and sort buttons are in place +while the list loads. diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index 12d9c575b..021eb3c42 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -1279,9 +1279,41 @@ measured iterations of the 11 `master` runs from 2026-10-03 08:25 to `open-source/renderer.layoutShiftScore` read 0.222 in that window and 0.233 in every iteration of every `master` run from 84aef83a6 (#1814, page Back -buttons moved into the header) on, so its value is 0.233 with `evidenceRun` -37372780064 (b78224376). The 0.011 that #1814 added is not explained yet; -lowering it back is a separate change. +buttons moved into the header) on, so its value was 0.233 with `evidenceRun` +37372780064 (b78224376). + +Where the 0.233 came from (`evidence.layoutShift.shifts`, a 1280×720 window; +the score of a shift is the area of every node that moved times the largest +distance any of them moved, as a fraction of the viewport): + +| Shift, ms after the click | Nodes that moved | Score | +| ------------------------- | ---------------------------------------------------------- | ------ | +| ~40 (page swap) | `main.workspace-content` x 60 → 383, width −323 | 0.222 | +| | switcher, `label.search-field`, `div.header-actions` x +44 | 0.011 | +| ~50-70 (categories) | `div.context-header__actions` x −60, width +60 | 0.0001 | + +The first two are one frame: the frame that replaces the dashboard with the +portal page also inserts the 323 px context panel the dashboard has no room +for, and the history Back that #1814 shows once an in-app previous entry +exists. The header items move only 44 px, but a shift is scored with the +largest distance in it, the content pane's 323 px, which is why Back added +0.011 rather than about 0.0015. The third is the category search and sort +buttons appearing when the categories arrive. The skeletons of the category +list and the grid are replaced by content without any shift. Nothing was +hidden behind the startup overlay: the "Syncing playlist" overlay of the +cached catalog load flashes for about two frames after the shift and covers +no further one. + +The content frame and the header controls now slide into place instead of +jumping ([Layout slides](./workspace-shell.md#layout-slides)): the new layout +applies in the same frame and only a transform animates, which Chromium does +not record as a layout shift. The category search and sort buttons render +disabled while the categories load. Locally (macOS, 2026-10-11) J2 then read +0 in every iteration with no shift recorded, J1 0 / 0, J3 0.001 and J4 0; +`renderer.domMutationsToFirstPage` rose by 22 (the slide's class and style +changes) and the wall-clock entries did not move. Under +`prefers-reduced-motion` there is no slide, so a user with that preference +still sees the jump that the journey no longer measures. Being deterministic is not the same as being validated. Principle 3 of the plan promotes a counter to a guardrail once a PR has shown that lowering it diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md index a161a3eee..f6242c6a6 100644 --- a/docs/architecture/workspace-shell.md +++ b/docs/architecture/workspace-shell.md @@ -127,6 +127,49 @@ synthesized mask, unlike any content layer) may span half the content area, and the backdrop-filtered controls must really be composited, so the check cannot pass on a page with nothing to clip. +### Layout slides + +When a route change adds or removes the context panel, the content frame +moves by the panel's width (323 px on desktop), and when Back or the phone +drawer toggle appears or leaves, the header controls after it move by the +slot's width (44 px). Both happen in the frame that swaps the page, so they +used to be one jump: a layout shift of 0.233 in the open-source journey, +0.222 of it the content frame (see +[performance journeys](./performance-journeys.md#j2-open-source-open-a-source-to-a-browsable-list)). + +`LayoutSlideDirective` (`workspace-shell/layout-slide.directive.ts`) turns +these jumps into a 180 ms slide, the duration of the context panel's own +width transition. The new layout applies at once; each target starts +translated back to the position it had and animates its transform to none +(FLIP), so nothing waits for the animation and Chromium records no layout +shift for a transform. The shell's `.workspace-body` slides +`.workspace-content-frame` when `showContextPanel` changes; the header slides +the drawer toggle, switcher, search and action groups when its leading +navigation (Back, drawer toggle) changes. + +1. **Previous positions.** `offsetLeft` values, which ignore transforms, are + recorded after every render in which the slide key or the record key + changed (the header's record key covers the other inputs that move its + controls: settings route, no playlists, parental lock, shortcuts) and + when the host or a target resizes (window resize, a context panel folding + or being dragged wider). A slide therefore starts where the target was + last laid out, and a target that has no recorded position or did not move + is left alone. +2. **The uncovered strip.** The frame is narrower beside the panel than + without it, so the side it moves away from would show the body colour + during the slide. While a slide runs the target carries + `layout-slide-active` and the uncovered widths as `--layout-slide-start` + and `--layout-slide-end`; the frame's stylesheet extends the content + surface over that strip with an `::after` below the scroller. +3. **Back fades in** over the same 180 ms, so the switcher still sliding away + from its slot does not draw over the arrow. +4. **Reduced motion.** Under `prefers-reduced-motion` there is no animation: + the jump (and its layout shift) remains. +5. **Not covered.** A slide does not change what the route renders. Going + back from a portal to the dashboard still shows one frame with the new + dashboard below the old page before the old page leaves; that jump is a + routing issue, not a layout slide. + `WorkspaceShellComponent` binds only to `WorkspaceShellFacade`. The facade is kept as a thin template-facing API and delegates ownership to component-scoped services: @@ -215,9 +258,10 @@ return logic (`backClicked`). When there is nowhere to go, the slot is empty rather than a disabled arrow. Sessions often start on a page that never navigates (an M3U playlist or live -TV), where a disabled arrow would stay for the whole session. The cost is one -shift of the switcher and search when Back first appears or leaves, which -happens only at the start of the history and together with a route change. +TV), where a disabled arrow would stay for the whole session. When Back first +appears or leaves, which happens only at the start of the history and together +with a route change, the switcher and search slide by the slot's width instead +of jumping (see [Layout slides](#layout-slides)). There is no Forward button: Stalker inline details are store state, not history entries, so Forward would skip them. @@ -263,7 +307,10 @@ Xtream and Stalker category panels preserve provider/server category order by default. The panel header exposes a sort menu next to category search with `Server sorting`, `A-Z`, and `Z-A`; when alphabetical sorting is active, synthetic "all categories" entries stay pinned before sorted provider -categories. +categories. Search and sort render disabled while the categories load (beside +the disabled Manage categories button), so the header row has its final width +under the skeleton instead of growing by two buttons when the list arrives; +they are hidden only once a load ended with no categories. On live sections the category panel can be folded away independently of the channels list; the folded panel is reachable as a popover from the channels diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html index fc4b6440b..d6ae3db4e 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.html @@ -3,12 +3,15 @@

{{ title() | translate }}

- @if (canSearchCategories()) { + + @if (showCategoryListActions()) { } - @if (canSearchCategories()) { + @if (showCategoryListActions()) {