From b466a821a6abcb0faceef483c35b7048d3a8b46d Mon Sep 17 00:00:00 2001 From: Matti Airas Date: Sun, 5 Jul 2026 01:20:18 +0300 Subject: [PATCH] feat(nav): reveal toolbar on page change, drop dashboard-scroller The auto-hiding toolbar's page-icon strip is now the transient page-position indicator. An effect reveals the toolbar whenever the active page changes, covering swipe, hotkey, tap and remote navigation since all route through the activeDashboard signal. Removes the dashboard-scroller component, whose vertical column of position dots read wrong once page navigation became horizontal. The toolbar's icon strip supersedes it: it names the active page rather than showing an anonymous, orientation-dependent dot. Closes #195. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018zURudCY3TTRcNd2acknJ9 --- src/app/app.component.spec.ts | 43 +++++++++- src/app/app.component.ts | 8 ++ .../dashboard-scroller.component.html | 13 --- .../dashboard-scroller.component.scss | 79 ------------------- .../dashboard-scroller.component.spec.ts | 28 ------- .../dashboard-scroller.component.ts | 31 -------- .../dashboard/dashboard.component.html | 5 -- .../dashboard/dashboard.component.ts | 3 +- 8 files changed, 50 insertions(+), 160 deletions(-) delete mode 100644 src/app/core/components/dashboard-scroller/dashboard-scroller.component.html delete mode 100644 src/app/core/components/dashboard-scroller/dashboard-scroller.component.scss delete mode 100644 src/app/core/components/dashboard-scroller/dashboard-scroller.component.spec.ts delete mode 100644 src/app/core/components/dashboard-scroller/dashboard-scroller.component.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index e94c7960..0ad5beff 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -24,7 +24,7 @@ describe('AppComponent', () => { let dashboard: { isDashboardStatic: ReturnType>; - activeDashboard: ReturnType>; + activeDashboard: ReturnType>; dashboards: ReturnType>; navigateToNextDashboard: ReturnType; navigateToPreviousDashboard: ReturnType; @@ -48,7 +48,7 @@ describe('AppComponent', () => { beforeEach(async () => { dashboard = { isDashboardStatic: signal(true), - activeDashboard: signal(0), + activeDashboard: signal(null), dashboards: signal([]), navigateToNextDashboard: vi.fn(), navigateToPreviousDashboard: vi.fn(), @@ -135,6 +135,45 @@ describe('AppComponent', () => { }); }); + describe('page-change toolbar reveal', () => { + it('reveals the toolbar when the active page changes', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + chrome.reveal.mockClear(); + + dashboard.activeDashboard.set(1); + fixture.detectChanges(); + + expect(chrome.reveal).toHaveBeenCalledTimes(1); + }); + + it('reveals again on each subsequent page change', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + dashboard.activeDashboard.set(1); + fixture.detectChanges(); + chrome.reveal.mockClear(); + + dashboard.activeDashboard.set(2); + fixture.detectChanges(); + + expect(chrome.reveal).toHaveBeenCalledTimes(1); + }); + + it('does not reveal on a transition to no active page', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + dashboard.activeDashboard.set(1); + fixture.detectChanges(); + chrome.reveal.mockClear(); + + dashboard.activeDashboard.set(null); + fixture.detectChanges(); + + expect(chrome.reveal).not.toHaveBeenCalled(); + }); + }); + describe('press-outside dismissal', () => { const outside = { target: { closest: () => null } }; const onToolbar = { target: { closest: (sel: string) => (sel === 'app-toolbar' ? {} : null) } }; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 13bb349d..1be122aa 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -115,6 +115,14 @@ export class AppComponent implements AfterViewInit, OnDestroy { } }); + // Reveal the auto-hiding toolbar on every page change: its page-icon strip + // is the transient page-position indicator. Fires for swipe, hotkey, tap and + // remote navigation alike, since all route through the activeDashboard signal. + effect(() => { + if (this._dashboard.activeDashboard() === null) return; + this.chrome.reveal(); + }); + // initialize dashboardVisible from current URL try { this.dashboardVisible.set(this.isUrlDashboard(this._router.url)); diff --git a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.html b/src/app/core/components/dashboard-scroller/dashboard-scroller.component.html deleted file mode 100644 index d8aa17a9..00000000 --- a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.html +++ /dev/null @@ -1,13 +0,0 @@ -@let _activePage = activePage(); -@let _dashboards = dashboards(); - -@if (visible()) { -
- {{ _dashboards[_activePage].name }} -
-
- @for (page of _dashboards; track page.id; let index = $index) { -
- } -
-} \ No newline at end of file diff --git a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.scss b/src/app/core/components/dashboard-scroller/dashboard-scroller.component.scss deleted file mode 100644 index f8294f41..00000000 --- a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.scss +++ /dev/null @@ -1,79 +0,0 @@ -:host { - pointer-events: none; -} - -.page-scroll-title { - position: absolute; - top: 50%; - right: 54px; - transform: translate(0%, -50%); - z-index: 100; - cursor: default; - text-align: center; - padding: 10px; - color: var(--mat-sys-inverse-surface); -} - -.page-scroll-page-number { - position: absolute; - top: 50%; - right: 15px; - transform: translate(0%, -50%); - z-index: 100; - cursor: default; - display: grid; - padding: 0px; -} - -.page-scroll-container { - opacity: 0; - background-color: var(--mat-sys-tertiary-fixed-dim); - border-radius: 10px; - border: 1px solid var(--mat-sys-inverse-surface); -} - -.page-label { - color: var(--mat-sys-on-surface); - text-shadow: - 1px 1px 0 var(--mat-sys-outline), - -1px 1px 0 var(--mat-sys-outline), - -1px -1px 0 var(--mat-sys-outline), - 1px -1px 0 var(--mat-sys-outline); - font-size: x-large; -} - -.page-marker { - height: 20px; - width: 20px; - margin: 4px; - border-radius: 50%; - border: 1px solid var(--mat-sys-outline); -} - -@keyframes fadeInHoldFadeOut { - /* 0ms */ - 0% { - opacity: 0; - } - - /* 100ms / 1350ms = 7.407% */ - 7.407% { - opacity: 1; - } - - /* (100ms + 250ms) / 1350ms = 25.926% */ - 25.926% { - opacity: 1; - } - - /* 1350ms */ - 100% { - opacity: 0; - } -} - -/* apply to the element you want to animate */ -.fade-toast { - opacity: 0; /* initial */ - animation: fadeInHoldFadeOut 1350ms linear forwards; -} \ No newline at end of file diff --git a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.spec.ts b/src/app/core/components/dashboard-scroller/dashboard-scroller.component.spec.ts deleted file mode 100644 index 2e86003e..00000000 --- a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { DashboardScrollerComponent } from './dashboard-scroller.component'; - -describe('DashboardScrollerComponent', () => { - let component: DashboardScrollerComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DashboardScrollerComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(DashboardScrollerComponent); - component = fixture.componentInstance; - // Provide required inputs before first detectChanges - fixture.componentRef.setInput('dashboards', [ - { id: '1', name: 'Test Dashboard', icon: 'dashboard-dashboard', configuration: [] } - ]); - fixture.componentRef.setInput('activePage', 0); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.ts b/src/app/core/components/dashboard-scroller/dashboard-scroller.component.ts deleted file mode 100644 index d4fed686..00000000 --- a/src/app/core/components/dashboard-scroller/dashboard-scroller.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, effect, input, signal } from '@angular/core'; -import { Dashboard } from './../../services/dashboard.service'; - - -@Component({ - selector: 'dashboard-scroller', - standalone: true, - imports: [], - templateUrl: './dashboard-scroller.component.html', - styleUrl: './dashboard-scroller.component.scss', -}) -export class DashboardScrollerComponent { - protected activePage = input(); - protected dashboards = input(); - protected visible = signal(false); - - constructor() { - effect(() => { - const page = this.activePage(); - - if (page === undefined || page === null) { - this.visible.set(false); - return; - } - - // Restart the CSS animation by removing + re-adding the DOM. - this.visible.set(false); - queueMicrotask(() => this.visible.set(true)); - }); - } -} diff --git a/src/app/core/components/dashboard/dashboard.component.html b/src/app/core/components/dashboard/dashboard.component.html index 3b468ede..5750e183 100644 --- a/src/app/core/components/dashboard/dashboard.component.html +++ b/src/app/core/components/dashboard/dashboard.component.html @@ -59,9 +59,4 @@ done -} @else { - } diff --git a/src/app/core/components/dashboard/dashboard.component.ts b/src/app/core/components/dashboard/dashboard.component.ts index 59145596..d63b12b8 100644 --- a/src/app/core/components/dashboard/dashboard.component.ts +++ b/src/app/core/components/dashboard/dashboard.component.ts @@ -3,7 +3,6 @@ import { GestureDirective } from '../../directives/gesture.directive'; import { GridstackComponent, NgGridStackWidget, NgGridStackOptions, } from 'gridstack/dist/angular'; import { GridItemHTMLElement, GridStackOptions } from 'gridstack'; import { DashboardService, widgetOperation } from '../../services/dashboard.service'; -import { DashboardScrollerComponent } from "../dashboard-scroller/dashboard-scroller.component"; import { UUID } from '../../utils/uuid.util'; import { ToastService } from '../../services/toast.service'; import { MatIconModule } from '@angular/material/icon'; @@ -31,7 +30,7 @@ interface GridApi { @Component({ selector: 'dashboard', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [GridstackComponent, DashboardScrollerComponent, MatIconModule, MatButtonModule, GestureDirective, ActionMenuComponent], + imports: [GridstackComponent, MatIconModule, MatButtonModule, GestureDirective, ActionMenuComponent], templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.scss', host: {