From 2cc0c02682c6abdfbad2fe407db68462f6c1e9b3 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 5 Sep 2025 14:46:20 +0200 Subject: [PATCH 01/61] feat(sideNav): add new left navigation and integrate --- e2e/constants/selectors.ts | 2 +- src/app/app.component.html | 28 +- src/app/app.component.scss | 37 + src/app/app.component.ts | 45 +- .../magic-side-nav/magic-side-nav.html | 335 +++++++++ .../magic-side-nav/magic-side-nav.scss | 665 ++++++++++++++++++ .../core-ui/magic-side-nav/magic-side-nav.ts | 394 +++++++++++ .../main-header/main-header.component.html | 14 +- .../side-nav/side-nav-config.service.ts | 451 ++++++++++++ .../side-nav-item.component.scss | 27 +- .../core-ui/side-nav/side-nav.component.html | 12 +- 11 files changed, 1971 insertions(+), 39 deletions(-) create mode 100644 src/app/core-ui/magic-side-nav/magic-side-nav.html create mode 100644 src/app/core-ui/magic-side-nav/magic-side-nav.scss create mode 100644 src/app/core-ui/magic-side-nav/magic-side-nav.ts create mode 100644 src/app/core-ui/side-nav/side-nav-config.service.ts diff --git a/e2e/constants/selectors.ts b/e2e/constants/selectors.ts index 71547dbccc..6255208197 100644 --- a/e2e/constants/selectors.ts +++ b/e2e/constants/selectors.ts @@ -1,3 +1,3 @@ export const cssSelectors = { - SIDENAV: 'side-nav', + SIDENAV: 'magic-side-nav', }; diff --git a/src/app/app.component.html b/src/app/app.component.html index 0891bb166d..1c1b7be357 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -25,18 +25,20 @@ @if (isShowFocusOverlay()) { } @else { - - + - - @if (workContextService.isActiveWorkContextProject$ | async) {} - - + + +
- - +
+ } } diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 1dca328215..dce70c9490 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -6,6 +6,43 @@ height: 100%; } +.app-container { + display: flex; + height: 100vh; + min-height: 100vh; + // mobile viewport bug fix + min-height: -webkit-fill-available; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; // Prevent flex item from growing beyond container + overflow: hidden; +} + +// Override magic-side-nav styles for flex layout integration +magic-side-nav { + ::ng-deep .nav-sidebar { + position: relative !important; + top: auto !important; + left: auto !important; + bottom: auto !important; + height: 100vh !important; + min-height: -webkit-fill-available !important; + z-index: auto !important; + } + + ::ng-deep .mobile-menu-toggle { + display: none !important; // Use app header toggle instead + } + + ::ng-deep .nav-overlay { + display: none !important; // Not needed in flex layout + } +} + .header-wrapper { position: relative; z-index: var(--z-main-header-wrapper); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c5d20b3655..1070ef7dee 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -45,13 +45,10 @@ import { IS_MOBILE } from './util/is-mobile'; import { warpAnimation, warpInAnimation } from './ui/animations/warp.ani'; import { GlobalConfigState } from './features/config/global-config.model'; import { AddTaskBarComponent } from './features/tasks/add-task-bar/add-task-bar.component'; -import { - MatSidenav, - MatSidenavContainer, - MatSidenavContent, -} from '@angular/material/sidenav'; import { Dir } from '@angular/cdk/bidi'; -import { SideNavComponent } from './core-ui/side-nav/side-nav.component'; +import { MagicSideNavComponent } from './core-ui/magic-side-nav/magic-side-nav'; +import { SideNavConfigService } from './core-ui/side-nav/side-nav-config.service'; +import { NavConfig } from './core-ui/magic-side-nav/magic-side-nav'; import { MainHeaderComponent } from './core-ui/main-header/main-header.component'; import { BannerComponent } from './core/banner/banner/banner.component'; import { GlobalProgressBarComponent } from './core-ui/global-progress-bar/global-progress-bar.component'; @@ -109,11 +106,8 @@ const productivityTip: string[] = w.productivityTips && w.productivityTips[w.ran changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AddTaskBarComponent, - MatSidenavContainer, Dir, - MatSidenav, - SideNavComponent, - MatSidenavContent, + MagicSideNavComponent, MainHeaderComponent, BannerComponent, RightPanelComponent, @@ -158,6 +152,7 @@ export class AppComponent implements OnDestroy, AfterViewInit { private _syncSafetyBackupService = inject(SyncSafetyBackupService); readonly syncTriggerService = inject(SyncTriggerService); + readonly sideNavConfigService = inject(SideNavConfigService); readonly imexMetaService = inject(ImexViewService); readonly workContextService = inject(WorkContextService); readonly layoutService = inject(LayoutService); @@ -195,6 +190,11 @@ export class AppComponent implements OnDestroy, AfterViewInit { initialValue: false, }); + private readonly _activeWorkContextId = toSignal( + this.workContextService.activeWorkContextId$, + { initialValue: null }, + ); + private _subs: Subscription = new Subscription(); private _intervalTimer?: NodeJS.Timeout; @@ -435,6 +435,31 @@ export class AppComponent implements OnDestroy, AfterViewInit { return outlet.activatedRouteData.page || 'one'; } + getMagicSideNavConfig(): NavConfig { + const baseConfig = this.sideNavConfigService.navConfig(); + return { + ...baseConfig, + expandedByDefault: this.layoutService.isShowSideNav(), + // Integrate with layout service for mobile detection + mobileBreakpoint: 0, // Disable mobile mode, use existing app mobile handling + }; + } + + onSideNavExpandedChange(isExpanded: boolean): void { + const currentlyExpanded = this.layoutService.isShowSideNav(); + if (isExpanded !== currentlyExpanded) { + this.layoutService.toggleSideNav(); + } + } + + getExpandedGroupIds(): Set { + return this.sideNavConfigService.expandedGroupIds(); + } + + getActiveWorkContextId(): string | null { + return this._activeWorkContextId(); + } + changeBackgroundFromUnsplash(): void { const dialogRef = this._matDialog.open(DialogUnsplashPickerComponent, { width: '900px', diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.html b/src/app/core-ui/magic-side-nav/magic-side-nav.html new file mode 100644 index 0000000000..7f955475ac --- /dev/null +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.html @@ -0,0 +1,335 @@ + +@if (isMobile()) { + +} + + +@if (isMobile() && showMobileMenu()) { + +} + + + diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.scss new file mode 100644 index 0000000000..ac6b763266 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.scss @@ -0,0 +1,665 @@ +// Use app's CSS variables for width and spacing +$sidebar-width-expanded: var(--side-nav-width, 260px); +$sidebar-width-collapsed: 72px; +$mobile-breakpoint: 768px; +$transition-speed: var(--transition-standard, 0.3s); +$overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); + +:host { + // Use app's CSS variables for consistent theming + --sidebar-bg: var(--palette-background-card); + --sidebar-border: var(--divider-color); + --sidebar-text: rgba( + var(--palette-foreground-text), + var(--palette-foreground-text-alpha) + ); + --sidebar-text-secondary: rgba( + var(--palette-foreground-secondary-text), + var(--palette-foreground-secondary-text-alpha) + ); + --sidebar-hover: var(--palette-background-hover); + --sidebar-active: var(--c-primary-10, rgba(var(--palette-primary-500-rgb), 0.04)); + --sidebar-active-text: var(--palette-primary-800); + + // Dark theme overrides + body.isDarkTheme & { + --sidebar-active-text: var(--c-primary); + } +} + +// Mobile menu toggle button +.mobile-menu-toggle { + display: none; + position: fixed; + top: 16px; + left: 16px; + z-index: 1001; + background: var(--sidebar-bg); + border: 1px solid var(--sidebar-border); + border-radius: 8px; + padding: 8px; + cursor: pointer; + transition: all $transition-speed ease; + + &.visible { + display: flex; + align-items: center; + justify-content: center; + } + + &:hover { + background: var(--sidebar-hover); + } + + svg { + color: var(--sidebar-text); + } +} + +// Mobile overlay +.nav-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: $overlay-bg; + z-index: 998; + opacity: 0; + transition: opacity $transition-speed ease; + + &.visible { + display: block; + opacity: 1; + } +} + +// Main sidebar +.nav-sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: $sidebar-width-expanded; + background: var(--sidebar-bg); + border-right: 1px solid var(--sidebar-border); + transition: all $transition-speed cubic-bezier(0.4, 0, 0.2, 1); + z-index: 999; + overflow-y: auto; + overflow-x: hidden; + + &.resizing { + transition: none !important; + } + + // Threshold zone visual feedback + &.collapse-zone { + border-left: 3px solid #ef4444; + box-shadow: inset 3px 0 0 rgba(239, 68, 68, 0.1); + + .resize-handle { + background-color: #ef4444 !important; + } + } + + &.expand-zone { + border-left: 3px solid #10b981; + box-shadow: inset 3px 0 0 rgba(16, 185, 129, 0.1); + + .resize-handle { + background-color: #10b981 !important; + } + } + + // Scrollbar styling + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--sidebar-border); + border-radius: 3px; + + &:hover { + background: var(--sidebar-text-secondary); + } + } + + // Collapsed state (desktop) + &.collapsed { + width: $sidebar-width-collapsed !important; + + .nav-label, + .nav-badge, + .nav-chevron, + .nav-children { + display: none; + } + + .nav-link { + justify-content: center; + padding: 12px; + + &[title] { + // Ensure tooltip shows on hover for collapsed items + } + } + + .nav-icon { + margin: 0; + } + + // Multi-button wrapper in collapsed mode + .g-multi-btn-wrapper { + justify-content: center; + + .expand-btn { + justify-content: center; + padding: 12px; + position: relative; + + .nav-icon { + margin: 0; + } + + // Show a small indicator when section is expanded in collapsed mode + &.isExpanded::after { + content: ''; + position: absolute; + bottom: 2px; + right: 2px; + width: 4px; + height: 4px; + background-color: var(--sidebar-active-text); + border-radius: 50%; + } + } + + // Hide additional buttons in collapsed mode + .additional-btn { + display: none; + } + } + + // Fix side-nav-item wrapping issues in collapsed mode + ::ng-deep side-nav-item { + .additional-btn { + display: none !important; + } + } + + .sidebar-toggle { + right: auto; + left: 50%; + transform: translateX(-50%); + } + + // Show resize handle in collapsed mode but style differently + .resize-handle.collapsed-handle { + display: block; + width: 8px; + right: -4px; + + &:hover { + background-color: var(--sidebar-active-text); + } + + .resize-handle-line { + width: 3px; + height: 60px; + opacity: 0.7; + } + } + } + + // Mobile specific + &.mobile { + transform: translateX(-100%); + width: $sidebar-width-expanded; + + &.mobile-visible { + transform: translateX(0); + } + } + + // Position right + &.position-right { + left: auto; + right: 0; + border-right: none; + border-left: 1px solid var(--sidebar-border); + + &.mobile { + transform: translateX(100%); + + &.mobile-visible { + transform: translateX(0); + } + } + + .sidebar-toggle { + left: 16px; + right: auto; + } + + &.collapse-zone { + border-left: none; + border-right: 3px solid #ef4444; + box-shadow: inset -3px 0 0 rgba(239, 68, 68, 0.1); + } + + &.expand-zone { + border-left: none; + border-right: 3px solid #10b981; + box-shadow: inset -3px 0 0 rgba(16, 185, 129, 0.1); + } + } + + // Note: Theme colors are handled by CSS variables above +} + +// Sidebar toggle button (desktop) +.sidebar-toggle { + display: none; + position: absolute; + top: 16px; + right: 16px; + background: transparent; + border: 1px solid var(--sidebar-border); + border-radius: 6px; + padding: 6px; + cursor: pointer; + transition: all $transition-speed ease; + color: var(--sidebar-text-secondary); + + &.visible { + display: flex; + align-items: center; + justify-content: center; + } + + &:hover { + background: var(--sidebar-hover); + color: var(--sidebar-text); + } +} + +// Navigation list +.nav-list { + list-style: none; + padding: 60px var(--s) var(--s2); + margin: 0; +} + +.nav-item { + margin-bottom: 2px; + + &.has-children { + margin-bottom: 4px; + } +} + +// Separator styling +.nav-separator { + height: 1px; + background: var(--divider-color); + margin: var(--s2) var(--s2); + border: none; + opacity: 0.7; +} + +// Navigation links +.nav-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; + + &:hover { + background: var(--sidebar-hover); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + + .nav-icon { + color: var(--sidebar-active-text); + } + } + + &.expanded .nav-chevron { + transform: rotate(180deg); + } +} + +// Navigation icon +.nav-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--sidebar-text-secondary); + font-size: 18px; + line-height: 1; +} + +// Navigation label +.nav-label { + flex: 1; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +// Navigation badge +.nav-badge { + flex-shrink: 0; + padding: 2px 8px; + border-radius: 12px; + background: var(--sidebar-active); + color: var(--sidebar-active-text); + font-size: 12px; + font-weight: 600; +} + +// Fix side-nav-item wrapping issues globally +::ng-deep side-nav-item { + display: flex !important; + align-items: stretch; + width: 100%; + + button:first-of-type { + white-space: nowrap; + overflow: hidden; + flex: 1; + min-width: 0; + + .text { + overflow: hidden; + text-overflow: ellipsis; + } + } + + .additional-btn { + flex-shrink: 0; + width: 40px !important; + min-width: 40px !important; + } +} + +// Navigation chevron +.nav-chevron { + flex-shrink: 0; + transition: transform 0.2s ease; + color: var(--sidebar-text-secondary); +} + +// Children navigation +.nav-children { + list-style: none; + padding: 0; + margin: 4px 0 0 0; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + + &.visible { + max-height: 500px; + } +} + +.nav-child-item { + margin-left: var(--s3); +} + +.nav-child-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s) var(--s2); + color: var(--sidebar-text-secondary); + text-decoration: none; + border-radius: 6px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + font-size: 14px; + + &:hover { + background: var(--sidebar-hover); + color: var(--sidebar-text); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + + .nav-icon { + color: var(--sidebar-active-text); + } + } + + .nav-icon { + width: 20px; + height: 20px; + font-size: 16px; + } +} + +// Resize handle +.resize-handle { + position: absolute; + top: 0; + bottom: 0; + width: 4px; + cursor: col-resize; + z-index: 1002; + transition: background-color 0.2s ease; + + &.position-left { + right: -2px; + } + + &.position-right { + left: -2px; + } + + &:hover { + background-color: var(--sidebar-active-text); + + .resize-handle-line { + opacity: 1; + } + } + + &:active { + background-color: var(--sidebar-active-text); + } +} + +.resize-handle-line { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 40px; + background: var(--sidebar-text-secondary); + border-radius: 1px; + opacity: 0.5; + transition: opacity 0.2s ease; +} + +// Multi-button wrapper styling for expandable sections with additional buttons +.g-multi-btn-wrapper { + display: flex; + align-items: center; + position: relative; + + .expand-btn { + flex: 1; + text-align: left; + + &.isExpanded .nav-icon { + transform: rotate(180deg); + } + } + + .additional-btn { + flex-shrink: 0; + width: 32px; + height: 32px; + min-width: 32px; + padding: 4px; + margin-left: var(--s); + color: var(--sidebar-text-secondary); + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + } + + .title { + font-weight: 500; + } +} + +// Integration styles for side-nav-item components +.work-context-item { + // Remove default nav-child-item padding to let side-nav-item handle its own spacing + padding: 0; + margin: 0; + + side-nav-item { + // Ensure side-nav-item fits well within the navigation structure + display: block; + width: 100%; + + // Override side-nav-item styles to match magic-side-nav theming + .g-multi-btn-wrapper { + background-color: transparent; + + .color-bar { + // Make color bar thinner to fit better in child context + width: 2px; + } + + button { + color: var(--sidebar-text); + padding: var(--s) var(--s2); + font-size: 14px; + + &:hover { + background-color: var(--sidebar-hover); + } + + &.isActiveRoute { + background-color: var(--sidebar-active); + color: var(--sidebar-active-text); + } + } + + .additional-btn { + color: var(--sidebar-text-secondary); + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } + } + } + } +} + +// Media queries +@media (max-width: $mobile-breakpoint) { + .nav-sidebar { + &.collapsed { + width: $sidebar-width-expanded; + + .nav-label, + .nav-badge, + .nav-chevron, + .nav-children.visible { + display: initial; + } + + .nav-link { + justify-content: flex-start; + padding: 10px 12px; + } + } + } +} + +// Top-level work context items (Today/Inbox) +.work-context-item-main { + display: block; + width: 100%; + + side-nav-item { + display: block; + width: 100%; + + .g-multi-btn-wrapper { + background-color: transparent; + + button { + color: var(--sidebar-text); + + &:hover { + background-color: var(--sidebar-hover); + } + + &.isActiveRoute { + background-color: var(--sidebar-active); + color: var(--sidebar-active-text); + } + } + + .additional-btn { + color: var(--sidebar-text-secondary); + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } + } + } + } +} diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts new file mode 100644 index 0000000000..9d3c7420ec --- /dev/null +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -0,0 +1,394 @@ +import { + Component, + Input, + HostListener, + signal, + computed, + effect, + output, + OnInit, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { MatIcon } from '@angular/material/icon'; +import { MatIconButton } from '@angular/material/button'; +import { MatTooltip } from '@angular/material/tooltip'; +import { + MatMenu, + MatMenuContent, + MatMenuItem, + MatMenuTrigger, +} from '@angular/material/menu'; +import { TranslatePipe } from '@ngx-translate/core'; +import { + WorkContextCommon, + WorkContextType, +} from '../../features/work-context/work-context.model'; +import { SideNavItemComponent } from '../side-nav/side-nav-item/side-nav-item.component'; +import { ContextMenuComponent } from '../../ui/context-menu/context-menu.component'; + +export interface NavItem { + id: string; + label: string; + icon: string; + svgIcon?: string; // For SVG icons like schedule + route?: string; + action?: () => void; + children?: NavItem[]; + badge?: string | number; + // For work context items (projects/tags) + workContext?: WorkContextCommon; + workContextType?: WorkContextType; + defaultIcon?: string; + // Additional buttons for navigation sections + additionalButtons?: NavAdditionalButton[]; + // Context menu items + contextMenuItems?: NavItem[]; +} + +export interface NavAdditionalButton { + id: string; + icon: string; + tooltip?: string; + action?: () => void; + hidden?: boolean; + contextMenu?: NavItem[]; +} + +export interface NavConfig { + items: NavItem[]; + expandedByDefault?: boolean; + showLabels?: boolean; + mobileBreakpoint?: number; + position?: 'left' | 'right'; + theme?: 'light' | 'dark'; + resizable?: boolean; + minWidth?: number; + maxWidth?: number; + defaultWidth?: number; + collapseThreshold?: number; + expandThreshold?: number; +} + +@Component({ + selector: 'magic-side-nav', + standalone: true, + imports: [ + CommonModule, + RouterModule, + MatIcon, + MatIconButton, + MatTooltip, + MatMenu, + MatMenuContent, + MatMenuItem, + MatMenuTrigger, + TranslatePipe, + SideNavItemComponent, + ContextMenuComponent, + ], + templateUrl: './magic-side-nav.html', + styleUrl: './magic-side-nav.scss', +}) +export class MagicSideNavComponent implements OnInit { + @Input() config: NavConfig = { + items: [], + expandedByDefault: true, + showLabels: true, + mobileBreakpoint: 768, + position: 'left', + theme: 'light', + resizable: false, + minWidth: 200, + maxWidth: 400, + defaultWidth: 260, + collapseThreshold: 150, + expandThreshold: 180, + }; + + @Input() expandedGroupIds: Set = new Set(); + @Input() activeWorkContextId: string | null = null; + + itemClick = output(); + expandedChange = output(); + widthChange = output(); + + isExpanded = signal(true); + isMobile = signal(false); + showMobileMenu = signal(false); + expandedGroups = signal(new Set()); + + // Resize functionality + currentWidth = signal(260); + minWidth = signal(200); + maxWidth = signal(400); + isResizing = signal(false); + startX = signal(0); + startWidth = signal(0); + collapseThreshold = signal(150); + expandThreshold = signal(180); + + // Computed values + sidebarWidth = computed(() => { + if (this.isMobile()) return 260; + if (!this.isExpanded()) return 72; + return this.currentWidth(); + }); + + constructor() { + // Effect to update sidebar width whenever signals change + effect(() => { + this.updateSidebarWidth(); + }); + + // Effect to emit width changes + effect(() => { + this.widthChange.emit(this.sidebarWidth()); + }); + } + + ngOnInit(): void { + // Load saved expansion state or default to config value + const savedExpanded = localStorage.getItem('nav-sidebar-expanded'); + const initialExpanded = + savedExpanded !== null + ? savedExpanded === 'true' + : (this.config.expandedByDefault ?? true); + + this.isExpanded.set(initialExpanded); + this.currentWidth.set(this.config.defaultWidth ?? 260); + this.minWidth.set(this.config.minWidth ?? 200); + this.maxWidth.set(this.config.maxWidth ?? 400); + this.collapseThreshold.set(this.config.collapseThreshold ?? 150); + this.expandThreshold.set(this.config.expandThreshold ?? 180); + + // Load saved width from localStorage + const savedWidth = localStorage.getItem('nav-sidebar-width'); + if (savedWidth) { + const width = Math.max( + this.minWidth(), + Math.min(this.maxWidth(), parseInt(savedWidth)), + ); + this.currentWidth.set(width); + } + + this.checkScreenSize(); + } + + @HostListener('window:resize') + onWindowResize(): void { + this.checkScreenSize(); + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent): void { + if (this.isMobile() && this.showMobileMenu()) { + const target = event.target as HTMLElement; + const nav = document.querySelector('.nav-sidebar'); + const toggle = document.querySelector('.mobile-menu-toggle'); + + if (nav && !nav.contains(target) && toggle && !toggle.contains(target)) { + this.showMobileMenu.set(false); + } + } + } + + private checkScreenSize(): void { + const wasMobile = this.isMobile(); + const currentMobile = window.innerWidth < (this.config.mobileBreakpoint || 768); + this.isMobile.set(currentMobile); + + if (wasMobile !== currentMobile) { + if (currentMobile) { + this.showMobileMenu.set(false); + } else { + this.isExpanded.set(this.config.expandedByDefault ?? true); + } + } + } + + toggleSidebar(): void { + if (this.isMobile()) { + this.showMobileMenu.update((show) => !show); + } else { + const newExpanded = !this.isExpanded(); + this.isExpanded.set(newExpanded); + // Save expansion state to localStorage + localStorage.setItem('nav-sidebar-expanded', newExpanded.toString()); + this.expandedChange.emit(newExpanded); + } + } + + toggleGroup(item: NavItem): void { + if (item.children && item.children.length > 0) { + this.expandedGroups.update((groups) => { + const newGroups = new Set(groups); + if (newGroups.has(item.id)) { + newGroups.delete(item.id); + } else { + newGroups.add(item.id); + } + return newGroups; + }); + } + } + + isGroupExpanded(item: NavItem): boolean { + // Use external expansion state if available, otherwise fall back to local state + if (this.expandedGroupIds && this.expandedGroupIds.size > 0) { + return this.expandedGroupIds.has(item.id); + } + return this.expandedGroups().has(item.id); + } + + onItemClick(item: NavItem): void { + if (item.children && item.children.length > 0) { + // For items with children, call the action (which should be the expand/collapse handler) + if (item.action) { + item.action(); + } + // Also toggle local state as fallback + this.toggleGroup(item); + } else { + if (item.action) { + item.action(); + } + this.itemClick.emit(item); + + if (this.isMobile()) { + this.showMobileMenu.set(false); + } + } + } + + // Resize functionality + onResizeStart(event: MouseEvent): void { + if (!this.config.resizable || this.isMobile()) return; + + this.isResizing.set(true); + this.startX.set(event.clientX); + this.startWidth.set(this.isExpanded() ? this.currentWidth() : 72); + + document.addEventListener('mousemove', this.handleDrag.bind(this)); + document.addEventListener('mouseup', this.handleDragEnd.bind(this)); + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + + event.preventDefault(); + } + + private handleDrag(event: MouseEvent): void { + if (!this.isResizing()) return; + + const deltaX = + this.config.position === 'right' + ? this.startX() - event.clientX + : event.clientX - this.startX(); + + const potentialWidth = this.startWidth() + deltaX; + + // Handle seamless mode transitions + if (this.isExpanded()) { + // Currently expanded - check for collapse threshold + if (potentialWidth < this.collapseThreshold()) { + // Auto-collapse without releasing mouse + this.isExpanded.set(false); + this.expandedChange.emit(this.isExpanded()); + + // Reset starting point for continued dragging from collapsed state + this.startWidth.set(72); + this.startX.set(event.clientX); + + // Visual feedback + this.updateSidebarVisualFeedback('normal'); + return; + } + + // Show visual feedback when approaching collapse threshold + if (potentialWidth < this.collapseThreshold() + 20) { + this.updateSidebarVisualFeedback('collapse'); + } else { + this.updateSidebarVisualFeedback('normal'); + } + + // Normal resize when expanded + const newWidth = Math.max( + this.minWidth(), + Math.min(this.maxWidth(), potentialWidth), + ); + + this.currentWidth.set(newWidth); + } else { + // Currently collapsed - check for expand threshold + const collapsedWidth = 72; + const draggedWidth = collapsedWidth + deltaX; + + if (draggedWidth > this.expandThreshold()) { + // Auto-expand without releasing mouse + this.isExpanded.set(true); + const newWidth = this.expandThreshold() + 20; // Start slightly beyond threshold + this.currentWidth.set(newWidth); + this.expandedChange.emit(this.isExpanded()); + + // Reset starting point for continued dragging from expanded state + this.startWidth.set(this.currentWidth()); + this.startX.set(event.clientX); + + // Visual feedback + this.updateSidebarVisualFeedback('normal'); + return; + } + + // Show visual feedback when approaching threshold + if (draggedWidth > this.expandThreshold() - 20) { + this.updateSidebarVisualFeedback('expand'); + } else { + this.updateSidebarVisualFeedback('normal'); + } + } + } + + private handleDragEnd(): void { + if (!this.isResizing()) return; + + this.isResizing.set(false); + document.removeEventListener('mousemove', this.handleDrag.bind(this)); + document.removeEventListener('mouseup', this.handleDragEnd.bind(this)); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Reset visual feedback + this.updateSidebarVisualFeedback('normal'); + + // Save width to localStorage (only save if expanded) + if (this.isExpanded()) { + localStorage.setItem('nav-sidebar-width', this.currentWidth().toString()); + } + } + + private updateSidebarWidth(): void { + const sidebar = document.querySelector('.nav-sidebar') as HTMLElement; + if (sidebar && !this.isMobile()) { + if (this.isExpanded()) { + sidebar.style.setProperty('width', `${this.currentWidth()}px`, 'important'); + } else { + sidebar.style.setProperty('width', '72px', 'important'); + } + } + } + + private updateSidebarVisualFeedback(state: 'normal' | 'collapse' | 'expand'): void { + const sidebar = document.querySelector('.nav-sidebar') as HTMLElement; + if (!sidebar) return; + + // Remove all feedback classes + sidebar.classList.remove('collapse-zone', 'expand-zone'); + + // Add appropriate class based on state + if (state === 'collapse') { + sidebar.classList.add('collapse-zone'); + } else if (state === 'expand') { + sidebar.classList.add('expand-zone'); + } + } +} diff --git a/src/app/core-ui/main-header/main-header.component.html b/src/app/core-ui/main-header/main-header.component.html index 0260908c62..d0b5c46f86 100644 --- a/src/app/core-ui/main-header/main-header.component.html +++ b/src/app/core-ui/main-header/main-header.component.html @@ -1,12 +1,12 @@
@if (!isNavAlwaysVisible()) { - + + + + + + + } ({ + mainContext: mainContext?.taskIds?.length || 0, + inboxContext: inboxContext?.taskIds?.length || 0, + }), + ), + ), + { initialValue: { mainContext: 0, inboxContext: 0 } as TaskCounts }, + ); + + // Computed navigation config + readonly navConfig = computed(() => { + const items = this._buildNavItems(); + return { + items, + expandedByDefault: true, + showLabels: true, + mobileBreakpoint: 768, + position: 'left', + theme: 'light', + resizable: true, + minWidth: 200, + maxWidth: 400, + defaultWidth: 260, + collapseThreshold: 150, + expandThreshold: 180, + }; + }); + + private _buildNavItems(): NavItem[] { + const items: NavItem[] = []; + + // Main context items + this._addMainContextItems(items); + + // Separator after main context + this._addSeparator(items, 'separator-1'); + + // Main navigation routes + this._addMainRouteItems(items); + + // Separator after main routes + this._addSeparator(items, 'separator-2'); + + // Projects section + this._addProjectsSection(items); + + // Tags section + this._addTagsSection(items); + + // Separator before app section + this._addSeparator(items, 'separator-3'); + + // App section (search, planned, help, settings) + this._addAppSection(items); + + return items; + } + + private _addMainContextItems(items: NavItem[]): void { + const mainContext = this._mainWorkContext(); + const inboxContext = this._inboxContext(); + const taskCounts = this._taskCounts(); + + // Add Today context using side-nav-item component + if (mainContext) { + items.push({ + id: `main-${mainContext.id}`, + label: mainContext.title, + icon: mainContext.icon || 'inbox', + route: `/tag/${mainContext.id}/tasks`, + badge: taskCounts.mainContext > 0 ? taskCounts.mainContext.toString() : undefined, + // Work context data for side-nav-item component + workContext: mainContext, + workContextType: WorkContextType.TAG, + defaultIcon: mainContext.icon || 'inbox', + }); + } + + // Add Inbox context using side-nav-item component + if (inboxContext) { + items.push({ + id: `inbox-${inboxContext.id}`, + label: inboxContext.title, + icon: inboxContext.icon || 'inbox', + route: `/project/${inboxContext.id}/tasks`, + badge: + taskCounts.inboxContext > 0 ? taskCounts.inboxContext.toString() : undefined, + // Work context data for side-nav-item component + workContext: inboxContext, + workContextType: WorkContextType.PROJECT, + defaultIcon: inboxContext.icon || 'inbox', + }); + } + } + + private _addMainRouteItems(items: NavItem[]): void { + items.push( + { + id: 'schedule', + label: T.MH.SCHEDULE, + icon: 'early_on', // Keep icon as fallback + svgIcon: 'early_on', // Use SVG icon + route: '/schedule', + }, + { + id: 'planner', + label: T.MH.PLANNER, + icon: 'edit_calendar', + route: '/planner', + }, + { + id: 'boards', + label: T.MH.BOARDS, + icon: 'grid_view', + route: '/boards', + }, + ); + } + + private _addProjectsSection(items: NavItem[]): void { + const projects = this._allVisibleProjects(); + const allNonInboxProjects = projects || []; + + // Always provide all projects - let the magic-side-nav handle visibility + const projectChildren: NavItem[] = allNonInboxProjects.map((project) => ({ + id: `project-${project.id}`, + label: project.title, + icon: project.icon || 'folder_special', + route: `/project/${project.id}/tasks`, + badge: project.taskIds?.length > 0 ? project.taskIds.length.toString() : undefined, + // Work context data for side-nav-item component + workContext: project, + workContextType: WorkContextType.PROJECT, + defaultIcon: 'folder_special', + })); + + // Context menu items for project visibility + const contextMenuItems: NavItem[] = allNonInboxProjects.map((project) => ({ + id: `toggle-visibility-${project.id}`, + label: project.title, + icon: (project as any).isHiddenFromMenu ? 'visibility_off' : 'visibility', + action: () => this._toggleProjectVisibility(project), + })); + + // Always show the projects section, even if empty + items.push({ + id: 'projects', + label: T.MH.PROJECTS, + icon: 'expand_more', + children: projectChildren, + action: () => this._toggleProjectsExpansion(), + additionalButtons: [ + { + id: 'project-visibility', + icon: 'visibility_off', + tooltip: 'Toggle project visibility', + hidden: allNonInboxProjects.length === 0, + contextMenu: contextMenuItems, + }, + { + id: 'add-project', + icon: 'add', + tooltip: T.MH.CREATE_PROJECT, + action: () => this.addProject(), + }, + ], + contextMenuItems, + }); + } + + private _addTagsSection(items: NavItem[]): void { + const tags = this._tagList(); + + // Always provide all tags - let the magic-side-nav handle visibility + const tagChildren: NavItem[] = (tags || []).map((tag) => ({ + id: `tag-${tag.id}`, + label: tag.title, + icon: tag.icon || 'label', + route: `/tag/${tag.id}/tasks`, + badge: tag.taskIds?.length > 0 ? tag.taskIds.length.toString() : undefined, + // Work context data for side-nav-item component + workContext: tag, + workContextType: WorkContextType.TAG, + defaultIcon: 'label', + })); + + // Always show the tags section, even if empty + items.push({ + id: 'tags', + label: T.MH.TAGS, + icon: 'expand_more', + children: tagChildren, + action: () => this._toggleTagsExpansion(), + // Tags don't have additional buttons like projects do + }); + } + + private _addAppSection(items: NavItem[]): void { + items.push( + { + id: 'search', + label: T.MH.SEARCH, + icon: 'search', + route: '/search', + }, + { + id: 'scheduled-list', + label: T.MH.ALL_PLANNED_LIST, + icon: 'list', + route: '/scheduled-list', + }, + { + id: 'help', + label: T.MH.HELP, + icon: 'help_center', + children: [ + { + id: 'help-online', + label: T.MH.HM.GET_HELP_ONLINE, + icon: 'help_center', + action: () => this._openHelp(), + }, + { + id: 'help-report', + label: T.MH.HM.REPORT_A_PROBLEM, + icon: 'bug_report', + action: () => this._reportProblem(), + }, + { + id: 'help-contribute', + label: T.MH.HM.CONTRIBUTE, + icon: 'volunteer_activism', + action: () => this._openContribute(), + }, + { + id: 'help-reddit', + label: T.MH.HM.REDDIT_COMMUNITY, + icon: 'forum', + action: () => this._openReddit(), + }, + { + id: 'help-tour-welcome', + label: T.MH.HM.START_WELCOME, + icon: 'directions', + action: () => this._startTour(TourId.Welcome), + }, + { + id: 'help-tour-keyboard', + label: T.MH.HM.KEYBOARD, + icon: 'directions', + action: () => this._startTour(TourId.KeyboardNav), + }, + { + id: 'help-tour-sync', + label: T.MH.HM.SYNC, + icon: 'directions', + action: () => this._startTour(TourId.Sync), + }, + { + id: 'help-tour-calendars', + label: T.MH.HM.CALENDARS, + icon: 'directions', + action: () => this._startTour(TourId.IssueProviders), + }, + ], + }, + { + id: 'settings', + label: T.MH.SETTINGS, + icon: 'settings', + route: '/config', + }, + ); + } + + // Action handlers + private _toggleProjectsExpansion(): void { + const newState = !this._isProjectsExpanded(); + this._isProjectsExpanded.set(newState); + localStorage.setItem(LS.IS_PROJECT_LIST_EXPANDED, newState.toString()); + } + + private _toggleTagsExpansion(): void { + const newState = !this._isTagsExpanded(); + this._isTagsExpanded.set(newState); + localStorage.setItem(LS.IS_TAG_LIST_EXPANDED, newState.toString()); + } + + addProject(): void { + this._matDialog.open(DialogCreateProjectComponent, { + restoreFocus: true, + }); + } + + private _toggleProjectVisibility(project: any): void { + this._store.dispatch(toggleHideFromMenu({ id: project.id })); + } + + private _startTour(tourId: TourId): void { + this._shepherdService.show(tourId); + } + + private _openHelp(): void { + window.open( + 'https://github.com/johannesjo/super-productivity/blob/master/README.md#question-how-to-use-it', + '_blank', + ); + } + + private _reportProblem(): void { + const url = getGithubErrorUrl('', undefined, true); + window.open(url, '_blank'); + } + + private _openContribute(): void { + window.open( + 'https://github.com/johannesjo/super-productivity/blob/master/CONTRIBUTING.md', + '_blank', + ); + } + + private _openReddit(): void { + window.open('https://www.reddit.com/r/superProductivity/', '_blank'); + } + + // State helpers + private _fetchProjectListState(): boolean { + const stored = localStorage.getItem(LS.IS_PROJECT_LIST_EXPANDED); + // Default to true if not set, or if set to 'true' + return stored === null || stored === 'true'; + } + + private _fetchTagListState(): boolean { + const stored = localStorage.getItem(LS.IS_TAG_LIST_EXPANDED); + // Default to true if not set, or if set to 'true' + return stored === null || stored === 'true'; + } + + private _addSeparator(items: NavItem[], id: string): void { + items.push({ + id, + label: '', + icon: '', + action: () => {}, // No-op action for separators + }); + } + + // Public methods for external use + onNavItemClick(item: NavItem): void { + if (item.action) { + item.action(); + } else if (item.route) { + this._router.navigate([item.route]); + } + } + + // Getter for expansion states + get isProjectsExpanded(): boolean { + return this._isProjectsExpanded(); + } + + get isTagsExpanded(): boolean { + return this._isTagsExpanded(); + } + + // Computed signal for expanded group IDs + readonly expandedGroupIds = computed(() => { + const expandedIds = new Set(); + if (this._isProjectsExpanded()) { + expandedIds.add('projects'); + } + if (this._isTagsExpanded()) { + expandedIds.add('tags'); + } + return expandedIds; + }); +} diff --git a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss index 0b5c972e5f..159c3ab4a1 100644 --- a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss +++ b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss @@ -1,8 +1,31 @@ @use '_globals' as *; :host { - button .badge { - display: none; + display: flex !important; + align-items: stretch; + position: relative; + width: 100%; + + button:first-of-type { + white-space: nowrap; + overflow: hidden; + flex: 1; + min-width: 0; + + .text { + overflow: hidden; + text-overflow: ellipsis; + } + + .badge { + display: none; + } + } + + .additional-btn { + flex-shrink: 0; + width: 40px !important; + min-width: 40px !important; } &.hasTasks { diff --git a/src/app/core-ui/side-nav/side-nav.component.html b/src/app/core-ui/side-nav/side-nav.component.html index 6e85e94332..8e8052f1a9 100644 --- a/src/app/core-ui/side-nav/side-nav.component.html +++ b/src/app/core-ui/side-nav/side-nav.component.html @@ -59,7 +59,7 @@
@@ -211,7 +211,7 @@ - - - @if (isExpanded() || (isMobile() && showMobileMenu())) { - @for (btn of item.additionalButtons; track btn.id) { - @if (!btn.hidden) { - - } - } - } - - - @if (item.contextMenuItems && item.contextMenuItems.length > 0) { - - - @for (contextItem of item.contextMenuItems; track contextItem.id) { - - } - - } -
- } @else { - - - } - } - - - @if (item.children && item.children.length > 0) { - - } - + } } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index d38f8e3db6..171d927e2b 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -26,6 +26,8 @@ import { } from '../../features/work-context/work-context.model'; import { SideNavItemComponent } from '../side-nav/side-nav-item/side-nav-item.component'; import { ContextMenuComponent } from '../../ui/context-menu/context-menu.component'; +import { NavEntryComponent } from './nav-entry/nav-entry.component'; +import { NavMultiBtnComponent } from './nav-multi-btn/nav-multi-btn.component'; export interface NavItem { id: string; @@ -87,6 +89,8 @@ export interface NavConfig { TranslatePipe, SideNavItemComponent, ContextMenuComponent, + NavEntryComponent, + NavMultiBtnComponent, ], templateUrl: './magic-side-nav.html', styleUrl: './magic-side-nav.scss', @@ -118,6 +122,8 @@ export class MagicSideNavComponent implements OnInit { isMobile = signal(false); showMobileMenu = signal(false); expandedGroups = signal(new Set()); + // Visual feedback state for resizing thresholds + resizeState = signal<'normal' | 'collapse' | 'expand'>('normal'); // Resize functionality currentWidth = signal(260); @@ -136,12 +142,12 @@ export class MagicSideNavComponent implements OnInit { return this.currentWidth(); }); - constructor() { - // Effect to update sidebar width whenever signals change - effect(() => { - this.updateSidebarWidth(); - }); + // Keep stable references for event listeners to ensure proper cleanup + private readonly onDrag: (event: MouseEvent) => void = (event: MouseEvent) => + this.handleDrag(event); + private readonly onDragEnd: () => void = () => this.handleDragEnd(); + constructor() { // Effect to emit width changes effect(() => { this.widthChange.emit(this.sidebarWidth()); @@ -274,8 +280,8 @@ export class MagicSideNavComponent implements OnInit { this.startX.set(event.clientX); this.startWidth.set(this.isExpanded() ? this.currentWidth() : 72); - document.addEventListener('mousemove', this.handleDrag.bind(this)); - document.addEventListener('mouseup', this.handleDragEnd.bind(this)); + document.addEventListener('mousemove', this.onDrag); + document.addEventListener('mouseup', this.onDragEnd); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; @@ -305,15 +311,15 @@ export class MagicSideNavComponent implements OnInit { this.startX.set(event.clientX); // Visual feedback - this.updateSidebarVisualFeedback('normal'); + this.resizeState.set('normal'); return; } // Show visual feedback when approaching collapse threshold if (potentialWidth < this.collapseThreshold() + 20) { - this.updateSidebarVisualFeedback('collapse'); + this.resizeState.set('collapse'); } else { - this.updateSidebarVisualFeedback('normal'); + this.resizeState.set('normal'); } // Normal resize when expanded @@ -340,15 +346,15 @@ export class MagicSideNavComponent implements OnInit { this.startX.set(event.clientX); // Visual feedback - this.updateSidebarVisualFeedback('normal'); + this.resizeState.set('normal'); return; } // Show visual feedback when approaching threshold if (draggedWidth > this.expandThreshold() - 20) { - this.updateSidebarVisualFeedback('expand'); + this.resizeState.set('expand'); } else { - this.updateSidebarVisualFeedback('normal'); + this.resizeState.set('normal'); } } } @@ -357,13 +363,13 @@ export class MagicSideNavComponent implements OnInit { if (!this.isResizing()) return; this.isResizing.set(false); - document.removeEventListener('mousemove', this.handleDrag.bind(this)); - document.removeEventListener('mouseup', this.handleDragEnd.bind(this)); + document.removeEventListener('mousemove', this.onDrag); + document.removeEventListener('mouseup', this.onDragEnd); document.body.style.cursor = ''; document.body.style.userSelect = ''; // Reset visual feedback - this.updateSidebarVisualFeedback('normal'); + this.resizeState.set('normal'); // Save width to localStorage (only save if expanded) if (this.isExpanded()) { @@ -371,29 +377,5 @@ export class MagicSideNavComponent implements OnInit { } } - private updateSidebarWidth(): void { - const sidebar = document.querySelector('.nav-sidebar') as HTMLElement; - if (sidebar && !this.isMobile()) { - if (this.isExpanded()) { - sidebar.style.setProperty('width', `${this.currentWidth()}px`, 'important'); - } else { - sidebar.style.setProperty('width', '72px', 'important'); - } - } - } - - private updateSidebarVisualFeedback(state: 'normal' | 'collapse' | 'expand'): void { - const sidebar = document.querySelector('.nav-sidebar') as HTMLElement; - if (!sidebar) return; - - // Remove all feedback classes - sidebar.classList.remove('collapse-zone', 'expand-zone'); - - // Add appropriate class based on state - if (state === 'collapse') { - sidebar.classList.add('collapse-zone'); - } else if (state === 'expand') { - sidebar.classList.add('expand-zone'); - } - } + // Inline width binding via template replaces imperative DOM style updates } diff --git a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html new file mode 100644 index 0000000000..1766fdfbd8 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html @@ -0,0 +1,165 @@ + diff --git a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.scss b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.scss new file mode 100644 index 0000000000..87960de640 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.scss @@ -0,0 +1,279 @@ +// Navigation item styles +.nav-item { + margin-bottom: 2px; + position: relative; // For absolute positioning of children in collapsed mode + + &.has-children { + margin-bottom: 4px; + } +} + +// Navigation links +.nav-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: + all 0.2s ease, + padding var(--transition-standard, 0.3s) ease, + justify-content var(--transition-standard, 0.3s) ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; + + &:hover { + background: var(--sidebar-hover); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + + .nav-icon { + color: var(--sidebar-active-text); + } + } + + &.expanded .nav-chevron { + transform: rotate(180deg); + } +} + +// Navigation icon +.nav-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--sidebar-text-secondary); + font-size: 18px; + line-height: 1; + transition: + margin var(--transition-standard, 0.3s) ease, + transform var(--transition-standard, 0.3s) ease; +} + +// Navigation label +.nav-label { + flex: 1; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 1; + visibility: visible; + transform: translateX(0); + transition: + opacity var(--transition-standard, 0.3s) ease, + transform var(--transition-standard, 0.3s) ease, + visibility var(--transition-standard, 0.3s) ease; +} + +// Navigation badge +.nav-badge { + flex-shrink: 0; + padding: 2px 8px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); // Safari support + color: var(--sidebar-text); + border: 1px solid rgba(255, 255, 255, 0.12); + font-size: 11px; + font-weight: 500; + opacity: 1; + visibility: visible; + transform: translateX(0); + transition: + all 0.2s ease, + opacity var(--transition-standard, 0.3s) ease, + transform var(--transition-standard, 0.3s) ease, + visibility var(--transition-standard, 0.3s) ease; + + // Dark theme adjustments + :host-context(.isDarkTheme) & { + background: rgba(0, 0, 0, 0.15); + border: 1px solid rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.85); + } + + // Enhanced milkglass when background image is present + :host-context(.hasBgImage) & { + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + border: 1px solid rgba(255, 255, 255, 0.15); + } + + // Dark theme with background image + :host-context(.isDarkTheme.hasBgImage) & { + background: rgba(0, 0, 0, 0.25); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + border: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.9); + } +} + +// Navigation chevron +.nav-chevron { + flex-shrink: 0; + opacity: 1; + visibility: visible; + transform: translateX(0); + transition: + transform 0.2s ease, + opacity var(--transition-standard, 0.3s) ease, + visibility var(--transition-standard, 0.3s) ease; + color: var(--sidebar-text-secondary); +} + +// Children navigation +.nav-children { + list-style: none; + padding: 0; + margin: 4px 0 0 0; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + + &.visible { + max-height: 500px; + overflow: visible; // Allow overflow for dropdown in collapsed mode + } +} + +.nav-child-item { + margin-left: var(--s3); +} + +.nav-child-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s) var(--s2); + color: var(--sidebar-text-secondary); + text-decoration: none; + border-radius: 6px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + font-size: 14px; + + &:hover { + background: var(--sidebar-hover); + color: var(--sidebar-text); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + + .nav-icon { + color: var(--sidebar-active-text); + } + } + + .nav-icon { + width: 20px; + height: 20px; + font-size: 16px; + } +} + +// Work context items styling +.work-context-item { + // Remove default nav-child-item padding to let side-nav-item handle its own spacing + padding: 0; + margin: 0; + + ::ng-deep side-nav-item { + // Ensure side-nav-item fits well within the navigation structure + display: block; + width: 100%; + + // Override side-nav-item styles to match magic-side-nav theming + .g-multi-btn-wrapper { + background-color: transparent; + + .color-bar { + // Make color bar thinner to fit better in child context + width: 2px; + } + + button { + color: var(--sidebar-text); + padding: var(--s) var(--s2); + font-size: 14px; + + &:hover { + background-color: var(--sidebar-hover); + } + + &.isActiveRoute { + background-color: var(--sidebar-active); + color: var(--sidebar-active-text); + } + } + + .additional-btn { + color: var(--sidebar-text-secondary); + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } + } + } + } +} + +// Top-level work context items (Today/Inbox) +.work-context-item-main { + display: block; + width: 100%; + + ::ng-deep side-nav-item { + display: block; + width: 100%; + + .g-multi-btn-wrapper { + background-color: transparent; + + button { + color: var(--sidebar-text); + + &:hover { + background-color: var(--sidebar-hover); + } + + &.isActiveRoute { + background-color: var(--sidebar-active); + color: var(--sidebar-active-text); + } + } + + .additional-btn { + color: var(--sidebar-text-secondary); + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } + } + } + } +} diff --git a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.ts b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.ts new file mode 100644 index 0000000000..c026ff6fc0 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { NavItem } from '../magic-side-nav'; +import { SideNavItemComponent } from '../../side-nav/side-nav-item/side-nav-item.component'; +import { NavMultiBtnComponent } from '../nav-multi-btn/nav-multi-btn.component'; +import { NavRowComponent } from '../nav-row/nav-row.component'; + +@Component({ + selector: 'nav-entry', + standalone: true, + imports: [ + CommonModule, + RouterModule, + SideNavItemComponent, + NavMultiBtnComponent, + NavRowComponent, + ], + templateUrl: './nav-entry.component.html', + styleUrl: './nav-entry.component.scss', +}) +export class NavEntryComponent { + @Input({ required: true }) item!: NavItem; + // Whether labels and badges should be visible (driven by parent nav state) + @Input() showText = true; + @Input() isGroupExpanded: boolean = false; + @Input() activeWorkContextId: string | null = null; + + @Output() itemClick = new EventEmitter(); + + onItemClick(): void { + this.itemClick.emit(this.item); + } + + onChildClick(child: NavItem): void { + this.itemClick.emit(child); + } +} diff --git a/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.html b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.html new file mode 100644 index 0000000000..48be3121f2 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.html @@ -0,0 +1,62 @@ +
+ + + + @if (showText) { + @for (btn of item.additionalButtons; track btn.id) { + @if (!btn.hidden) { + + } + } + } + + + @if (item.contextMenuItems && item.contextMenuItems.length > 0) { + + + @for (contextItem of item.contextMenuItems; track contextItem.id) { + + } + + } +
diff --git a/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.scss b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.scss new file mode 100644 index 0000000000..7e2bd63279 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.scss @@ -0,0 +1,162 @@ +// Multi-button wrapper styling for expandable sections with additional buttons +.g-multi-btn-wrapper { + display: flex; + align-items: center; + position: relative; + + .expand-btn { + flex: 1; + text-align: left; + + &.isExpanded .nav-icon { + transform: rotate(180deg); + } + } + + .additional-btn { + flex-shrink: 0; + width: 32px; + height: 32px; + min-width: 32px; + padding: 4px; + margin-left: var(--s); + color: var(--sidebar-text-secondary); + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } + + ::ng-deep mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + } + + .title { + font-weight: 500; + } +} + +// Navigation links (inherited from parent but scoped to this component) +.nav-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: + all 0.2s ease, + padding var(--transition-standard, 0.3s) ease, + justify-content var(--transition-standard, 0.3s) ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; + + &:hover { + background: var(--sidebar-hover); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + + .nav-icon { + color: var(--sidebar-active-text); + } + } + + &.expanded .nav-chevron { + transform: rotate(180deg); + } +} + +// Navigation icon +.nav-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--sidebar-text-secondary); + font-size: 18px; + line-height: 1; + transition: + margin var(--transition-standard, 0.3s) ease, + transform var(--transition-standard, 0.3s) ease; +} + +// Navigation label +.nav-label { + flex: 1; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 1; + visibility: visible; + transform: translateX(0); + transition: + opacity var(--transition-standard, 0.3s) ease, + transform var(--transition-standard, 0.3s) ease, + visibility var(--transition-standard, 0.3s) ease; +} + +// Navigation badge +.nav-badge { + flex-shrink: 0; + padding: 2px 8px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); // Safari support + color: var(--sidebar-text); + border: 1px solid rgba(255, 255, 255, 0.12); + font-size: 11px; + font-weight: 500; + opacity: 1; + visibility: visible; + transform: translateX(0); + transition: + all 0.2s ease, + opacity var(--transition-standard, 0.3s) ease, + transform var(--transition-standard, 0.3s) ease, + visibility var(--transition-standard, 0.3s) ease; + + // Dark theme adjustments + :host-context(.isDarkTheme) & { + background: rgba(0, 0, 0, 0.15); + border: 1px solid rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.85); + } + + // Enhanced milkglass when background image is present + :host-context(.hasBgImage) & { + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + border: 1px solid rgba(255, 255, 255, 0.15); + } + + // Dark theme with background image + :host-context(.isDarkTheme.hasBgImage) & { + background: rgba(0, 0, 0, 0.25); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + border: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.9); + } +} diff --git a/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.ts b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.ts new file mode 100644 index 0000000000..122e3a46c4 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.ts @@ -0,0 +1,51 @@ +import { + Component, + Input, + Output, + EventEmitter, + ViewChild, + ElementRef, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatIcon } from '@angular/material/icon'; +import { MatIconButton } from '@angular/material/button'; +import { MatTooltip } from '@angular/material/tooltip'; +import { NavItem, NavAdditionalButton } from '../magic-side-nav'; +import { ContextMenuComponent } from '../../../ui/context-menu/context-menu.component'; +import { MatMenuItem } from '@angular/material/menu'; +import { NavRowComponent } from '../nav-row/nav-row.component'; + +@Component({ + selector: 'nav-multi-btn', + standalone: true, + imports: [ + CommonModule, + MatIcon, + MatIconButton, + MatTooltip, + ContextMenuComponent, + MatMenuItem, + NavRowComponent, + ], + templateUrl: './nav-multi-btn.component.html', + styleUrl: './nav-multi-btn.component.scss', +}) +export class NavMultiBtnComponent { + @Input({ required: true }) item!: NavItem; + // Whether labels and badges should be visible (driven by parent nav state) + @Input() showText = true; + @Input() isGroupExpanded: boolean = false; + + @Output() itemClick = new EventEmitter(); + + @ViewChild('expandBtn', { read: ElementRef }) expandBtn?: ElementRef; + + onExpandClick(): void { + this.itemClick.emit(this.item); + } + + onAdditionalButtonClick(btn: NavAdditionalButton, event: Event): void { + btn.action?.(); + event.stopPropagation(); + } +} From ae23f82a5dba14bc130e8ac5fab9fa3e6747081e Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sun, 7 Sep 2025 14:00:45 +0200 Subject: [PATCH 05/61] refactor(sideNav): refactor into multiple components 2 --- .../magic-side-nav/magic-side-nav.html | 30 +-- .../core-ui/magic-side-nav/magic-side-nav.ts | 107 +++++++--- .../nav-entry/nav-entry.component.html | 189 ++++++++++-------- .../nav-multi-btn/nav-multi-btn.component.ts | 2 + .../nav-row/nav-row.component.ts | 59 ++++++ .../side-nav/side-nav-config.service.ts | 77 +++++-- 6 files changed, 320 insertions(+), 144 deletions(-) create mode 100644 src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.html b/src/app/core-ui/magic-side-nav/magic-side-nav.html index ed99128711..8c79b9e442 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.html @@ -77,20 +77,22 @@ role="list" > @for (item of config.items; track item.id) { - - @if (item.id.startsWith('separator-') && item.label === '' && item.icon === '') { - - } @else { - + @switch (item.type) { + @case ('separator') { + + } + @default { + + } } } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index 171d927e2b..b5255814d8 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -29,24 +29,73 @@ import { ContextMenuComponent } from '../../ui/context-menu/context-menu.compone import { NavEntryComponent } from './nav-entry/nav-entry.component'; import { NavMultiBtnComponent } from './nav-multi-btn/nav-multi-btn.component'; -export interface NavItem { +export type NavItem = + | NavSeparatorItem + | NavWorkContextItem + | NavRouteItem + | NavHrefItem + | NavActionItem + | NavGroupItem; + +export interface NavBaseItem { + id: string; + label?: string; + icon?: string; + svgIcon?: string; + badge?: string | number; +} + +export interface NavSeparatorItem extends NavBaseItem { + type: 'separator'; +} + +export interface NavWorkContextItem extends NavBaseItem { + type: 'workContext'; + label: string; + icon: string; + route: string; + workContext: WorkContextCommon; + workContextType: WorkContextType; + defaultIcon: string; +} + +export interface NavRouteItem extends NavBaseItem { + type: 'route'; + label: string; + icon: string; + route: string; +} + +export interface NavHrefItem extends NavBaseItem { + type: 'href'; + label: string; + icon: string; + href: string; +} + +export interface NavActionItem extends NavBaseItem { + type: 'action'; + label: string; + icon: string; + action: () => void; +} + +export interface NavContextItem { id: string; label: string; icon: string; - svgIcon?: string; // For SVG icons like schedule - route?: string; // Internal Angular route - href?: string; // External link + svgIcon?: string; action?: () => void; - children?: NavItem[]; - badge?: string | number; - // For work context items (projects/tags) - workContext?: WorkContextCommon; - workContextType?: WorkContextType; - defaultIcon?: string; - // Additional buttons for navigation sections +} + +export interface NavGroupItem extends NavBaseItem { + type: 'group'; + label: string; + icon: string; + children: NavItem[]; additionalButtons?: NavAdditionalButton[]; - // Context menu items - contextMenuItems?: NavItem[]; + contextMenuItems?: NavContextItem[]; + action?: () => void; // optional external toggle logic } export interface NavAdditionalButton { @@ -55,7 +104,7 @@ export interface NavAdditionalButton { tooltip?: string; action?: () => void; hidden?: boolean; - contextMenu?: NavItem[]; + contextMenu?: NavContextItem[]; } export interface NavConfig { @@ -227,7 +276,7 @@ export class MagicSideNavComponent implements OnInit { } toggleGroup(item: NavItem): void { - if (item.children && item.children.length > 0) { + if (item.type === 'group' && item.children && item.children.length > 0) { this.expandedGroups.update((groups) => { const newGroups = new Set(groups); if (newGroups.has(item.id)) { @@ -241,6 +290,7 @@ export class MagicSideNavComponent implements OnInit { } isGroupExpanded(item: NavItem): boolean { + if (item.type !== 'group') return false; // Use external expansion state if available, otherwise fall back to local state if (this.expandedGroupIds && this.expandedGroupIds.size > 0) { return this.expandedGroupIds.has(item.id); @@ -249,26 +299,25 @@ export class MagicSideNavComponent implements OnInit { } onItemClick(item: NavItem): void { - // Special handling for expandable sections (projects/tags) - they should always be toggleable - const isExpandableSection = item.id === 'projects' || item.id === 'tags'; - - if ((item.children && item.children.length > 0) || isExpandableSection) { - // For items with children OR expandable sections, call the action + if (item.type === 'group') { + // Allow external toggle handler if provided if (item.action) { item.action(); } else { - // Only use local state if no external action is provided this.toggleGroup(item); } - } else { - if (item.action) { - item.action(); - } - this.itemClick.emit(item); + return; + } - if (this.isMobile()) { - this.showMobileMenu.set(false); - } + if (item.type === 'action') { + item.action?.(); + } + + // Emit to parent for external handling (e.g., href open) + this.itemClick.emit(item); + + if (this.isMobile()) { + this.showMobileMenu.set(false); } } diff --git a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html index 1766fdfbd8..347d969f92 100644 --- a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html +++ b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html @@ -1,72 +1,60 @@ } diff --git a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.scss b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.scss index 87960de640..419152dfb7 100644 --- a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.scss @@ -8,6 +8,12 @@ } } +// Ensure nested nav-item and nav-row components inherit styles +::ng-deep nav-item, +::ng-deep nav-row { + display: contents; // Make these components transparent to layout +} + // Navigation links .nav-link { display: flex; diff --git a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.ts b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.ts index c026ff6fc0..b2fdeaba6f 100644 --- a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.ts @@ -1,21 +1,13 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; import { NavItem } from '../magic-side-nav'; -import { SideNavItemComponent } from '../../side-nav/side-nav-item/side-nav-item.component'; import { NavMultiBtnComponent } from '../nav-multi-btn/nav-multi-btn.component'; -import { NavRowComponent } from '../nav-row/nav-row.component'; +import { NavItemComponent } from '../nav-item/nav-item.component'; @Component({ selector: 'nav-entry', standalone: true, - imports: [ - CommonModule, - RouterModule, - SideNavItemComponent, - NavMultiBtnComponent, - NavRowComponent, - ], + imports: [CommonModule, NavMultiBtnComponent, NavItemComponent], templateUrl: './nav-entry.component.html', styleUrl: './nav-entry.component.scss', }) diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html new file mode 100644 index 0000000000..4313113b72 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -0,0 +1,84 @@ +@switch (item.type) { + @case ('workContext') { + + } + + @case ('route') { + + + + } + + @case ('href') { + + + + } + + @case ('action') { + + } + + @case ('group') { + + } +} diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts new file mode 100644 index 0000000000..d03f1fdcb1 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts @@ -0,0 +1,51 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { MatIcon } from '@angular/material/icon'; +import { MatIconButton } from '@angular/material/button'; +import { MatTooltip } from '@angular/material/tooltip'; +import { TranslatePipe } from '@ngx-translate/core'; +import { NavItem } from '../magic-side-nav'; +import { SideNavItemComponent } from '../../side-nav/side-nav-item/side-nav-item.component'; +import { NavRowComponent } from '../nav-row/nav-row.component'; + +@Component({ + selector: 'nav-item', + standalone: true, + imports: [ + CommonModule, + RouterModule, + MatIcon, + MatIconButton, + MatTooltip, + TranslatePipe, + SideNavItemComponent, + NavRowComponent, + ], + templateUrl: './nav-item.component.html', +}) +export class NavItemComponent { + @Input({ required: true }) item!: NavItem; + @Input() showText = true; + @Input() isExpanded = false; + @Input() activeWorkContextId: string | null = null; + @Input() isChild = false; + + @Output() itemClick = new EventEmitter(); + + get linkClass(): string { + return this.isChild ? 'nav-child-link' : 'nav-link'; + } + + get itemClass(): string { + return this.isChild ? 'nav-child-item' : 'nav-item'; + } + + onItemClick(): void { + this.itemClick.emit(this.item); + } + + shouldShowTooltip(): boolean { + return !this.showText && !!this.item.label; + } +} diff --git a/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.html b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.html index 48be3121f2..3b36c3d2d0 100644 --- a/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.html +++ b/src/app/core-ui/magic-side-nav/nav-multi-btn/nav-multi-btn.component.html @@ -15,7 +15,6 @@ [label]="item.label" [badge]="item.badge" [showText]="showText" - [showChevron]="item.children && item.children.length > 0 && showText" labelClass="title" > diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.html b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.html new file mode 100644 index 0000000000..cbb1a11f35 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.html @@ -0,0 +1,34 @@ +@if (svgIcon) { + +} @else if (icon) { + {{ icon }} +} + +@if (showText && label) { + {{ label | translate }} +} + +@if (showText && badge) { + {{ badge }} +} + +@if (showChevron) { + + + +} diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts index 90f1e5907e..2b0c017850 100644 --- a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts @@ -7,53 +7,14 @@ import { TranslatePipe } from '@ngx-translate/core'; selector: 'nav-row', standalone: true, imports: [CommonModule, MatIcon, TranslatePipe], - template: ` - - @if (svgIcon) { - - } @else if (icon) { - {{ icon }} - } - - - @if (showText && label) { - {{ label | translate }} - } - - - @if (showText && badge !== undefined && badge !== null && badge !== '') { - {{ badge }} - } - - - @if (showText && showChevron) { - - - - } - `, + templateUrl: './nav-row.component.html', }) export class NavRowComponent { - @Input() icon: string | undefined; - @Input() svgIcon: string | undefined; - @Input() label: string | undefined; - @Input() badge: string | number | undefined | null; + @Input() icon?: string; + @Input() svgIcon?: string; + @Input() label?: string; + @Input() badge?: string | number; @Input() showText = true; @Input() showChevron = false; - @Input() labelClass = ''; + @Input() labelClass?: string; } From 0cfc4d360dc840c248e3b41746eb720f2d493eff Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sun, 7 Sep 2025 15:17:51 +0200 Subject: [PATCH 07/61] refactor(sideNav): refactor into multiple components 4 --- .../magic-side-nav/magic-side-nav.scss | 179 +----------------- .../core-ui/magic-side-nav/magic-side-nav.ts | 2 +- .../nav-entry/nav-entry.component.html | 55 +++--- .../nav-entry/nav-entry.component.scss | 85 +-------- .../nav-entry/nav-entry.component.ts | 29 ++- .../nav-item/nav-item.component.html | 81 ++++---- .../nav-item/nav-item.component.scss | 41 ++++ .../nav-item/nav-item.component.ts | 77 ++++++-- .../nav-multi-btn.component.html | 24 +-- .../nav-multi-btn/nav-multi-btn.component.ts | 21 +- .../nav-row/nav-row.component.scss | 39 ++++ .../nav-row/nav-row.component.ts | 39 ++++ .../side-nav-item.component.html | 0 .../side-nav-item.component.scss | 40 +++- .../side-nav-item.component.spec.ts | 0 .../side-nav-item/side-nav-item.component.ts | 13 ++ .../core-ui/side-nav/side-nav.component.ts | 2 +- 17 files changed, 348 insertions(+), 379 deletions(-) create mode 100644 src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss create mode 100644 src/app/core-ui/magic-side-nav/nav-row/nav-row.component.scss rename src/app/core-ui/{side-nav => magic-side-nav}/side-nav-item/side-nav-item.component.html (100%) rename src/app/core-ui/{side-nav => magic-side-nav}/side-nav-item/side-nav-item.component.scss (73%) rename src/app/core-ui/{side-nav => magic-side-nav}/side-nav-item/side-nav-item.component.spec.ts (100%) rename src/app/core-ui/{side-nav => magic-side-nav}/side-nav-item/side-nav-item.component.ts (87%) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.scss index ccfbd946dc..3f053c9001 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.scss @@ -576,53 +576,6 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); } } -// Integration styles for side-nav-item components -.work-context-item { - // Remove default nav-child-item padding to let side-nav-item handle its own spacing - padding: 0; - margin: 0; - - side-nav-item { - // Ensure side-nav-item fits well within the navigation structure - display: block; - width: 100%; - - // Override side-nav-item styles to match magic-side-nav theming - .g-multi-btn-wrapper { - background-color: transparent; - - .color-bar { - // Make color bar thinner to fit better in child context - width: 2px; - } - - button { - color: var(--sidebar-text); - padding: var(--s) var(--s2); - font-size: 14px; - - &:hover { - background-color: var(--sidebar-hover); - } - - &.isActiveRoute { - background-color: var(--sidebar-active); - color: var(--sidebar-active-text); - } - } - - .additional-btn { - color: var(--sidebar-text-secondary); - - &:hover { - background-color: var(--sidebar-hover); - color: var(--sidebar-text); - } - } - } - } -} - // Media queries @media (max-width: $mobile-breakpoint) { .nav-sidebar { @@ -644,134 +597,6 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); } } -// Top-level work context items (Today/Inbox) -.work-context-item-main { - display: block; - width: 100%; +// Removed deep overrides for side-nav-item; it now supports a 'nav' variant - side-nav-item { - display: block; - width: 100%; - - .g-multi-btn-wrapper { - background-color: transparent; - - button { - color: var(--sidebar-text); - - &:hover { - background-color: var(--sidebar-hover); - } - - &.isActiveRoute { - background-color: var(--sidebar-active); - color: var(--sidebar-active-text); - } - } - - .additional-btn { - color: var(--sidebar-text-secondary); - - &:hover { - background-color: var(--sidebar-hover); - color: var(--sidebar-text); - } - } - } - } -} - -// Ensure nav-entry child components inherit proper styling -::ng-deep nav-entry { - display: contents; // Make nav-entry transparent to layout - - nav-item, - nav-row { - display: contents; // Make these components transparent to layout - } -} - -// Global nav component styling to ensure proper inheritance -::ng-deep nav-item { - .nav-link, - .nav-child-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - position: relative; - - &:hover { - background: var(--sidebar-hover); - } - - &.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); - } - } - - .nav-child-link { - padding: var(--s) var(--s2); - color: var(--sidebar-text-secondary); - border-radius: 6px; - font-size: 14px; - - &:hover { - color: var(--sidebar-text); - } - } -} - -::ng-deep nav-row { - display: contents; - - .nav-icon { - flex-shrink: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - color: var(--sidebar-text-secondary); - font-size: 18px; - line-height: 1; - } - - .nav-label { - flex: 1; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .nav-badge { - flex-shrink: 0; - padding: 2px 8px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.08); - color: var(--sidebar-text); - font-size: 11px; - font-weight: 500; - } - - .nav-chevron { - flex-shrink: 0; - color: var(--sidebar-text-secondary); - } - - .title { - font-weight: 500; - } -} +// Removed deep styling for nav-entry, nav-item and nav-row diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index b5255814d8..9e001adb76 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -24,7 +24,7 @@ import { WorkContextCommon, WorkContextType, } from '../../features/work-context/work-context.model'; -import { SideNavItemComponent } from '../side-nav/side-nav-item/side-nav-item.component'; +import { SideNavItemComponent } from './side-nav-item/side-nav-item.component'; import { ContextMenuComponent } from '../../ui/context-menu/context-menu.component'; import { NavEntryComponent } from './nav-entry/nav-entry.component'; import { NavMultiBtnComponent } from './nav-multi-btn/nav-multi-btn.component'; diff --git a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html index 23595ec915..ef31c8a451 100644 --- a/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html +++ b/src/app/core-ui/magic-side-nav/nav-entry/nav-entry.component.html @@ -1,41 +1,48 @@ } @default { @@ -100,12 +100,12 @@ class="nav-item" role="listitem" > - + > } } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index b48af2cd44..749725c189 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -24,7 +24,6 @@ import { WorkContextCommon, WorkContextType, } from '../../features/work-context/work-context.model'; -import { SideNavItemComponent } from './side-nav-item/side-nav-item.component'; import { ContextMenuComponent } from '../../ui/context-menu/context-menu.component'; import { SpNavItemComponent } from './nav-item/nav-item.component'; import { SpNavSectionComponent } from './nav-list/nav-list.component'; @@ -136,7 +135,6 @@ export interface NavConfig { MatMenuItem, MatMenuTrigger, TranslatePipe, - SideNavItemComponent, ContextMenuComponent, SpNavItemComponent, SpNavSectionComponent, diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.css b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.css new file mode 100644 index 0000000000..968566ce7f --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.css @@ -0,0 +1,43 @@ +:host { + display: contents; +} + +.nav-link, +.nav-child-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; +} + +.nav-link:hover, +.nav-child-link:hover { + background: var(--sidebar-hover); +} + +.nav-link.active, +.nav-child-link.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); +} + +.nav-child-link { + padding: var(--s) var(--s2); + color: var(--sidebar-text-secondary); + border-radius: 6px; + font-size: 14px; +} + +.nav-child-link:hover { + color: var(--sidebar-text); +} diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss new file mode 100644 index 0000000000..d15ea60d4f --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -0,0 +1,41 @@ +:host { + display: contents; +} + +.nav-link, +.nav-child-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; + + &:hover { + background: var(--sidebar-hover); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + } +} + +.nav-child-link { + padding: var(--s) var(--s2); + color: var(--sidebar-text-secondary); + border-radius: 6px; + font-size: 14px; + + &:hover { + color: var(--sidebar-text); + } +} diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts index 95ea7981fe..15fdddb63c 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts @@ -1,70 +1,16 @@ import { Component, computed, input, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; -import { MatIcon } from '@angular/material/icon'; -import { MatIconButton } from '@angular/material/button'; -import { MatTooltip } from '@angular/material/tooltip'; -import { TranslatePipe } from '@ngx-translate/core'; import { NavItem } from '../magic-side-nav'; import { SideNavItemComponent } from '../side-nav-item/side-nav-item.component'; import { NavRowComponent } from '../nav-row/nav-row.component'; @Component({ - selector: 'sp-nav-item', + selector: 'nav-item', standalone: true, - imports: [ - CommonModule, - RouterModule, - MatIcon, - MatIconButton, - MatTooltip, - TranslatePipe, - SideNavItemComponent, - NavRowComponent, - ], + imports: [CommonModule, RouterModule, SideNavItemComponent, NavRowComponent], templateUrl: './nav-item.component.html', - styles: [ - ` - :host { - display: contents; - } - .nav-link, - .nav-child-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - position: relative; - } - .nav-link:hover, - .nav-child-link:hover { - background: var(--sidebar-hover); - } - .nav-link.active, - .nav-child-link.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); - } - .nav-child-link { - padding: var(--s) var(--s2); - color: var(--sidebar-text-secondary); - border-radius: 6px; - font-size: 14px; - } - .nav-child-link:hover { - color: var(--sidebar-text); - } - `, - ], + styleUrl: './nav-item.component.scss', }) export class SpNavItemComponent { item = input.required(); diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css new file mode 100644 index 0000000000..e52e48a541 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css @@ -0,0 +1,76 @@ +:host { + display: contents; +} + +.g-multi-btn-wrapper { + display: flex; + align-items: center; + position: relative; +} + +.nav-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; +} + +.nav-link:hover { + background: var(--sidebar-hover); +} + +.nav-link.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); +} + +.nav-link.expanded .nav-chevron { + transform: rotate(180deg); +} + +.additional-btn { + flex-shrink: 0; + width: 32px; + height: 32px; + min-width: 32px; + padding: 4px; + margin-left: var(--s); + color: var(--sidebar-text-secondary); + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.additional-btn:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); +} + +.nav-children { + list-style: none; + padding: 0; + margin: 4px 0 0 0; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.nav-children.visible { + max-height: 500px; +} + +.nav-child-item { + margin-left: var(--s3); +} diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index a4505d0d25..1b5fc7f780 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -48,13 +48,13 @@ class="nav-child-item" role="listitem" > - + > } diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss new file mode 100644 index 0000000000..0e5c07a218 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss @@ -0,0 +1,76 @@ +:host { + display: contents; +} + +.g-multi-btn-wrapper { + display: flex; + align-items: center; + position: relative; +} + +.nav-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; + + &:hover { + background: var(--sidebar-hover); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + } + + &.expanded .nav-chevron { + transform: rotate(180deg); + } +} + +.additional-btn { + flex-shrink: 0; + width: 32px; + height: 32px; + min-width: 32px; + padding: 4px; + margin-left: var(--s); + color: var(--sidebar-text-secondary); + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } +} + +.nav-children { + list-style: none; + padding: 0; + margin: 4px 0 0 0; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + + &.visible { + max-height: 500px; + } +} + +.nav-child-item { + //margin-left: var(--s3); +} diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts index ee18658468..cb36733890 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts @@ -9,7 +9,7 @@ import { NavRowComponent } from '../nav-row/nav-row.component'; import { SpNavItemComponent } from '../nav-item/nav-item.component'; @Component({ - selector: 'sp-nav-section', + selector: 'nav-list', standalone: true, imports: [ CommonModule, @@ -21,81 +21,7 @@ import { SpNavItemComponent } from '../nav-item/nav-item.component'; SpNavItemComponent, ], templateUrl: './nav-list.component.html', - styles: [ - ` - :host { - display: contents; - } - .g-multi-btn-wrapper { - display: flex; - align-items: center; - position: relative; - } - .nav-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - position: relative; - } - .nav-link:hover { - background: var(--sidebar-hover); - } - .nav-link.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); - } - .nav-link.expanded .nav-chevron { - transform: rotate(180deg); - } - .additional-btn { - flex-shrink: 0; - width: 32px; - height: 32px; - min-width: 32px; - padding: 4px; - margin-left: var(--s); - color: var(--sidebar-text-secondary); - background: transparent; - border: none; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - } - .additional-btn:hover { - background-color: var(--sidebar-hover); - color: var(--sidebar-text); - } - .additional-btn mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } - .nav-children { - list-style: none; - padding: 0; - margin: 4px 0 0 0; - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease; - } - .nav-children.visible { - max-height: 500px; - } - .nav-child-item { - /*margin-left: var(--s3);*/ - } - `, - ], + styleUrl: './nav-list.component.scss', }) export class SpNavSectionComponent { item = input.required(); diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.css b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.css new file mode 100644 index 0000000000..71a390d7c6 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.css @@ -0,0 +1,39 @@ +:host { + display: contents; +} + +.nav-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--sidebar-text-secondary); + font-size: 18px; + line-height: 1; +} + +.nav-label { + flex: 1; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nav-badge { + flex-shrink: 0; + padding: 2px 8px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.08); + color: var(--sidebar-text); + font-size: 11px; + font-weight: 500; +} + +.nav-chevron { + flex-shrink: 0; + color: var(--sidebar-text-secondary); +} diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.scss b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.scss new file mode 100644 index 0000000000..71a390d7c6 --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.scss @@ -0,0 +1,39 @@ +:host { + display: contents; +} + +.nav-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--sidebar-text-secondary); + font-size: 18px; + line-height: 1; +} + +.nav-label { + flex: 1; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nav-badge { + flex-shrink: 0; + padding: 2px 8px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.08); + color: var(--sidebar-text); + font-size: 11px; + font-weight: 500; +} + +.nav-chevron { + flex-shrink: 0; + color: var(--sidebar-text-secondary); +} diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts index d093d31adf..e730b7cc0d 100644 --- a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts @@ -8,45 +8,7 @@ import { TranslatePipe } from '@ngx-translate/core'; standalone: true, imports: [CommonModule, MatIcon, TranslatePipe], templateUrl: './nav-row.component.html', - styles: [ - ` - :host { - display: contents; - } - .nav-icon { - flex-shrink: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - color: var(--sidebar-text-secondary); - font-size: 18px; - line-height: 1; - } - .nav-label { - flex: 1; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .nav-badge { - flex-shrink: 0; - padding: 2px 8px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.08); - color: var(--sidebar-text); - font-size: 11px; - font-weight: 500; - } - .nav-chevron { - flex-shrink: 0; - color: var(--sidebar-text-secondary); - } - `, - ], + styleUrl: './nav-row.component.scss', }) export class NavRowComponent { @Input() icon?: string; diff --git a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.html b/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.html index 2b3cf0b946..b6e1e45c73 100644 --- a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.html @@ -3,11 +3,6 @@ *cdkDragPlaceholder >
-
- + + @if (showMoreButton()) { + + } + + + + + + +} @else { + + @if (svgIcon()) { + + } @else if (icon()) { + {{ icon() }} + } + + @if (showLabels() && label()) { + {{ label() | translate }} + } + + @if (showLabels() && badge() !== undefined && badge() !== null && badge() !== '') { + {{ badge() }} + } + + @if (showChevron()) { + + + + } +} diff --git a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss similarity index 100% rename from src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.scss rename to src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss diff --git a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.spec.ts b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.spec.ts similarity index 100% rename from src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.spec.ts rename to src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.spec.ts diff --git a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts similarity index 61% rename from src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.ts rename to src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts index 4f5986a20b..2c287c0f03 100644 --- a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts @@ -24,9 +24,10 @@ import { MatMenuItem } from '@angular/material/menu'; import { toSignal } from '@angular/core/rxjs-interop'; import { selectAllDoneIds } from '../../../features/tasks/store/task.selectors'; import { Store } from '@ngrx/store'; +import { TranslatePipe } from '@ngx-translate/core'; @Component({ - selector: 'side-nav-item', + selector: 'nav-item-inner', imports: [ RouterLink, RouterModule, @@ -36,48 +37,65 @@ import { Store } from '@ngrx/store'; MatIconButton, MatIcon, MatMenuItem, + TranslatePipe, ], - templateUrl: './side-nav-item.component.html', - styleUrl: './side-nav-item.component.scss', + templateUrl: './nav-item-inner.component.html', + styleUrl: './nav-item-inner.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'g-multi-btn-wrapper' }, standalone: true, }) -export class SideNavItemComponent { +export class NavItemInnerComponent { private readonly _store = inject(Store); - workContext = input.required(); - type = input.required(); - defaultIcon = input.required(); - activeWorkContextId = input.required(); + // Mode selection + mode = input<'work' | 'row'>('work'); + + // Work context inputs + workContext = input(null); + type = input(null); + defaultIcon = input('folder_special'); + activeWorkContextId = input(''); + // Variant styling to integrate into magic-side-nav without deep selectors variant = input<'default' | 'nav'>('default'); compact = input(false); showMoreButton = input(true); + // Presentational row inputs + label = input(undefined); + icon = input(undefined); + svgIcon = input(undefined); + badge = input(undefined); + showLabels = input(true); + showChevron = input(false); + allUndoneTaskIds = toSignal(this._store.select(selectAllDoneIds), { initialValue: [] }); nrOfOpenTasks = computed(() => { - // const allUndoneTasks + const wc = this.workContext(); + if (!wc) return 0; const allUndoneTaskIds = this.allUndoneTaskIds(); - return this.workContext().taskIds.filter((tid) => !allUndoneTaskIds.includes(tid)) - .length; + return wc.taskIds.filter((tid) => !allUndoneTaskIds.includes(tid)).length; }); - readonly routeBtn = viewChild.required('routeBtn', { read: ElementRef }); + readonly routeBtn = viewChild('routeBtn', { read: ElementRef }); @HostBinding('class.hasTasks') get workContextHasTasks(): boolean { - return this.workContext().taskIds.length > 0; + const wc = this.workContext(); + return !!wc && wc.taskIds.length > 0; } @HostBinding('class.isActiveContext') get isActiveContext(): boolean { - return this.workContext().id === this.activeWorkContextId(); + const wc = this.workContext(); + return !!wc && wc.id === this.activeWorkContextId(); } @HostBinding('class.isHidden') get isHidden(): boolean { - return !!(this.workContext() as Project)?.isHiddenFromMenu; + const wc = this.workContext(); + return !!(wc as Project | null)?.isHiddenFromMenu; } @HostBinding('class.variant-nav') @@ -91,6 +109,9 @@ export class SideNavItemComponent { } focus(): void { - this.routeBtn().nativeElement.focus(); + const btn = this.routeBtn(); + if (btn) { + btn.nativeElement.focus(); + } } } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index 37f97c1993..f01cd9719b 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -1,6 +1,6 @@ @switch (item().type) { @case ('workContext') { - + > } @case ('route') { @@ -19,13 +19,14 @@ [attr.title]="shouldShowTooltip() ? item().label : null" (click)="onItemClick()" > - + > } @@ -38,13 +39,14 @@ [attr.title]="shouldShowTooltip() ? item().label : null" (click)="onItemClick()" > - + > } @@ -54,13 +56,14 @@ [attr.title]="shouldShowTooltip() ? item().label : null" (click)="onItemClick()" > - + > } @@ -73,14 +76,15 @@ [attr.aria-controls]="'group-' + item().id" (click)="onItemClick()" > - + > } } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts index 15fdddb63c..1cb6f9bfbf 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts @@ -2,15 +2,14 @@ import { Component, computed, input, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { NavItem } from '../magic-side-nav'; -import { SideNavItemComponent } from '../side-nav-item/side-nav-item.component'; -import { NavRowComponent } from '../nav-row/nav-row.component'; +import { NavItemInnerComponent } from '../nav-item-inner/nav-item-inner.component'; @Component({ selector: 'nav-item', standalone: true, - imports: [CommonModule, RouterModule, SideNavItemComponent, NavRowComponent], + imports: [CommonModule, RouterModule, NavItemInnerComponent], templateUrl: './nav-item.component.html', - styleUrl: './nav-item.component.scss', + styleUrl: './nav-item.component.css', }) export class SpNavItemComponent { item = input.required(); diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index 1b5fc7f780..aaa56c2092 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -8,7 +8,8 @@ [attr.aria-expanded]="isExpanded()" [attr.aria-controls]="'group-' + item().id" > - + > @if (showLabels()) { diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts index cb36733890..9534b696d3 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts @@ -5,7 +5,7 @@ import { MatIconButton } from '@angular/material/button'; import { MatTooltip } from '@angular/material/tooltip'; import { TranslatePipe } from '@ngx-translate/core'; import { NavGroupItem, NavItem } from '../magic-side-nav'; -import { NavRowComponent } from '../nav-row/nav-row.component'; +import { NavItemInnerComponent } from '../nav-item-inner/nav-item-inner.component'; import { SpNavItemComponent } from '../nav-item/nav-item.component'; @Component({ @@ -17,11 +17,11 @@ import { SpNavItemComponent } from '../nav-item/nav-item.component'; MatIconButton, MatTooltip, TranslatePipe, - NavRowComponent, + NavItemInnerComponent, SpNavItemComponent, ], templateUrl: './nav-list.component.html', - styleUrl: './nav-list.component.scss', + styleUrl: './nav-list.component.css', }) export class SpNavSectionComponent { item = input.required(); diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.css b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.css deleted file mode 100644 index 71a390d7c6..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.css +++ /dev/null @@ -1,39 +0,0 @@ -:host { - display: contents; -} - -.nav-icon { - flex-shrink: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - color: var(--sidebar-text-secondary); - font-size: 18px; - line-height: 1; -} - -.nav-label { - flex: 1; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.nav-badge { - flex-shrink: 0; - padding: 2px 8px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.08); - color: var(--sidebar-text); - font-size: 11px; - font-weight: 500; -} - -.nav-chevron { - flex-shrink: 0; - color: var(--sidebar-text-secondary); -} diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.html b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.html deleted file mode 100644 index 88f4ddf7eb..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.html +++ /dev/null @@ -1,34 +0,0 @@ -@if (svgIcon) { - -} @else if (icon) { - {{ icon }} -} - -@if (showLabels && label) { - {{ label | translate }} -} - -@if (showLabels && badge) { - {{ badge }} -} - -@if (showChevron) { - - - -} diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.scss b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.scss deleted file mode 100644 index 71a390d7c6..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.scss +++ /dev/null @@ -1,39 +0,0 @@ -:host { - display: contents; -} - -.nav-icon { - flex-shrink: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - color: var(--sidebar-text-secondary); - font-size: 18px; - line-height: 1; -} - -.nav-label { - flex: 1; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.nav-badge { - flex-shrink: 0; - padding: 2px 8px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.08); - color: var(--sidebar-text); - font-size: 11px; - font-weight: 500; -} - -.nav-chevron { - flex-shrink: 0; - color: var(--sidebar-text-secondary); -} diff --git a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts b/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts deleted file mode 100644 index e730b7cc0d..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-row/nav-row.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatIcon } from '@angular/material/icon'; -import { TranslatePipe } from '@ngx-translate/core'; - -@Component({ - selector: 'nav-row', - standalone: true, - imports: [CommonModule, MatIcon, TranslatePipe], - templateUrl: './nav-row.component.html', - styleUrl: './nav-row.component.scss', -}) -export class NavRowComponent { - @Input() icon?: string; - @Input() svgIcon?: string; - @Input() label?: string; - @Input() badge?: string | number; - @Input() showLabels = true; - @Input() showChevron = false; - @Input() labelClass?: string; -} diff --git a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.html b/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.html deleted file mode 100644 index b6e1e45c73..0000000000 --- a/src/app/core-ui/magic-side-nav/side-nav-item/side-nav-item.component.html +++ /dev/null @@ -1,40 +0,0 @@ -
- - - - - -@if (showMoreButton()) { - -} - - - - - diff --git a/src/app/core-ui/side-nav/side-nav.component.html b/src/app/core-ui/side-nav/side-nav.component.html index 8e8052f1a9..a3a79fdea4 100644 --- a/src/app/core-ui/side-nav/side-nav.component.html +++ b/src/app/core-ui/side-nav/side-nav.component.html @@ -1,22 +1,22 @@
@if (workContextService.mainWorkContext$ | async; as mainContext) { - + > } @let inboxContext = workContextService.inboxWorkContext$ | async; @if (inboxContext) { - + > } @@ -52,10 +54,6 @@ {{ label() | translate }} } - @if (showLabels() && badge() !== undefined && badge() !== null && badge() !== '') { - {{ badge() }} - } - @if (showChevron()) { (undefined); icon = input(undefined); svgIcon = input(undefined); - badge = input(undefined); showLabels = input(true); showChevron = input(false); diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index f01cd9719b..8f20bd0e53 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -24,7 +24,6 @@ [icon]="item().icon" [svgIcon]="item().svgIcon" [label]="item().label" - [badge]="item().badge" [showLabels]="showLabels()" > @@ -44,7 +43,6 @@ [icon]="item().icon" [svgIcon]="item().svgIcon" [label]="item().label" - [badge]="item().badge" [showLabels]="showLabels()" > @@ -61,7 +59,6 @@ [icon]="item().icon" [svgIcon]="item().svgIcon" [label]="item().label" - [badge]="item().badge" [showLabels]="showLabels()" > @@ -81,7 +78,6 @@ [icon]="item().icon" [svgIcon]="item().svgIcon" [label]="item().label" - [badge]="item().badge" [showLabels]="showLabels()" [showChevron]="hasChildren() && showLabels()" > diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index aaa56c2092..8587eacf0b 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -13,7 +13,6 @@ [icon]="item().icon" [svgIcon]="item().svgIcon" [label]="item().label" - [badge]="item().badge" [showLabels]="showLabels()" [showChevron]="item().children && item().children.length > 0 && showLabels()" labelClass="title" diff --git a/src/app/core-ui/side-nav/side-nav-config.service.ts b/src/app/core-ui/side-nav/side-nav-config.service.ts index 212499ba5f..d25f32bce0 100644 --- a/src/app/core-ui/side-nav/side-nav-config.service.ts +++ b/src/app/core-ui/side-nav/side-nav-config.service.ts @@ -261,9 +261,6 @@ export class SideNavConfigService { workContext: mainContext, workContextType: WorkContextType.TAG, defaultIcon: 'today', - badge: mainContext.taskIds?.length - ? mainContext.taskIds.length.toString() - : undefined, }); } @@ -277,9 +274,6 @@ export class SideNavConfigService { workContext: inboxContext, workContextType: WorkContextType.PROJECT, defaultIcon: 'inbox', - badge: inboxContext.taskIds?.length - ? inboxContext.taskIds.length.toString() - : undefined, }); } @@ -305,7 +299,6 @@ export class SideNavConfigService { workContext: project, workContextType: WorkContextType.PROJECT, defaultIcon: 'folder_special', - badge: project.taskIds?.length ? project.taskIds.length.toString() : undefined, })); } @@ -328,7 +321,6 @@ export class SideNavConfigService { workContext: tag, workContextType: WorkContextType.TAG, defaultIcon: 'label', - badge: tag.taskIds?.length ? tag.taskIds.length.toString() : undefined, })); } From c7b88327a3725fc4800fa9f7aa771eb9632c8c7e Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 11:08:48 +0200 Subject: [PATCH 14/61] feat(sideNav): animate --- .../magic-side-nav/magic-side-nav.html | 1 + .../magic-side-nav/magic-side-nav.scss | 42 ++++++++++++++---- .../core-ui/magic-side-nav/magic-side-nav.ts | 44 ++++++++++++++++++- .../nav-item-inner.component.html | 18 ++++++-- .../nav-item-inner.component.scss | 28 ++++++++++++ .../nav-item/nav-item.component.html | 1 + 6 files changed, 122 insertions(+), 12 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.html b/src/app/core-ui/magic-side-nav/magic-side-nav.html index e199330a9e..4daac9b19d 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.html @@ -41,6 +41,7 @@ [class.collapsed]="!isExpanded() && !isMobile()" [class.mobile]="isMobile()" [class.mobile-visible]="isMobile() && showMobileMenu()" + [class.animate]="animateWidth()" [class.theme-dark]="config.theme === 'dark'" [class.position-right]="config.position === 'right'" [class.resizing]="isResizing()" diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.scss index 515e47650c..d1eb490a21 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.scss @@ -2,10 +2,17 @@ $sidebar-width-expanded: var(--side-nav-width, 260px); $sidebar-width-collapsed: 72px; $mobile-breakpoint: 768px; -$transition-speed: var(--transition-standard, 0.3s); +// Use duration/timing variables directly; --transition-standard is a shorthand +$transition-duration: var(--transition-duration-m, 0.3s); +$transition-timing: var(--ani-standard-timing, cubic-bezier(0.4, 0, 0.2, 1)); $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); :host { + // Ensure the component itself participates in layout and animates width + display: block; + flex: 0 0 auto; + transition: none; + will-change: width; // Use app's CSS variables for consistent theming --sidebar-bg: var(--palette-background-card); --sidebar-border: var(--divider-color); @@ -27,6 +34,16 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); } } +// Enable host width transition only when toggling collapsed/expanded +:host(.animate) { + transition: width $transition-duration $transition-timing; +} + +// Disable host transition during resize drags +:host(.resizing) { + transition: none !important; +} + // Mobile menu toggle button .mobile-menu-toggle { display: none; @@ -39,7 +56,7 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); border-radius: 8px; padding: 8px; cursor: pointer; - transition: all $transition-speed ease; + transition: var(--transition-standard); &.visible { display: flex; @@ -67,7 +84,7 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); background: $overlay-bg; z-index: 998; opacity: 0; - transition: opacity $transition-speed ease; + transition: opacity $transition-duration $transition-timing; &.visible { display: block; @@ -84,15 +101,26 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); width: $sidebar-width-expanded; background: var(--sidebar-bg); border-right: 1px solid var(--sidebar-border); - transition: all $transition-speed cubic-bezier(0.4, 0, 0.2, 1); + // Default: don't animate width + transition: none; z-index: 999; overflow-y: auto; overflow-x: hidden; + &.animate { + transition: width $transition-duration $transition-timing; + } + &.resizing { transition: none !important; } + // When resizing, also suppress label/text transitions inside entries + &.resizing .nav-label, + &.resizing .text { + transition: none !important; + } + // Milkglass effect when background image is present // This will be applied when a CSS class is added to indicate background image presence :host-context(.hasBgImage) { @@ -149,10 +177,8 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); // Collapsed state (desktop) &.collapsed { - width: $sidebar-width-collapsed !important; + // width controlled via inline style for smooth transitions - .nav-label, - .nav-chevron, .nav-children { display: none; } @@ -290,7 +316,7 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); border-radius: 6px; padding: 6px; cursor: pointer; - transition: all $transition-speed ease; + transition: var(--transition-standard); color: var(--sidebar-text-secondary); &.visible { diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index 19468f1096..be0d634540 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -2,11 +2,13 @@ import { Component, Input, HostListener, + HostBinding, signal, computed, effect, output, OnInit, + OnDestroy, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; @@ -141,7 +143,7 @@ export interface NavConfig { templateUrl: './magic-side-nav.html', styleUrl: './magic-side-nav.scss', }) -export class MagicSideNavComponent implements OnInit { +export class MagicSideNavComponent implements OnInit, OnDestroy { @Input() config: NavConfig = { items: [], expandedByDefault: true, @@ -170,6 +172,9 @@ export class MagicSideNavComponent implements OnInit { expandedGroups = signal(new Set()); // Visual feedback state for resizing thresholds resizeState = signal<'normal' | 'collapse' | 'expand'>('normal'); + // Animate only for collapsed/expanded toggle + animateWidth = signal(false); + private _animateTimeoutId: number | null = null; // Resize functionality currentWidth = signal(260); @@ -188,6 +193,22 @@ export class MagicSideNavComponent implements OnInit { return this.currentWidth(); }); + // Animate the host width so layout adjusts smoothly + @HostBinding('style.width.px') + get hostWidth(): number { + return this.sidebarWidth(); + } + + @HostBinding('class.animate') + get hostAnimate(): boolean { + return this.animateWidth(); + } + + @HostBinding('class.resizing') + get hostResizing(): boolean { + return this.isResizing(); + } + // Keep stable references for event listeners to ensure proper cleanup private readonly onDrag: (event: MouseEvent) => void = (event: MouseEvent) => this.handleDrag(event); @@ -200,6 +221,13 @@ export class MagicSideNavComponent implements OnInit { }); } + ngOnDestroy(): void { + if (this._animateTimeoutId != null) { + window.clearTimeout(this._animateTimeoutId); + this._animateTimeoutId = null; + } + } + ngOnInit(): void { // Load saved expansion state or default to config value const savedExpanded = localStorage.getItem('nav-sidebar-expanded'); @@ -264,6 +292,7 @@ export class MagicSideNavComponent implements OnInit { if (this.isMobile()) { this.showMobileMenu.update((show) => !show); } else { + this._enableWidthAnimation(); const newExpanded = !this.isExpanded(); this.isExpanded.set(newExpanded); // Save expansion state to localStorage @@ -426,5 +455,18 @@ export class MagicSideNavComponent implements OnInit { } } + private _enableWidthAnimation(): void { + if (this._animateTimeoutId != null) { + window.clearTimeout(this._animateTimeoutId); + this._animateTimeoutId = null; + } + this.animateWidth.set(true); + // Slightly longer than --transition-duration-m (225ms) to ensure cleanup + this._animateTimeoutId = window.setTimeout(() => { + this.animateWidth.set(false); + this._animateTimeoutId = null; + }, 300); + } + // Inline width binding via template replaces imperative DOM style updates } diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html index 76a7abc3d7..c28cb00185 100644 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html @@ -14,7 +14,13 @@ {{ workContext()!.icon || defaultIcon() }} {{ nrOfOpenTasks() }} - {{ workContext()!.title }} + + {{ workContext()!.title }} + @if (showMoreButton()) { @@ -50,8 +56,14 @@ {{ icon() }} } - @if (showLabels() && label()) { - {{ label() | translate }} + @if (label()) { + + {{ label() | translate }} + } @if (showChevron()) { diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss index eb8e13f17e..c8ded48e80 100644 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss @@ -15,6 +15,14 @@ .text { overflow: hidden; text-overflow: ellipsis; + // smooth collapse/expand for title + display: inline-block; + white-space: nowrap; + max-width: 100%; + transition: + opacity var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + max-width var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + margin var(--transition-duration-m, 0.25s) var(--ani-standard-timing); } .badge { @@ -110,6 +118,26 @@ body.isDarkTheme :host.isActiveContext button.isActiveRoute { } } +// Smooth label show/hide for presentational rows +.nav-label { + display: inline-block; + overflow: hidden; + white-space: nowrap; + max-width: 100%; + transition: + opacity var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + max-width var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + margin var(--transition-duration-m, 0.25s) var(--ani-standard-timing); +} + +.nav-label.label-hidden, +.text.label-hidden { + opacity: 0; + max-width: 0; + margin-left: 0 !important; + pointer-events: none; +} + .drag-handle { /* Firefox 1.5-26 */ position: relative; diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index 8f20bd0e53..b70e094fd7 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -7,6 +7,7 @@ [activeWorkContextId]="activeWorkContextId() || ''" [variant]="'nav'" [compact]="isChild()" + [showLabels]="showLabels()" [showMoreButton]="showLabels()" > } From 8a06f9239743e91e94ce5c9ec95cd67aaae1ee8c Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 12:43:17 +0200 Subject: [PATCH 15/61] feat(sideNav): style adjustments --- src/app/core-ui/magic-side-nav/magic-side-nav.scss | 5 +++-- src/app/core-ui/magic-side-nav/magic-side-nav.ts | 10 ++++++---- .../magic-side-nav/nav-item/nav-item.component.scss | 11 ----------- .../magic-side-nav/nav-list/nav-list.component.css | 4 ---- .../magic-side-nav/nav-list/nav-list.component.scss | 4 ---- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.scss index d1eb490a21..7236d69c8e 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.scss @@ -1,6 +1,5 @@ // Use app's CSS variables for width and spacing $sidebar-width-expanded: var(--side-nav-width, 260px); -$sidebar-width-collapsed: 72px; $mobile-breakpoint: 768px; // Use duration/timing variables directly; --transition-standard is a shorthand $transition-duration: var(--transition-duration-m, 0.3s); @@ -334,7 +333,9 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); // Navigation list .nav-list { list-style: none; - padding: 60px var(--s) var(--s2); + //padding: 60px var(--s) var(--s2); + padding: 0; + padding-top: 60px; margin: 0; } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index be0d634540..cc2b9bfff6 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -122,6 +122,8 @@ export interface NavConfig { expandThreshold?: number; } +const COLLAPSED_WIDTH = 64; + @Component({ selector: 'magic-side-nav', standalone: true, @@ -189,7 +191,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // Computed values sidebarWidth = computed(() => { if (this.isMobile()) return 260; - if (!this.isExpanded()) return 72; + if (!this.isExpanded()) return COLLAPSED_WIDTH; return this.currentWidth(); }); @@ -356,7 +358,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { this.isResizing.set(true); this.startX.set(event.clientX); - this.startWidth.set(this.isExpanded() ? this.currentWidth() : 72); + this.startWidth.set(this.isExpanded() ? this.currentWidth() : COLLAPSED_WIDTH); document.addEventListener('mousemove', this.onDrag); document.addEventListener('mouseup', this.onDragEnd); @@ -385,7 +387,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { this.expandedChange.emit(this.isExpanded()); // Reset starting point for continued dragging from collapsed state - this.startWidth.set(72); + this.startWidth.set(COLLAPSED_WIDTH); this.startX.set(event.clientX); // Visual feedback @@ -409,7 +411,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { this.currentWidth.set(newWidth); } else { // Currently collapsed - check for expand threshold - const collapsedWidth = 72; + const collapsedWidth = COLLAPSED_WIDTH; const draggedWidth = collapsedWidth + deltaX; if (draggedWidth > this.expandThreshold()) { diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index d15ea60d4f..dd7373310f 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -28,14 +28,3 @@ color: var(--sidebar-active-text); } } - -.nav-child-link { - padding: var(--s) var(--s2); - color: var(--sidebar-text-secondary); - border-radius: 6px; - font-size: 14px; - - &:hover { - color: var(--sidebar-text); - } -} diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css index e52e48a541..381195bfd3 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css @@ -70,7 +70,3 @@ .nav-children.visible { max-height: 500px; } - -.nav-child-item { - margin-left: var(--s3); -} diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss index 0e5c07a218..a3c0297ba5 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss @@ -70,7 +70,3 @@ max-height: 500px; } } - -.nav-child-item { - //margin-left: var(--s3); -} From 1422a4ba36c6949db8fbecdd33c7a878c31fc7dc Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 15:54:39 +0200 Subject: [PATCH 16/61] feat(sideNav): make overlay nav half way work --- src/app/app.component.html | 2 ++ src/app/app.component.scss | 35 +++++++++++-------- src/app/app.component.ts | 11 ++++-- .../magic-side-nav/magic-side-nav.scss | 4 +-- .../core-ui/magic-side-nav/magic-side-nav.ts | 25 ++++++++++++- .../main-header/main-header.component.html | 15 ++++---- 6 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 1c1b7be357..96249efd4c 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -33,6 +33,8 @@ [config]="getMagicSideNavConfig()" [expandedGroupIds]="getExpandedGroupIds()" [activeWorkContextId]="getActiveWorkContextId()" + [mobileVisible]="layoutService.isShowSideNav()" + (mobileVisibleChange)="onMobileNavVisibleChange($event)" (itemClick)="sideNavConfigService.onNavItemClick($event)" (expandedChange)="onSideNavExpandedChange($event)" > diff --git a/src/app/app.component.scss b/src/app/app.component.scss index dce70c9490..a5a9b6f217 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -23,23 +23,28 @@ } // Override magic-side-nav styles for flex layout integration -magic-side-nav { - ::ng-deep .nav-sidebar { - position: relative !important; - top: auto !important; - left: auto !important; - bottom: auto !important; - height: 100vh !important; - min-height: -webkit-fill-available !important; - z-index: auto !important; - } +// Use desktop layout overrides only on larger screens +@media (min-width: 768px) { + magic-side-nav { + ::ng-deep .nav-sidebar { + position: relative !important; + top: auto !important; + left: auto !important; + bottom: auto !important; + height: 100vh !important; + min-height: -webkit-fill-available !important; + z-index: auto !important; + } - ::ng-deep .mobile-menu-toggle { - display: none !important; // Use app header toggle instead - } + // Header provides a toggle on small screens; keep native toggle hidden on desktop + ::ng-deep .mobile-menu-toggle { + display: none !important; + } - ::ng-deep .nav-overlay { - display: none !important; // Not needed in flex layout + // Overlay not needed on desktop + ::ng-deep .nav-overlay { + display: none !important; + } } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 38484992dd..bafbf777d3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -451,8 +451,8 @@ export class AppComponent implements OnDestroy, AfterViewInit { return { ...baseConfig, expandedByDefault: this.layoutService.isShowSideNav(), - // Integrate with layout service for mobile detection - mobileBreakpoint: 0, // Disable mobile mode, use existing app mobile handling + // Enable magic-side-nav mobile overlay for small screens + mobileBreakpoint: 768, }; } @@ -463,6 +463,13 @@ export class AppComponent implements OnDestroy, AfterViewInit { } } + // Keep layout service in sync when mobile overlay closes via backdrop/item click + onMobileNavVisibleChange(isVisible: boolean): void { + if (!isVisible && this.layoutService.isShowSideNav()) { + this.layoutService.hideSideNav(); + } + } + getExpandedGroupIds(): Set { return this.sideNavConfigService.expandedGroupIds(); } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.scss index 7236d69c8e..446ed172f9 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.scss @@ -81,7 +81,7 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); right: 0; bottom: 0; background: $overlay-bg; - z-index: 998; + z-index: 1001; opacity: 0; transition: opacity $transition-duration $transition-timing; @@ -102,7 +102,7 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); border-right: 1px solid var(--sidebar-border); // Default: don't animate width transition: none; - z-index: 999; + z-index: 1002; overflow-y: auto; overflow-x: hidden; diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index cc2b9bfff6..f5ac522605 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -7,6 +7,7 @@ import { computed, effect, output, + input, OnInit, OnDestroy, } from '@angular/core'; @@ -167,6 +168,9 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { itemClick = output(); expandedChange = output(); widthChange = output(); + // Externally controlled mobile overlay visibility + mobileVisible = input(null); + mobileVisibleChange = output(); isExpanded = signal(true); isMobile = signal(false); @@ -198,6 +202,10 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // Animate the host width so layout adjusts smoothly @HostBinding('style.width.px') get hostWidth(): number { + // On mobile, don't reserve layout space; use fixed overlay instead + if (this.isMobile()) { + return 0; + } return this.sidebarWidth(); } @@ -221,6 +229,21 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { effect(() => { this.widthChange.emit(this.sidebarWidth()); }); + + // Sync external mobile visibility input → internal state + effect(() => { + const desired = this.mobileVisible(); + if (desired !== null && this.isMobile()) { + this.showMobileMenu.set(!!desired); + } + }); + + // Emit mobile visible changes to parent while in mobile mode + effect(() => { + if (this.isMobile()) { + this.mobileVisibleChange.emit(this.showMobileMenu()); + } + }); } ngOnDestroy(): void { @@ -270,7 +293,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { const nav = document.querySelector('.nav-sidebar'); const toggle = document.querySelector('.mobile-menu-toggle'); - if (nav && !nav.contains(target) && toggle && !toggle.contains(target)) { + if (nav && !nav.contains(target) && (!toggle || !toggle.contains(target as Node))) { this.showMobileMenu.set(false); } } diff --git a/src/app/core-ui/main-header/main-header.component.html b/src/app/core-ui/main-header/main-header.component.html index d0b5c46f86..e9ff43618b 100644 --- a/src/app/core-ui/main-header/main-header.component.html +++ b/src/app/core-ui/main-header/main-header.component.html @@ -1,12 +1,13 @@
@if (!isNavAlwaysVisible()) { - - - - - - - + } Date: Mon, 8 Sep 2025 18:54:53 +0200 Subject: [PATCH 17/61] refactor(sideNav): improve --- src/app/app.component.ts | 2 +- .../magic-side-nav/magic-side-nav.html | 16 +- .../magic-side-nav/magic-side-nav.model.ts | 95 ++++++++ .../magic-side-nav/magic-side-nav.scss | 27 ++- .../core-ui/magic-side-nav/magic-side-nav.ts | 212 ++++-------------- .../nav-item-inner.component.spec.ts | 23 -- .../nav-item-inner.component.ts | 25 ++- .../nav-item/nav-item.component.css | 43 ---- .../nav-item/nav-item.component.ts | 4 +- .../nav-list/nav-list.component.css | 72 ------ .../nav-list/nav-list.component.html | 3 +- .../nav-list/nav-list.component.ts | 4 +- .../side-nav/side-nav-config.service.ts | 2 +- 13 files changed, 191 insertions(+), 337 deletions(-) create mode 100644 src/app/core-ui/magic-side-nav/magic-side-nav.model.ts delete mode 100644 src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.spec.ts delete mode 100644 src/app/core-ui/magic-side-nav/nav-item/nav-item.component.css delete mode 100644 src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css diff --git a/src/app/app.component.ts b/src/app/app.component.ts index bafbf777d3..51314f657a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -48,7 +48,6 @@ import { AddTaskBarComponent } from './features/tasks/add-task-bar/add-task-bar. import { Dir } from '@angular/cdk/bidi'; import { MagicSideNavComponent } from './core-ui/magic-side-nav/magic-side-nav'; import { SideNavConfigService } from './core-ui/side-nav/side-nav-config.service'; -import { NavConfig } from './core-ui/magic-side-nav/magic-side-nav'; import { MainHeaderComponent } from './core-ui/main-header/main-header.component'; import { BannerComponent } from './core/banner/banner/banner.component'; import { GlobalProgressBarComponent } from './core-ui/global-progress-bar/global-progress-bar.component'; @@ -83,6 +82,7 @@ import { ProjectService } from './features/project/project.service'; import { TagService } from './features/tag/tag.service'; import { ContextMenuComponent } from './ui/context-menu/context-menu.component'; import { WorkContextThemeCfg } from './features/work-context/work-context.model'; +import { NavConfig } from './core-ui/magic-side-nav/magic-side-nav.model'; interface BeforeInstallPromptEvent extends Event { prompt(): Promise; diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.html b/src/app/core-ui/magic-side-nav/magic-side-nav.html index 4daac9b19d..bbbf57f925 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.html @@ -42,8 +42,8 @@ [class.mobile]="isMobile()" [class.mobile-visible]="isMobile() && showMobileMenu()" [class.animate]="animateWidth()" - [class.theme-dark]="config.theme === 'dark'" - [class.position-right]="config.position === 'right'" + [class.theme-dark]="config().theme === 'dark'" + [class.position-right]="config().position === 'right'" [class.resizing]="isResizing()" [class.collapse-zone]="resizeState() === 'collapse'" [class.expand-zone]="resizeState() === 'expand'" @@ -77,7 +77,7 @@ class="nav-list" role="list" > - @for (item of config.items; track item.id) { + @for (item of config().items; track item.id) { @switch (item.type) { @case ('separator') { @@ -91,7 +91,7 @@ [item]="item" [showLabels]="isExpanded() || (isMobile() && showMobileMenu())" [isExpanded]="isGroupExpanded(item)" - [activeWorkContextId]="activeWorkContextId" + [activeWorkContextId]="activeWorkContextId()" (itemClick)="onItemClick($event)" > @@ -104,7 +104,7 @@ @@ -114,11 +114,11 @@ - @if (config.resizable && !isMobile()) { + @if (config().resizable && !isMobile()) {
void; +} + +export interface NavContextItem { + id: string; + label: string; + icon: string; + svgIcon?: string; + action?: () => void; +} + +export interface NavGroupItem extends NavBaseItem { + type: 'group'; + label: string; + icon: string; + children: NavItem[]; + additionalButtons?: NavAdditionalButton[]; + contextMenuItems?: NavContextItem[]; + action?: () => void; // optional external toggle logic +} + +export interface NavAdditionalButton { + id: string; + icon: string; + tooltip?: string; + action?: () => void; + hidden?: boolean; + contextMenu?: NavContextItem[]; +} +export interface NavConfig { + items: NavItem[]; + expandedByDefault?: boolean; + showLabels?: boolean; + mobileBreakpoint?: number; + position?: 'left' | 'right'; + theme?: 'light' | 'dark'; + resizable?: boolean; + minWidth?: number; + maxWidth?: number; + defaultWidth?: number; + collapseThreshold?: number; + expandThreshold?: number; +} diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.scss index 446ed172f9..31dac0c8a9 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.scss @@ -4,7 +4,7 @@ $mobile-breakpoint: 768px; // Use duration/timing variables directly; --transition-standard is a shorthand $transition-duration: var(--transition-duration-m, 0.3s); $transition-timing: var(--ani-standard-timing, cubic-bezier(0.4, 0, 0.2, 1)); -$overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); +$overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); :host { // Ensure the component itself participates in layout and animates width @@ -12,6 +12,8 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); flex: 0 0 auto; transition: none; will-change: width; + position: relative; + z-index: var(--z-side-nav); // Use app's CSS variables for consistent theming --sidebar-bg: var(--palette-background-card); --sidebar-border: var(--divider-color); @@ -72,6 +74,14 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); } } +// Dark theme milkglass adjustments +:host-context(.isDarkTheme.hasBgImage) :host { + background: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px) saturate(180%); + border-right: 1px solid rgba(255, 255, 255, 0.08); + -webkit-backdrop-filter: blur(20px) saturate(180%); // Safari support +} + // Mobile overlay .nav-overlay { display: none; @@ -81,7 +91,7 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); right: 0; bottom: 0; background: $overlay-bg; - z-index: 1001; + z-index: var(--z-backdrop, 1001); opacity: 0; transition: opacity $transition-duration $transition-timing; @@ -106,6 +116,8 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); overflow-y: auto; overflow-x: hidden; + // Slight elevation and clear background for overlay/mobile mode is defined in the mobile block below + &.animate { transition: width $transition-duration $transition-timing; } @@ -129,14 +141,6 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); -webkit-backdrop-filter: blur(20px) saturate(180%); // Safari support } - // Dark theme milkglass adjustments - :host-context(.isDarkTheme.hasBgImage) { - background: rgba(0, 0, 0, 0.2); - backdrop-filter: blur(20px) saturate(180%); - border-right: 1px solid rgba(255, 255, 255, 0.08); - -webkit-backdrop-filter: blur(20px) saturate(180%); // Safari support - } - // Threshold zone visual feedback //&.collapse-zone { // border-left: 3px solid #ef4444; @@ -260,6 +264,9 @@ $overlay-bg: var(--c-dark-50, rgba(0, 0, 0, 0.5)); // Mobile specific &.mobile { + transition: transform $transition-duration $transition-timing; + will-change: transform; + box-shadow: 4px 0 16px rgba(0, 0, 0, 0.3); transform: translateX(-100%); width: $sidebar-width-expanded; diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index f5ac522605..8965d51593 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -1,153 +1,40 @@ import { Component, - Input, - HostListener, - HostBinding, - signal, computed, effect, - output, + HostListener, input, - OnInit, OnDestroy, + OnInit, + output, + signal, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; -import { MatIcon } from '@angular/material/icon'; -import { MatIconButton } from '@angular/material/button'; -import { MatTooltip } from '@angular/material/tooltip'; -import { - MatMenu, - MatMenuContent, - MatMenuItem, - MatMenuTrigger, -} from '@angular/material/menu'; -import { TranslatePipe } from '@ngx-translate/core'; -import { - WorkContextCommon, - WorkContextType, -} from '../../features/work-context/work-context.model'; -import { ContextMenuComponent } from '../../ui/context-menu/context-menu.component'; import { SpNavItemComponent } from './nav-item/nav-item.component'; import { SpNavSectionComponent } from './nav-list/nav-list.component'; - -export type NavItem = - | NavSeparatorItem - | NavWorkContextItem - | NavRouteItem - | NavHrefItem - | NavActionItem - | NavGroupItem; - -export interface NavBaseItem { - id: string; - label?: string; - icon?: string; - svgIcon?: string; -} - -export interface NavSeparatorItem extends NavBaseItem { - type: 'separator'; -} - -export interface NavWorkContextItem extends NavBaseItem { - type: 'workContext'; - label: string; - icon: string; - route: string; - workContext: WorkContextCommon; - workContextType: WorkContextType; - defaultIcon: string; -} - -export interface NavRouteItem extends NavBaseItem { - type: 'route'; - label: string; - icon: string; - route: string; -} - -export interface NavHrefItem extends NavBaseItem { - type: 'href'; - label: string; - icon: string; - href: string; -} - -export interface NavActionItem extends NavBaseItem { - type: 'action'; - label: string; - icon: string; - action: () => void; -} - -export interface NavContextItem { - id: string; - label: string; - icon: string; - svgIcon?: string; - action?: () => void; -} - -export interface NavGroupItem extends NavBaseItem { - type: 'group'; - label: string; - icon: string; - children: NavItem[]; - additionalButtons?: NavAdditionalButton[]; - contextMenuItems?: NavContextItem[]; - action?: () => void; // optional external toggle logic -} - -export interface NavAdditionalButton { - id: string; - icon: string; - tooltip?: string; - action?: () => void; - hidden?: boolean; - contextMenu?: NavContextItem[]; -} - -export interface NavConfig { - items: NavItem[]; - expandedByDefault?: boolean; - showLabels?: boolean; - mobileBreakpoint?: number; - position?: 'left' | 'right'; - theme?: 'light' | 'dark'; - resizable?: boolean; - minWidth?: number; - maxWidth?: number; - defaultWidth?: number; - collapseThreshold?: number; - expandThreshold?: number; -} +import { NavConfig, NavItem } from './magic-side-nav.model'; const COLLAPSED_WIDTH = 64; +const HOST_WIDTH = '[style.width.px]'; +const HOST_ANIMATE = '[class.animate]'; +const HOST_RESIZING = '[class.resizing]'; + @Component({ selector: 'magic-side-nav', standalone: true, - imports: [ - CommonModule, - RouterModule, - MatIcon, - MatIconButton, - MatTooltip, - MatMenu, - MatMenuContent, - MatMenuItem, - MatMenuTrigger, - TranslatePipe, - ContextMenuComponent, - SpNavItemComponent, - SpNavSectionComponent, - ], + imports: [CommonModule, RouterModule, SpNavItemComponent, SpNavSectionComponent], templateUrl: './magic-side-nav.html', styleUrl: './magic-side-nav.scss', + host: { + [HOST_WIDTH]: 'hostWidth()', + [HOST_ANIMATE]: 'hostAnimate()', + [HOST_RESIZING]: 'hostResizing()', + }, }) export class MagicSideNavComponent implements OnInit, OnDestroy { - @Input() config: NavConfig = { + config = input({ items: [], expandedByDefault: true, showLabels: true, @@ -160,10 +47,10 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { defaultWidth: 260, collapseThreshold: 150, expandThreshold: 180, - }; + }); - @Input() expandedGroupIds: Set = new Set(); - @Input() activeWorkContextId: string | null = null; + expandedGroupIds = input>(new Set()); + activeWorkContextId = input(null); itemClick = output(); expandedChange = output(); @@ -200,8 +87,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { }); // Animate the host width so layout adjusts smoothly - @HostBinding('style.width.px') - get hostWidth(): number { + hostWidth(): number { // On mobile, don't reserve layout space; use fixed overlay instead if (this.isMobile()) { return 0; @@ -209,20 +95,18 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { return this.sidebarWidth(); } - @HostBinding('class.animate') - get hostAnimate(): boolean { + hostAnimate(): boolean { return this.animateWidth(); } - @HostBinding('class.resizing') - get hostResizing(): boolean { + hostResizing(): boolean { return this.isResizing(); } // Keep stable references for event listeners to ensure proper cleanup - private readonly onDrag: (event: MouseEvent) => void = (event: MouseEvent) => - this.handleDrag(event); - private readonly onDragEnd: () => void = () => this.handleDragEnd(); + private readonly _onDrag: (event: MouseEvent) => void = (event: MouseEvent) => + this._handleDrag(event); + private readonly _onDragEnd: () => void = () => this._handleDragEnd(); constructor() { // Effect to emit width changes @@ -259,14 +143,14 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { const initialExpanded = savedExpanded !== null ? savedExpanded === 'true' - : (this.config.expandedByDefault ?? true); + : (this.config().expandedByDefault ?? true); this.isExpanded.set(initialExpanded); - this.currentWidth.set(this.config.defaultWidth ?? 260); - this.minWidth.set(this.config.minWidth ?? 200); - this.maxWidth.set(this.config.maxWidth ?? 400); - this.collapseThreshold.set(this.config.collapseThreshold ?? 150); - this.expandThreshold.set(this.config.expandThreshold ?? 180); + this.currentWidth.set(this.config().defaultWidth ?? 260); + this.minWidth.set(this.config().minWidth ?? 200); + this.maxWidth.set(this.config().maxWidth ?? 400); + this.collapseThreshold.set(this.config().collapseThreshold ?? 150); + this.expandThreshold.set(this.config().expandThreshold ?? 180); // Load saved width from localStorage const savedWidth = localStorage.getItem('nav-sidebar-width'); @@ -278,12 +162,12 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { this.currentWidth.set(width); } - this.checkScreenSize(); + this._checkScreenSize(); } @HostListener('window:resize') onWindowResize(): void { - this.checkScreenSize(); + this._checkScreenSize(); } @HostListener('document:click', ['$event']) @@ -299,16 +183,16 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { } } - private checkScreenSize(): void { + private _checkScreenSize(): void { const wasMobile = this.isMobile(); - const currentMobile = window.innerWidth < (this.config.mobileBreakpoint || 768); + const currentMobile = window.innerWidth < (this.config().mobileBreakpoint || 768); this.isMobile.set(currentMobile); if (wasMobile !== currentMobile) { if (currentMobile) { this.showMobileMenu.set(false); } else { - this.isExpanded.set(this.config.expandedByDefault ?? true); + this.isExpanded.set(this.config().expandedByDefault ?? true); } } } @@ -343,8 +227,8 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { isGroupExpanded(item: NavItem): boolean { if (item.type !== 'group') return false; // Prefer external expansion state for known groups, but fall back to local for others (e.g., Help) - if (this.expandedGroupIds && this.expandedGroupIds.size > 0) { - if (this.expandedGroupIds.has(item.id)) { + if (this.expandedGroupIds && this.expandedGroupIds().size > 0) { + if (this.expandedGroupIds().has(item.id)) { return true; } // If not controlled externally, use local state @@ -357,9 +241,9 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // Allow external toggle handler if provided if (item.action) { item.action(); - } else { - this.toggleGroup(item); } + // Always reflect the toggle locally so UI responds immediately + this.toggleGroup(item); return; } @@ -377,25 +261,25 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // Resize functionality onResizeStart(event: MouseEvent): void { - if (!this.config.resizable || this.isMobile()) return; + if (!this.config().resizable || this.isMobile()) return; this.isResizing.set(true); this.startX.set(event.clientX); this.startWidth.set(this.isExpanded() ? this.currentWidth() : COLLAPSED_WIDTH); - document.addEventListener('mousemove', this.onDrag); - document.addEventListener('mouseup', this.onDragEnd); + document.addEventListener('mousemove', this._onDrag); + document.addEventListener('mouseup', this._onDragEnd); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; event.preventDefault(); } - private handleDrag(event: MouseEvent): void { + private _handleDrag(event: MouseEvent): void { if (!this.isResizing()) return; const deltaX = - this.config.position === 'right' + this.config().position === 'right' ? this.startX() - event.clientX : event.clientX - this.startX(); @@ -462,12 +346,12 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { } } - private handleDragEnd(): void { + private _handleDragEnd(): void { if (!this.isResizing()) return; this.isResizing.set(false); - document.removeEventListener('mousemove', this.onDrag); - document.removeEventListener('mouseup', this.onDragEnd); + document.removeEventListener('mousemove', this._onDrag); + document.removeEventListener('mouseup', this._onDragEnd); document.body.style.cursor = ''; document.body.style.userSelect = ''; diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.spec.ts b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.spec.ts deleted file mode 100644 index ce5989673a..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -// import { ComponentFixture, TestBed } from '@angular/core/testing'; -// -// import { SideNavItemComponent } from './side-nav-item.component'; -// -// describe('SideNavItemComponent', () => { -// let component: SideNavItemComponent; -// let fixture: ComponentFixture; -// -// beforeEach(async () => { -// await TestBed.configureTestingModule({ -// imports: [SideNavItemComponent] -// }) -// .compileComponents(); -// -// fixture = TestBed.createComponent(SideNavItemComponent); -// component = fixture.componentInstance; -// fixture.detectChanges(); -// }); -// -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts index 92b1dfb5dd..a1ef85a117 100644 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts @@ -3,7 +3,6 @@ import { Component, computed, ElementRef, - HostBinding, inject, input, viewChild, @@ -26,6 +25,12 @@ import { selectAllDoneIds } from '../../../features/tasks/store/task.selectors'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; +const HOST_HAS_TASKS = '[class.hasTasks]'; +const HOST_IS_ACTIVE = '[class.isActiveContext]'; +const HOST_IS_HIDDEN = '[class.isHidden]'; +const HOST_VARIANT_NAV = '[class.variant-nav]'; +const HOST_IS_COMPACT = '[class.compact]'; + @Component({ selector: 'nav-item-inner', imports: [ @@ -42,7 +47,14 @@ import { TranslatePipe } from '@ngx-translate/core'; templateUrl: './nav-item-inner.component.html', styleUrl: './nav-item-inner.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'g-multi-btn-wrapper' }, + host: { + class: 'g-multi-btn-wrapper', + [HOST_HAS_TASKS]: 'workContextHasTasks', + [HOST_IS_ACTIVE]: 'isActiveContext', + [HOST_IS_HIDDEN]: 'isHidden', + [HOST_VARIANT_NAV]: 'isVariantNav', + [HOST_IS_COMPACT]: 'isCompact', + }, standalone: true, }) export class NavItemInnerComponent { @@ -77,38 +89,33 @@ export class NavItemInnerComponent { return wc.taskIds.filter((tid) => !allUndoneTaskIds.includes(tid)).length; }); - readonly routeBtn = viewChild('routeBtn', { read: ElementRef }); + private readonly _routeBtn = viewChild('routeBtn', { read: ElementRef }); - @HostBinding('class.hasTasks') get workContextHasTasks(): boolean { const wc = this.workContext(); return !!wc && wc.taskIds.length > 0; } - @HostBinding('class.isActiveContext') get isActiveContext(): boolean { const wc = this.workContext(); return !!wc && wc.id === this.activeWorkContextId(); } - @HostBinding('class.isHidden') get isHidden(): boolean { const wc = this.workContext(); return !!(wc as Project | null)?.isHiddenFromMenu; } - @HostBinding('class.variant-nav') get isVariantNav(): boolean { return this.variant() === 'nav'; } - @HostBinding('class.compact') get isCompact(): boolean { return this.compact(); } focus(): void { - const btn = this.routeBtn(); + const btn = this._routeBtn(); if (btn) { btn.nativeElement.focus(); } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.css b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.css deleted file mode 100644 index 968566ce7f..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.css +++ /dev/null @@ -1,43 +0,0 @@ -:host { - display: contents; -} - -.nav-link, -.nav-child-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - position: relative; -} - -.nav-link:hover, -.nav-child-link:hover { - background: var(--sidebar-hover); -} - -.nav-link.active, -.nav-child-link.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); -} - -.nav-child-link { - padding: var(--s) var(--s2); - color: var(--sidebar-text-secondary); - border-radius: 6px; - font-size: 14px; -} - -.nav-child-link:hover { - color: var(--sidebar-text); -} diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts index 1cb6f9bfbf..1a5b171995 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts @@ -1,15 +1,15 @@ import { Component, computed, input, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; -import { NavItem } from '../magic-side-nav'; import { NavItemInnerComponent } from '../nav-item-inner/nav-item-inner.component'; +import { NavItem } from '../magic-side-nav.model'; @Component({ selector: 'nav-item', standalone: true, imports: [CommonModule, RouterModule, NavItemInnerComponent], templateUrl: './nav-item.component.html', - styleUrl: './nav-item.component.css', + styleUrl: './nav-item.component.scss', }) export class SpNavItemComponent { item = input.required(); diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css deleted file mode 100644 index 381195bfd3..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.css +++ /dev/null @@ -1,72 +0,0 @@ -:host { - display: contents; -} - -.g-multi-btn-wrapper { - display: flex; - align-items: center; - position: relative; -} - -.nav-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - position: relative; -} - -.nav-link:hover { - background: var(--sidebar-hover); -} - -.nav-link.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); -} - -.nav-link.expanded .nav-chevron { - transform: rotate(180deg); -} - -.additional-btn { - flex-shrink: 0; - width: 32px; - height: 32px; - min-width: 32px; - padding: 4px; - margin-left: var(--s); - color: var(--sidebar-text-secondary); - background: transparent; - border: none; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; -} - -.additional-btn:hover { - background-color: var(--sidebar-hover); - color: var(--sidebar-text); -} - -.nav-children { - list-style: none; - padding: 0; - margin: 4px 0 0 0; - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease; -} - -.nav-children.visible { - max-height: 500px; -} diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index 8587eacf0b..6e2f700471 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -9,13 +9,12 @@ [attr.aria-controls]="'group-' + item().id" > diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts index 9534b696d3..761580fef8 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts @@ -4,9 +4,9 @@ import { MatIcon } from '@angular/material/icon'; import { MatIconButton } from '@angular/material/button'; import { MatTooltip } from '@angular/material/tooltip'; import { TranslatePipe } from '@ngx-translate/core'; -import { NavGroupItem, NavItem } from '../magic-side-nav'; import { NavItemInnerComponent } from '../nav-item-inner/nav-item-inner.component'; import { SpNavItemComponent } from '../nav-item/nav-item.component'; +import { NavGroupItem, NavItem } from '../magic-side-nav.model'; @Component({ selector: 'nav-list', @@ -21,7 +21,7 @@ import { SpNavItemComponent } from '../nav-item/nav-item.component'; SpNavItemComponent, ], templateUrl: './nav-list.component.html', - styleUrl: './nav-list.component.css', + styleUrl: './nav-list.component.scss', }) export class SpNavSectionComponent { item = input.required(); diff --git a/src/app/core-ui/side-nav/side-nav-config.service.ts b/src/app/core-ui/side-nav/side-nav-config.service.ts index d25f32bce0..7ffeca41b6 100644 --- a/src/app/core-ui/side-nav/side-nav-config.service.ts +++ b/src/app/core-ui/side-nav/side-nav-config.service.ts @@ -3,7 +3,6 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { Store } from '@ngrx/store'; import { MatDialog } from '@angular/material/dialog'; -import { NavConfig, NavItem } from '../magic-side-nav/magic-side-nav'; import { WorkContextType } from '../../features/work-context/work-context.model'; import { WorkContextService } from '../../features/work-context/work-context.service'; import { TagService } from '../../features/tag/tag.service'; @@ -14,6 +13,7 @@ import { LS } from '../../core/persistence/storage-keys.const'; import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component'; import { getGithubErrorUrl } from '../../core/error-handler/global-error-handler.util'; import { selectUnarchivedVisibleProjects } from '../../features/project/store/project.selectors'; +import { NavConfig, NavItem } from '../magic-side-nav/magic-side-nav.model'; @Injectable({ providedIn: 'root', From bd36bf9d2b7f86ee97a5f9108a34c42c593805fe Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 19:23:29 +0200 Subject: [PATCH 18/61] refactor(sideNav): improve 2 --- src/app/app.component.html | 6 +- src/app/app.component.ts | 28 +- src/app/core-ui/layout/layout.service.ts | 11 +- .../magic-nav-config.service.ts} | 10 +- .../magic-side-nav/magic-side-nav.html | 2 - .../magic-side-nav/magic-side-nav.model.ts | 22 +- .../core-ui/magic-side-nav/magic-side-nav.ts | 171 +++------- .../core-ui/side-nav/side-nav.component.html | 309 ----------------- .../core-ui/side-nav/side-nav.component.scss | 177 ---------- .../side-nav/side-nav.component.spec.ts | 25 -- .../core-ui/side-nav/side-nav.component.ts | 321 ------------------ .../core/persistence/storage-keys.const.ts | 4 + .../features/shepherd/shepherd-steps.const.ts | 4 +- 13 files changed, 82 insertions(+), 1008 deletions(-) rename src/app/core-ui/{side-nav/side-nav-config.service.ts => magic-side-nav/magic-nav-config.service.ts} (97%) delete mode 100644 src/app/core-ui/side-nav/side-nav.component.html delete mode 100644 src/app/core-ui/side-nav/side-nav.component.scss delete mode 100644 src/app/core-ui/side-nav/side-nav.component.spec.ts delete mode 100644 src/app/core-ui/side-nav/side-nav.component.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index 96249efd4c..4ddec663cc 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -30,13 +30,9 @@ [dir]="isRTL ? 'rtl' : 'ltr'" > diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 51314f657a..a4a9343c44 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -47,7 +47,7 @@ import { GlobalConfigState } from './features/config/global-config.model'; import { AddTaskBarComponent } from './features/tasks/add-task-bar/add-task-bar.component'; import { Dir } from '@angular/cdk/bidi'; import { MagicSideNavComponent } from './core-ui/magic-side-nav/magic-side-nav'; -import { SideNavConfigService } from './core-ui/side-nav/side-nav-config.service'; +import { MagicNavConfigService } from './core-ui/magic-side-nav/magic-nav-config.service'; import { MainHeaderComponent } from './core-ui/main-header/main-header.component'; import { BannerComponent } from './core/banner/banner/banner.component'; import { GlobalProgressBarComponent } from './core-ui/global-progress-bar/global-progress-bar.component'; @@ -82,7 +82,6 @@ import { ProjectService } from './features/project/project.service'; import { TagService } from './features/tag/tag.service'; import { ContextMenuComponent } from './ui/context-menu/context-menu.component'; import { WorkContextThemeCfg } from './features/work-context/work-context.model'; -import { NavConfig } from './core-ui/magic-side-nav/magic-side-nav.model'; interface BeforeInstallPromptEvent extends Event { prompt(): Promise; @@ -152,7 +151,7 @@ export class AppComponent implements OnDestroy, AfterViewInit { private _syncSafetyBackupService = inject(SyncSafetyBackupService); readonly syncTriggerService = inject(SyncTriggerService); - readonly sideNavConfigService = inject(SideNavConfigService); + readonly sideNavConfigService = inject(MagicNavConfigService); readonly imexMetaService = inject(ImexViewService); readonly workContextService = inject(WorkContextService); readonly layoutService = inject(LayoutService); @@ -446,34 +445,13 @@ export class AppComponent implements OnDestroy, AfterViewInit { return outlet.activatedRouteData.page || 'one'; } - getMagicSideNavConfig(): NavConfig { - const baseConfig = this.sideNavConfigService.navConfig(); - return { - ...baseConfig, - expandedByDefault: this.layoutService.isShowSideNav(), - // Enable magic-side-nav mobile overlay for small screens - mobileBreakpoint: 768, - }; - } - - onSideNavExpandedChange(isExpanded: boolean): void { - const currentlyExpanded = this.layoutService.isShowSideNav(); - if (isExpanded !== currentlyExpanded) { - this.layoutService.toggleSideNav(); - } - } - // Keep layout service in sync when mobile overlay closes via backdrop/item click onMobileNavVisibleChange(isVisible: boolean): void { - if (!isVisible && this.layoutService.isShowSideNav()) { + if (!isVisible) { this.layoutService.hideSideNav(); } } - getExpandedGroupIds(): Set { - return this.sideNavConfigService.expandedGroupIds(); - } - getActiveWorkContextId(): string | null { return this._activeWorkContextId(); } diff --git a/src/app/core-ui/layout/layout.service.ts b/src/app/core-ui/layout/layout.service.ts index b933f9e82d..a204e9c432 100644 --- a/src/app/core-ui/layout/layout.service.ts +++ b/src/app/core-ui/layout/layout.service.ts @@ -47,7 +47,7 @@ export class LayoutService { readonly isShowAddTaskBar$: Observable = this._store$.pipe( select(selectIsShowAddTaskBar), ); - readonly isShowSideNav$: Observable = this._store$.pipe( + readonly isShowMobileSideNav$: Observable = this._store$.pipe( select(selectIsShowSideNav), ); readonly isShowIssuePanel$: Observable = this._store$.pipe( @@ -129,15 +129,10 @@ export class LayoutService { return !this.isRightPanelNextNavOver(); }); - private readonly _isShowSideNav = toSignal(this.isShowSideNav$, { + private readonly isShowMobileSideNav = toSignal(this.isShowMobileSideNav$, { initialValue: false, }); - readonly isShowSideNav = computed(() => { - const isShow = this._isShowSideNav(); - return isShow || this.isNavAlwaysVisible(); - }); - readonly isShowNotes = toSignal(this._store$.pipe(select(selectIsShowNotes)), { initialValue: false, }); @@ -156,7 +151,7 @@ export class LayoutService { this._router.events.pipe(filter((ev) => ev instanceof NavigationStart)), this._workContextService.onWorkContextChange$, ).subscribe(() => { - if (this.isNavOver() && this._isShowSideNav()) { + if (this.isNavOver() && this.isShowMobileSideNav()) { this.hideSideNav(); } }); diff --git a/src/app/core-ui/side-nav/side-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts similarity index 97% rename from src/app/core-ui/side-nav/side-nav-config.service.ts rename to src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index 7ffeca41b6..e2a634198c 100644 --- a/src/app/core-ui/side-nav/side-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -1,4 +1,4 @@ -import { Injectable, computed, inject, signal } from '@angular/core'; +import { computed, inject, Injectable, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { Store } from '@ngrx/store'; import { MatDialog } from '@angular/material/dialog'; @@ -13,12 +13,12 @@ import { LS } from '../../core/persistence/storage-keys.const'; import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component'; import { getGithubErrorUrl } from '../../core/error-handler/global-error-handler.util'; import { selectUnarchivedVisibleProjects } from '../../features/project/store/project.selectors'; -import { NavConfig, NavItem } from '../magic-side-nav/magic-side-nav.model'; +import { NavConfig, NavItem } from './magic-side-nav.model'; @Injectable({ providedIn: 'root', }) -export class SideNavConfigService { +export class MagicNavConfigService { private readonly _workContextService = inject(WorkContextService); private readonly _tagService = inject(TagService); private readonly _shepherdService = inject(ShepherdService); @@ -298,7 +298,7 @@ export class SideNavConfigService { route: `/project/${project.id}/tasks`, workContext: project, workContextType: WorkContextType.PROJECT, - defaultIcon: 'folder_special', + defaultIcon: project.icon || 'folder_special', })); } @@ -320,7 +320,7 @@ export class SideNavConfigService { route: `/tag/${tag.id}/tasks`, workContext: tag, workContextType: WorkContextType.TAG, - defaultIcon: 'label', + defaultIcon: tag.icon || 'label', })); } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.html b/src/app/core-ui/magic-side-nav/magic-side-nav.html index bbbf57f925..a928f1962c 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.html @@ -45,8 +45,6 @@ [class.theme-dark]="config().theme === 'dark'" [class.position-right]="config().position === 'right'" [class.resizing]="isResizing()" - [class.collapse-zone]="resizeState() === 'collapse'" - [class.expand-zone]="resizeState() === 'expand'" [style.width.px]="sidebarWidth()" role="navigation" > diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts index 2feeb3452a..1ef4a84c30 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts @@ -81,15 +81,15 @@ export interface NavAdditionalButton { } export interface NavConfig { items: NavItem[]; - expandedByDefault?: boolean; - showLabels?: boolean; - mobileBreakpoint?: number; - position?: 'left' | 'right'; - theme?: 'light' | 'dark'; - resizable?: boolean; - minWidth?: number; - maxWidth?: number; - defaultWidth?: number; - collapseThreshold?: number; - expandThreshold?: number; + expandedByDefault: boolean; + showLabels: boolean; + mobileBreakpoint: number; + position: 'left' | 'right'; + theme: 'light' | 'dark'; + resizable: boolean; + minWidth: number; + maxWidth: number; + defaultWidth: number; + collapseThreshold: number; + expandThreshold: number; } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index 8965d51593..4b3fac950e 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { Component, computed, effect, HostListener, + inject, input, OnDestroy, OnInit, @@ -13,14 +15,12 @@ import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { SpNavItemComponent } from './nav-item/nav-item.component'; import { SpNavSectionComponent } from './nav-list/nav-list.component'; -import { NavConfig, NavItem } from './magic-side-nav.model'; +import { NavItem } from './magic-side-nav.model'; +import { LS } from '../../core/persistence/storage-keys.const'; +import { MagicNavConfigService } from './magic-nav-config.service'; const COLLAPSED_WIDTH = 64; -const HOST_WIDTH = '[style.width.px]'; -const HOST_ANIMATE = '[class.animate]'; -const HOST_RESIZING = '[class.resizing]'; - @Component({ selector: 'magic-side-nav', standalone: true, @@ -28,33 +28,18 @@ const HOST_RESIZING = '[class.resizing]'; templateUrl: './magic-side-nav.html', styleUrl: './magic-side-nav.scss', host: { - [HOST_WIDTH]: 'hostWidth()', - [HOST_ANIMATE]: 'hostAnimate()', - [HOST_RESIZING]: 'hostResizing()', + '[style.width.px]': 'hostWidthSignal()', + '[class.animate]': 'animateWidth()', + '[class.resizing]': 'isResizing()', }, }) export class MagicSideNavComponent implements OnInit, OnDestroy { - config = input({ - items: [], - expandedByDefault: true, - showLabels: true, - mobileBreakpoint: 768, - position: 'left', - theme: 'light', - resizable: false, - minWidth: 200, - maxWidth: 400, - defaultWidth: 260, - collapseThreshold: 150, - expandThreshold: 180, - }); + private readonly _sideNavConfigService = inject(MagicNavConfigService); + // Use service's computed signal directly + readonly config = this._sideNavConfigService.navConfig; - expandedGroupIds = input>(new Set()); activeWorkContextId = input(null); - itemClick = output(); - expandedChange = output(); - widthChange = output(); // Externally controlled mobile overlay visibility mobileVisible = input(null); mobileVisibleChange = output(); @@ -62,22 +47,18 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { isExpanded = signal(true); isMobile = signal(false); showMobileMenu = signal(false); - expandedGroups = signal(new Set()); - // Visual feedback state for resizing thresholds - resizeState = signal<'normal' | 'collapse' | 'expand'>('normal'); + // Track expanded groups as array for better signal change detection + expandedGroups = signal([]); // Animate only for collapsed/expanded toggle animateWidth = signal(false); private _animateTimeoutId: number | null = null; // Resize functionality currentWidth = signal(260); - minWidth = signal(200); - maxWidth = signal(400); + // Use values directly from config for min/max/thresholds isResizing = signal(false); startX = signal(0); startWidth = signal(0); - collapseThreshold = signal(150); - expandThreshold = signal(180); // Computed values sidebarWidth = computed(() => { @@ -86,22 +67,8 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { return this.currentWidth(); }); - // Animate the host width so layout adjusts smoothly - hostWidth(): number { - // On mobile, don't reserve layout space; use fixed overlay instead - if (this.isMobile()) { - return 0; - } - return this.sidebarWidth(); - } - - hostAnimate(): boolean { - return this.animateWidth(); - } - - hostResizing(): boolean { - return this.isResizing(); - } + // Host width as computed signal: don't reserve space on mobile overlay + readonly hostWidthSignal = computed(() => (this.isMobile() ? 0 : this.sidebarWidth())); // Keep stable references for event listeners to ensure proper cleanup private readonly _onDrag: (event: MouseEvent) => void = (event: MouseEvent) => @@ -109,16 +76,11 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { private readonly _onDragEnd: () => void = () => this._handleDragEnd(); constructor() { - // Effect to emit width changes - effect(() => { - this.widthChange.emit(this.sidebarWidth()); - }); - // Sync external mobile visibility input → internal state effect(() => { const desired = this.mobileVisible(); if (desired !== null && this.isMobile()) { - this.showMobileMenu.set(!!desired); + this.showMobileMenu.set(desired); } }); @@ -139,26 +101,19 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { ngOnInit(): void { // Load saved expansion state or default to config value - const savedExpanded = localStorage.getItem('nav-sidebar-expanded'); + const savedExpanded = localStorage.getItem(LS.NAV_SIDEBAR_EXPANDED); const initialExpanded = - savedExpanded !== null - ? savedExpanded === 'true' - : (this.config().expandedByDefault ?? true); + savedExpanded !== null ? savedExpanded === 'true' : this.config().expandedByDefault; this.isExpanded.set(initialExpanded); - this.currentWidth.set(this.config().defaultWidth ?? 260); - this.minWidth.set(this.config().minWidth ?? 200); - this.maxWidth.set(this.config().maxWidth ?? 400); - this.collapseThreshold.set(this.config().collapseThreshold ?? 150); - this.expandThreshold.set(this.config().expandThreshold ?? 180); + this.currentWidth.set(this.config().defaultWidth); // Load saved width from localStorage - const savedWidth = localStorage.getItem('nav-sidebar-width'); + const savedWidth = localStorage.getItem(LS.NAV_SIDEBAR_WIDTH); if (savedWidth) { - const width = Math.max( - this.minWidth(), - Math.min(this.maxWidth(), parseInt(savedWidth)), - ); + const minW = this.config().minWidth; + const maxW = this.config().maxWidth; + const width = Math.max(minW, Math.min(maxW, parseInt(savedWidth))); this.currentWidth.set(width); } @@ -192,7 +147,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { if (currentMobile) { this.showMobileMenu.set(false); } else { - this.isExpanded.set(this.config().expandedByDefault ?? true); + this.isExpanded.set(this.config().expandedByDefault); } } } @@ -205,35 +160,25 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { const newExpanded = !this.isExpanded(); this.isExpanded.set(newExpanded); // Save expansion state to localStorage - localStorage.setItem('nav-sidebar-expanded', newExpanded.toString()); - this.expandedChange.emit(newExpanded); + localStorage.setItem(LS.NAV_SIDEBAR_EXPANDED, newExpanded.toString()); + // handled internally } } toggleGroup(item: NavItem): void { if (item.type === 'group' && item.children && item.children.length > 0) { - this.expandedGroups.update((groups) => { - const newGroups = new Set(groups); - if (newGroups.has(item.id)) { - newGroups.delete(item.id); - } else { - newGroups.add(item.id); - } - return newGroups; - }); + const groups = this.expandedGroups(); + if (groups.includes(item.id)) { + this.expandedGroups.set(groups.filter((g) => g !== item.id)); + } else { + this.expandedGroups.set([...groups, item.id]); + } } } isGroupExpanded(item: NavItem): boolean { if (item.type !== 'group') return false; - // Prefer external expansion state for known groups, but fall back to local for others (e.g., Help) - if (this.expandedGroupIds && this.expandedGroupIds().size > 0) { - if (this.expandedGroupIds().has(item.id)) { - return true; - } - // If not controlled externally, use local state - } - return this.expandedGroups().has(item.id); + return this.expandedGroups().includes(item.id); } onItemClick(item: NavItem): void { @@ -251,8 +196,8 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { item.action?.(); } - // Emit to parent for external handling (e.g., href open) - this.itemClick.emit(item); + // Handle via service for actions/hrefs + this._sideNavConfigService.onNavItemClick(item); if (this.isMobile()) { this.showMobileMenu.set(false); @@ -284,36 +229,32 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { : event.clientX - this.startX(); const potentialWidth = this.startWidth() + deltaX; + const cfg = this.config(); + const collapseTh = cfg.collapseThreshold; + const expandTh = cfg.expandThreshold; + const minW = cfg.minWidth; + const maxW = cfg.maxWidth; // Handle seamless mode transitions if (this.isExpanded()) { // Currently expanded - check for collapse threshold - if (potentialWidth < this.collapseThreshold()) { + if (potentialWidth < collapseTh) { // Auto-collapse without releasing mouse this.isExpanded.set(false); - this.expandedChange.emit(this.isExpanded()); + // handled internally // Reset starting point for continued dragging from collapsed state this.startWidth.set(COLLAPSED_WIDTH); this.startX.set(event.clientX); - // Visual feedback - this.resizeState.set('normal'); + // No visual feedback return; } - // Show visual feedback when approaching collapse threshold - if (potentialWidth < this.collapseThreshold() + 20) { - this.resizeState.set('collapse'); - } else { - this.resizeState.set('normal'); - } + // No visual feedback when approaching collapse threshold // Normal resize when expanded - const newWidth = Math.max( - this.minWidth(), - Math.min(this.maxWidth(), potentialWidth), - ); + const newWidth = Math.max(minW, Math.min(maxW, potentialWidth)); this.currentWidth.set(newWidth); } else { @@ -321,28 +262,23 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { const collapsedWidth = COLLAPSED_WIDTH; const draggedWidth = collapsedWidth + deltaX; - if (draggedWidth > this.expandThreshold()) { + if (draggedWidth > expandTh) { // Auto-expand without releasing mouse this.isExpanded.set(true); - const newWidth = this.expandThreshold() + 20; // Start slightly beyond threshold + const newWidth = expandTh + 20; // Start slightly beyond threshold this.currentWidth.set(newWidth); - this.expandedChange.emit(this.isExpanded()); + // handled internally // Reset starting point for continued dragging from expanded state this.startWidth.set(this.currentWidth()); this.startX.set(event.clientX); - // Visual feedback - this.resizeState.set('normal'); + // No visual feedback return; } // Show visual feedback when approaching threshold - if (draggedWidth > this.expandThreshold() - 20) { - this.resizeState.set('expand'); - } else { - this.resizeState.set('normal'); - } + // No visual feedback when approaching expand threshold } } @@ -355,12 +291,11 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { document.body.style.cursor = ''; document.body.style.userSelect = ''; - // Reset visual feedback - this.resizeState.set('normal'); + // No visual feedback to reset // Save width to localStorage (only save if expanded) if (this.isExpanded()) { - localStorage.setItem('nav-sidebar-width', this.currentWidth().toString()); + localStorage.setItem(LS.NAV_SIDEBAR_WIDTH, this.currentWidth().toString()); } } diff --git a/src/app/core-ui/side-nav/side-nav.component.html b/src/app/core-ui/side-nav/side-nav.component.html deleted file mode 100644 index a3a79fdea4..0000000000 --- a/src/app/core-ui/side-nav/side-nav.component.html +++ /dev/null @@ -1,309 +0,0 @@ -
- @if (workContextService.mainWorkContext$ | async; as mainContext) { - - } - @let inboxContext = workContextService.inboxWorkContext$ | async; - @if (inboxContext) { - - } - - - - - - -
- -@if (nonHiddenProjects(); as projectList) { -
-
- - - - - - - - - - -
- - - - @for (project of allNonInboxProjects(); track project.id) { - - } - -
- @for (project of projectList; track project.id) { - - } -
- @if (!projectList.length && isProjectsExpanded()) { -
{{ T.MH.NO_PROJECT_INFO | translate }}
- } -
-} -@if (tagList(); as tagList) { -
- -
- @for (tag of tagList; track tag.id) { - - } -
- @if (!tagList.length && isTagsExpanded()) { -
{{ T.MH.NO_TAG_INFO | translate }}
- } - - - - - - -
-} - - - - -
- - - - - - - - - help_center - {{ T.MH.HM.GET_HELP_ONLINE | translate }} - - - - bug_report - {{ T.MH.HM.REPORT_A_PROBLEM | translate }} - - - - volunteer_activism - {{ T.MH.HM.CONTRIBUTE | translate }} - - - - forum - {{ T.MH.HM.REDDIT_COMMUNITY | translate }} - - - - @if (IS_MOUSE_PRIMARY) { - - } - - - - - - - - -
- - - diff --git a/src/app/core-ui/side-nav/side-nav.component.scss b/src/app/core-ui/side-nav/side-nav.component.scss deleted file mode 100644 index 8216cd526e..0000000000 --- a/src/app/core-ui/side-nav/side-nav.component.scss +++ /dev/null @@ -1,177 +0,0 @@ -@use '_globals' as *; - -// MAIN LAYOUT -// ----------- -:host { - flex-grow: 1; - max-width: var(--side-nav-width); - width: var(--side-nav-width); - display: flex; - flex-direction: column; - min-height: 100vh; - // mobile viewport bug fix - min-height: -webkit-fill-available; - - &.minimal-nav { - max-width: 42px; - - @include mq(xxs) { - max-width: 52px; - } - - .expand-btn mat-icon { - margin-left: 0; - } - - .expand-btn .title { - display: none; - } - - ::ng-deep { - @include mq(xxs, max) { - .badge { - margin-right: -40px !important; - } - } - - .additional-btn { - display: none !important; - } - - .mat-mdc-menu-item { - padding: 0 8px; - - @include mq(xxs) { - padding: 0 13px; - } - } - } - } - - a, - button { - height: 44px !important; - line-height: 44px !important; - min-height: 44px !important; - } - - a { - color: rgba(var(--palette-foreground-text), var(--palette-foreground-text-alpha)); - } -} - -@media (min-width: var(--side-nav-width-switch-l)) { - :host { - max-width: var(--side-nav-width-l); - width: var(--side-nav-width-l); - } -} - -@media (max-width: 500px) { - :host { - max-width: var(--side-nav-width-mobile); - width: var(--side-nav-width-mobile); - } -} - -section { - display: flex; - flex-direction: column; -} - -.main { - margin-bottom: var(--s); - padding-bottom: var(--s); - - :host-context(.isMac.isElectron) & { - padding-top: var(--mac-title-bar-padding); - } - - &:after { - bottom: 0; - } -} - -.app { - margin-top: auto; - padding-top: var(--s); - - &:after { - top: 0; - } -} - -.other-task-level-section, -.main, -.tags, -.app { - position: relative; - - &:after { - border-bottom: 1px solid var(--divider-color); - content: ''; - position: absolute; - left: var(--s2); - right: var(--s2); - height: 0; - } -} - -.tags, -.projects { - margin-bottom: var(--s); - padding-bottom: var(--s); -} - -// LIST-ITEMS -// ---------- - -.route-link.isActiveRoute { - background-color: mat-css-color-primary(500, 0.04); - font-weight: bold; -} - -.route-link.isActiveRoute, -.route-link.isActiveRoute mat-icon { - color: var(--palette-primary-800); -} - -body.isDarkTheme .route-link.isActiveRoute, -body.isDarkTheme .route-link.isActiveRoute mat-icon { - color: var(--c-primary); -} - -// OTHER-ITEMS -// ---------- - -.expand-btn { - display: flex; - align-items: center; - font-weight: bold; - font-size: 12px; - opacity: 0.7; - - &:focus, - &:hover { - > mat-icon:last-of-type { - opacity: 1; - } - } - - > mat-icon:last-of-type { - margin-right: 0; - //margin-left: -10px; - opacity: 0.4; - transition: var(--transition-standard); - } - - &.isExpanded ::ng-deep mat-icon:last-of-type { - transform: rotate(180deg); - } -} - -.no-tags-info { - padding: 4px 16px; - font-style: italic; - opacity: 0.7; -} diff --git a/src/app/core-ui/side-nav/side-nav.component.spec.ts b/src/app/core-ui/side-nav/side-nav.component.spec.ts deleted file mode 100644 index 1c88ebcc7c..0000000000 --- a/src/app/core-ui/side-nav/side-nav.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -// -// import { SideNavComponent } from './side-nav.component'; -// -// describe('ProjectListComponent', () => { -// let component: SideNavComponent; -// let fixture: ComponentFixture; -// -// beforeEach(async(() => { -// TestBed.configureTestingModule({ -// declarations: [ SideNavComponent ] -// }) -// .compileComponents(); -// })); -// -// beforeEach(() => { -// fixture = TestBed.createComponent(SideNavComponent); -// component = fixture.componentInstance; -// fixture.detectChanges(); -// }); -// -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); diff --git a/src/app/core-ui/side-nav/side-nav.component.ts b/src/app/core-ui/side-nav/side-nav.component.ts deleted file mode 100644 index 382590ca91..0000000000 --- a/src/app/core-ui/side-nav/side-nav.component.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - effect, - ElementRef, - HostBinding, - HostListener, - inject, - OnDestroy, - signal, - viewChild, - viewChildren, -} from '@angular/core'; -import { ProjectService } from '../../features/project/project.service'; -import { T } from '../../t.const'; -import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component'; -import { Project } from '../../features/project/project.model'; -import { MatDialog } from '@angular/material/dialog'; -import { DRAG_DELAY_FOR_TOUCH_LONGER } from '../../app.constants'; -import { Subscription } from 'rxjs'; -import { WorkContextService } from '../../features/work-context/work-context.service'; -import { standardListAnimation } from '../../ui/animations/standard-list.ani'; -import { first } from 'rxjs/operators'; -import { TagService } from '../../features/tag/tag.service'; -import { Tag } from '../../features/tag/tag.model'; -import { WorkContextType } from '../../features/work-context/work-context.model'; -import { expandFadeAnimation } from '../../ui/animations/expand.ani'; -import { FocusKeyManager } from '@angular/cdk/a11y'; -import { - MatMenu, - MatMenuContent, - MatMenuItem, - MatMenuTrigger, -} from '@angular/material/menu'; -import { LayoutService } from '../layout/layout.service'; -import { TaskService } from '../../features/tasks/task.service'; -import { LS } from '../../core/persistence/storage-keys.const'; -import { TODAY_TAG } from '../../features/tag/tag.const'; -import { TourId } from '../../features/shepherd/shepherd-steps.const'; -import { ShepherdService } from '../../features/shepherd/shepherd.service'; -import { getGithubErrorUrl } from '../../core/error-handler/global-error-handler.util'; -import { IS_MOUSE_PRIMARY, IS_TOUCH_PRIMARY } from '../../util/is-mouse-primary'; -import { GlobalConfigService } from '../../features/config/global-config.service'; -import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; -import { moveItemBeforeItem } from '../../util/move-item-before-item'; -import { Store } from '@ngrx/store'; -import { - selectAllProjectsExceptInbox, - selectUnarchivedHiddenProjectIds, - selectUnarchivedVisibleProjects, -} from '../../features/project/store/project.selectors'; -import { NavItemInnerComponent } from '../magic-side-nav/nav-item-inner/nav-item-inner.component'; -import { RouterLink, RouterLinkActive } from '@angular/router'; -import { MatIcon } from '@angular/material/icon'; -import { MatIconButton } from '@angular/material/button'; -import { MatTooltip } from '@angular/material/tooltip'; -import { ContextMenuComponent } from '../../ui/context-menu/context-menu.component'; -import { TranslatePipe } from '@ngx-translate/core'; -import { AsyncPipe } from '@angular/common'; -import { toggleHideFromMenu } from '../../features/project/store/project.actions'; -import { PluginMenuComponent } from '../../plugins/ui/plugin-menu.component'; -import { toSignal } from '@angular/core/rxjs-interop'; - -@Component({ - selector: 'side-nav', - templateUrl: './side-nav.component.html', - styleUrls: ['./side-nav.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - animations: [standardListAnimation, expandFadeAnimation], - imports: [ - NavItemInnerComponent, - MatMenuItem, - RouterLink, - RouterLinkActive, - MatIcon, - MatIconButton, - MatTooltip, - ContextMenuComponent, - CdkDropList, - CdkDrag, - MatMenuTrigger, - MatMenu, - MatMenuContent, - TranslatePipe, - AsyncPipe, - PluginMenuComponent, - ], -}) -export class SideNavComponent implements OnDestroy { - readonly tagService = inject(TagService); - readonly projectService = inject(ProjectService); - readonly workContextService = inject(WorkContextService); - private readonly _matDialog = inject(MatDialog); - private readonly _layoutService = inject(LayoutService); - private readonly _taskService = inject(TaskService); - private readonly _shepherdService = inject(ShepherdService); - private readonly _globalConfigService = inject(GlobalConfigService); - private readonly _store = inject(Store); - - navEntries = viewChildren('menuEntry'); - IS_MOUSE_PRIMARY = IS_MOUSE_PRIMARY; - IS_TOUCH_PRIMARY = IS_TOUCH_PRIMARY; - DRAG_DELAY_FOR_TOUCH_LONGER = DRAG_DELAY_FOR_TOUCH_LONGER; - - keyboardFocusTimeout?: number; - projectExpandBtn = viewChild('projectExpandBtn', { read: ElementRef }); - isProjectsExpanded = signal(this.fetchProjectListState()); - - allNonInboxProjects = toSignal(this._store.select(selectAllProjectsExceptInbox), { - initialValue: [], - }); - private _allVisibleProjects = toSignal( - this._store.select(selectUnarchivedVisibleProjects), - { initialValue: [] }, - ); - private _activeWorkContextId = toSignal(this.workContextService.activeWorkContextId$, { - initialValue: null, - }); - - nonHiddenProjects = computed(() => { - const isExpanded = this.isProjectsExpanded(); - const projects = this._allVisibleProjects(); - if (isExpanded) { - return projects; - } - const activeId = this._activeWorkContextId(); - return projects.filter((p) => p.id === activeId); - }); - - tagExpandBtn = viewChild('tagExpandBtn', { read: ElementRef }); - isTagsExpanded = signal(this.fetchTagListState()); - - private _tagListToDisplay = toSignal(this.tagService.tagsNoMyDayAndNoList$, { - initialValue: [], - }); - - tagList = computed(() => { - const isExpanded = this.isTagsExpanded(); - const tags = this._tagListToDisplay(); - if (isExpanded) { - return tags; - } - const activeId = this._activeWorkContextId(); - return tags.filter((t) => t.id === activeId); - }); - T: typeof T = T; - activeWorkContextId?: string | null; - WorkContextType: typeof WorkContextType = WorkContextType; - TourId: typeof TourId = TourId; - private keyManager?: FocusKeyManager; - private _subs: Subscription = new Subscription(); - private _cachedIssueUrl?: string; - - @HostBinding('class') get cssClass(): string { - return this._globalConfigService.cfg()?.misc.isUseMinimalNav ? 'minimal-nav' : ''; - } - - constructor() { - this._subs.add( - this.workContextService.activeWorkContextId$.subscribe( - (id) => (this.activeWorkContextId = id), - ), - ); - - // Effect to handle side nav visibility changes - effect(() => { - const isShow = this._layoutService.isShowSideNav(); - const navEntries = this.navEntries(); - if (navEntries && isShow) { - this.keyManager = new FocusKeyManager( - navEntries, - ).withVerticalOrientation(true); - window.clearTimeout(this.keyboardFocusTimeout); - this.keyboardFocusTimeout = window.setTimeout(() => { - this.keyManager?.setFirstItemActive(); - }, 100); - // this.keyManager.change.subscribe((v) => Log.log('this.keyManager.change', v)); - } else if (!isShow) { - this._taskService.focusFirstTaskIfVisible(); - } - }); - } - - @HostListener('keydown', ['$event']) - onKeydown(event: KeyboardEvent): void { - this.keyManager?.onKeydown(event); - } - - ngOnDestroy(): void { - this._subs.unsubscribe(); - window.clearTimeout(this.keyboardFocusTimeout); - } - - addProject(): void { - this._matDialog.open(DialogCreateProjectComponent, { - restoreFocus: true, - }); - } - - fetchProjectListState(): boolean { - return localStorage.getItem(LS.IS_PROJECT_LIST_EXPANDED) === 'true'; - } - - private _storeProjectListState(isExpanded: boolean): void { - this.isProjectsExpanded.set(isExpanded); - localStorage.setItem(LS.IS_PROJECT_LIST_EXPANDED, isExpanded.toString()); - } - - fetchTagListState(): boolean { - return localStorage.getItem(LS.IS_TAG_LIST_EXPANDED) === 'true'; - } - - storeTagListState(isExpanded: boolean): void { - this.isTagsExpanded.set(isExpanded); - localStorage.setItem(LS.IS_TAG_LIST_EXPANDED, isExpanded.toString()); - } - - toggleExpandProjects(): void { - const newState: boolean = !this.isProjectsExpanded(); - this._storeProjectListState(newState); - } - - toggleExpandProjectsLeftRight(ev: KeyboardEvent): void { - if (ev.key === 'ArrowLeft' && this.isProjectsExpanded()) { - this._storeProjectListState(false); - } else if (ev.key === 'ArrowRight' && !this.isProjectsExpanded()) { - this._storeProjectListState(true); - } - } - - checkFocusProject(ev: KeyboardEvent): void { - if (ev.key === 'ArrowLeft' && this.projectExpandBtn()?.nativeElement) { - const targetIndex = this.navEntries().findIndex((value) => { - return ( - typeof value._getHostElement === 'function' && - value._getHostElement() === this.projectExpandBtn()?.nativeElement - ); - }); - - if (targetIndex) { - this.keyManager?.setActiveItem(targetIndex); - } - } - } - - toggleExpandTags(): void { - const newState: boolean = !this.isTagsExpanded(); - this.storeTagListState(newState); - } - - toggleExpandTagsLeftRight(ev: KeyboardEvent): void { - if (ev.key === 'ArrowLeft' && this.isTagsExpanded()) { - this.storeTagListState(false); - } else if (ev.key === 'ArrowRight' && !this.isTagsExpanded()) { - this.storeTagListState(true); - } - } - - checkFocusTag(ev: KeyboardEvent): void { - if (ev.key === 'ArrowLeft' && this.tagExpandBtn()?.nativeElement) { - const targetIndex = this.navEntries().findIndex((value) => { - return ( - typeof value._getHostElement === 'function' && - value._getHostElement() === this.tagExpandBtn()?.nativeElement - ); - }); - - if (targetIndex) { - this.keyManager?.setActiveItem(targetIndex); - } - } - } - - startTour(id: TourId): void { - this._shepherdService.show(id); - } - - getGithubErrorUrl(): string { - if (!this._cachedIssueUrl) { - this._cachedIssueUrl = getGithubErrorUrl('', undefined, true); - } - return this._cachedIssueUrl; - } - - toggleProjectVisibility(project: Project): void { - this._store.dispatch(toggleHideFromMenu({ id: project.id })); - } - - async dropOnProjectList( - allItems: Project[], - ev: CdkDragDrop, - ): Promise { - if (ev.previousContainer === ev.container && ev.currentIndex !== ev.previousIndex) { - const tag = ev.item.data; - const allIds = allItems.map((p) => p.id); - const targetTagId = allIds[ev.currentIndex] as string; - if (targetTagId) { - const hiddenIds = await this._store - .select(selectUnarchivedHiddenProjectIds) - .pipe(first()) - .toPromise(); - const newIds = [...moveItemBeforeItem(allIds, tag.id, targetTagId), ...hiddenIds]; - this.projectService.updateOrder(newIds); - } - } - } - - dropOnTagList(allItems: Tag[], ev: CdkDragDrop): void { - if (ev.previousContainer === ev.container && ev.currentIndex !== ev.previousIndex) { - const tag = ev.item.data; - const allIds = allItems.map((p) => p.id); - const targetTagId = allIds[ev.currentIndex] as string; - if (targetTagId) { - // special today list should always be first - const newIds = [TODAY_TAG.id, ...moveItemBeforeItem(allIds, tag.id, targetTagId)]; - this.tagService.updateOrder(newIds); - } - } - } -} diff --git a/src/app/core/persistence/storage-keys.const.ts b/src/app/core/persistence/storage-keys.const.ts index 38d1bec015..ec9feb81e5 100644 --- a/src/app/core/persistence/storage-keys.const.ts +++ b/src/app/core/persistence/storage-keys.const.ts @@ -48,6 +48,10 @@ export enum LS { DONE_TASKS_HIDDEN = 'DONE_TASKS_HIDDEN', LATER_TODAY_TASKS_HIDDEN = 'LATER_TODAY_TASKS_HIDDEN', OVERDUE_TASKS_HIDDEN = 'OVERDUE_TASKS_HIDDEN', + + // Magic side nav + NAV_SIDEBAR_EXPANDED = 'SUP_NAV_SIDEBAR_EXPANDED', + NAV_SIDEBAR_WIDTH = 'SUP_NAV_SIDEBAR_WIDTH', } // SESSION STORAGE diff --git a/src/app/features/shepherd/shepherd-steps.const.ts b/src/app/features/shepherd/shepherd-steps.const.ts index 0c083f3ba3..ba6b6129ae 100644 --- a/src/app/features/shepherd/shepherd-steps.const.ts +++ b/src/app/features/shepherd/shepherd-steps.const.ts @@ -443,7 +443,7 @@ export const SHEPHERD_STEPS = ( return Promise.resolve(); }, when: nextOnObs( - layoutService.isShowSideNav$.pipe(filter((v) => !!v)), + layoutService.isShowMobileSideNav$.pipe(filter((v) => !!v)), shepherdService, ), }, @@ -498,7 +498,7 @@ export const SHEPHERD_STEPS = ( }, text: 'Open the menu (menu)', when: nextOnObs( - layoutService.isShowSideNav$.pipe(filter((v) => !!v)), + layoutService.isShowMobileSideNav$.pipe(filter((v) => !!v)), shepherdService, ), }, From 0d979f57412a184ee9c905d918eb89897c7e61d6 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 20:02:08 +0200 Subject: [PATCH 19/61] refactor(sideNav): improve 3 --- .../magic-nav-config.service.ts | 8 -------- .../core-ui/magic-side-nav/magic-side-nav.ts | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index e2a634198c..75eda0ba12 100644 --- a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -222,14 +222,6 @@ export class MagicNavConfigService { expandThreshold: 180, })); - // Expanded groups - readonly expandedGroupIds = computed>(() => { - const expanded = new Set(); - if (this._isProjectsExpanded()) expanded.add('projects'); - if (this._isTagsExpanded()) expanded.add('tags'); - return expanded; - }); - // Simple action handler onNavItemClick(item: NavItem): void { switch (item.type) { diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.ts index 4b3fac950e..df54bf08a8 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.ts @@ -49,6 +49,8 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { showMobileMenu = signal(false); // Track expanded groups as array for better signal change detection expandedGroups = signal([]); + // Merge service-controlled and local expanded ids for reactive checks + // Animate only for collapsed/expanded toggle animateWidth = signal(false); private _animateTimeoutId: number | null = null; @@ -166,18 +168,19 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { } toggleGroup(item: NavItem): void { - if (item.type === 'group' && item.children && item.children.length > 0) { - const groups = this.expandedGroups(); - if (groups.includes(item.id)) { - this.expandedGroups.set(groups.filter((g) => g !== item.id)); - } else { - this.expandedGroups.set([...groups, item.id]); - } + if (item.type !== 'group' || !item.children) { + return; } + const groups = this.expandedGroups(); + if (groups.includes(item.id)) { + this.expandedGroups.set(groups.filter((g) => g !== item.id)); + } else { + this.expandedGroups.set([...groups, item.id]); + } + console.log(item, this.expandedGroups()); } isGroupExpanded(item: NavItem): boolean { - if (item.type !== 'group') return false; return this.expandedGroups().includes(item.id); } From 0c741682c19d86f7bd93b6a2e0556c91c3984751 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 20:12:23 +0200 Subject: [PATCH 20/61] feat(sideNav): make more_vert button work --- .../nav-item-inner.component.html | 19 ++++----- .../nav-item-inner.component.ts | 41 ++++++++----------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html index c28cb00185..4c3d90c4e9 100644 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html @@ -11,7 +11,7 @@ mat-menu-item >
- {{ workContext()!.icon || defaultIcon() }} + {{ workContext()!.icon || defaultIcon() }} {{ nrOfOpenTasks() }}
- @if (showMoreButton()) { - - } + (() => { const wc = this.workContext(); return !!wc && wc.taskIds.length > 0; - } + }); - get isActiveContext(): boolean { + isActiveContext = computed(() => { const wc = this.workContext(); return !!wc && wc.id === this.activeWorkContextId(); - } + }); - get isHidden(): boolean { + isHidden = computed(() => { const wc = this.workContext(); return !!(wc as Project | null)?.isHiddenFromMenu; - } + }); - get isVariantNav(): boolean { - return this.variant() === 'nav'; - } + isVariantNav = computed(() => this.variant() === 'nav'); - get isCompact(): boolean { - return this.compact(); - } + isCompact = computed(() => this.compact()); focus(): void { const btn = this._routeBtn(); From aea969845261419c23e5ca72e49a4d729fc82a4b Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 20:30:40 +0200 Subject: [PATCH 21/61] refactor(sideNav): improve 4 --- src/app/core-ui/magic-side-nav/magic-nav-config.service.ts | 3 ++- .../nav-item-inner/nav-item-inner.component.html | 6 ++---- .../nav-item-inner/nav-item-inner.component.scss | 4 ++++ .../core-ui/magic-side-nav/nav-item/nav-item.component.ts | 1 + 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index 75eda0ba12..834c43605e 100644 --- a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -60,7 +60,6 @@ export class MagicNavConfigService { ...this._buildWorkContextItems(), // Separator - { type: 'separator', id: 'sep-1' }, // Main Routes { @@ -68,6 +67,8 @@ export class MagicNavConfigService { id: 'schedule', label: T.MH.SCHEDULE, icon: 'early_on', + svgIcon: 'early_on', + route: '/schedule', }, { diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html index 4c3d90c4e9..56aae0cd6c 100644 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.html @@ -10,10 +10,7 @@ routerLinkActive="isActiveRoute" mat-menu-item > -
- {{ workContext()!.icon || defaultIcon() }} - {{ nrOfOpenTasks() }} -
+ {{ workContext()!.icon || defaultIcon() }} {{ workContext()!.title }} + {{ nrOfOpenTasks() }} - - - - - - - - -} @else { - - @if (svgIcon()) { - - } @else if (icon()) { - {{ icon() }} - } - - @if (label()) { - - {{ label() | translate }} - - } - - @if (showChevron()) { - - - - } -} diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss deleted file mode 100644 index 6f664072ee..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.scss +++ /dev/null @@ -1,158 +0,0 @@ -@use 'styles/globals' as *; - -:host { - display: flex !important; - align-items: stretch; - position: relative; - width: 100%; - - * { - border: 1px solid red; - } - - button:first-of-type { - white-space: nowrap; - overflow: hidden; - flex: 1; - min-width: 0; - - .text { - overflow: hidden; - text-overflow: ellipsis; - // smooth collapse/expand for title - display: inline-block; - white-space: nowrap; - max-width: 100%; - transition: - opacity var(--transition-duration-m, 0.25s) var(--ani-standard-timing), - max-width var(--transition-duration-m, 0.25s) var(--ani-standard-timing), - margin var(--transition-duration-m, 0.25s) var(--ani-standard-timing); - } - - .badge { - display: none; - } - - .ico-wrapper { - position: relative; - } - } - - .additional-btn { - flex-shrink: 0; - width: 40px !important; - min-width: 40px !important; - } - - &.hasTasks { - button .badge { - display: block; - z-index: 10; - position: absolute; - line-height: 1; - left: 40px; - text-align: center; - bottom: 6px; - font-size: 10px; - padding: 0; - border-radius: 4px; - - // avoid affecting drag handle - pointer-events: none; - - border-color: var(--extra-border-color); - background: var(--bg-lighter); - } - } - - &.isHidden { - display: none !important; - } -} - -:host.isActiveContext button { - font-weight: normal; - - &.isActiveRoute { - font-weight: bold; - color: var(--palette-primary-800); - - mat-icon { - color: var(--palette-primary-800); - } - } -} - -body.isDarkTheme :host.isActiveContext button.isActiveRoute { - color: var(--c-primary); - - mat-icon { - color: var(--c-primary); - } -} - -// Styling when used inside magic-side-nav -:host.variant-nav { - .g-multi-btn-wrapper { - background-color: transparent; - } - - button[mat-menu-item] { - color: var(--sidebar-text); - padding: var(--s) var(--s2); - font-size: 14px; - - &:hover { - background-color: var(--sidebar-hover); - } - - &.isActiveRoute { - background-color: var(--sidebar-active); - color: var(--sidebar-active-text); - } - } - - .additional-btn { - color: var(--sidebar-text-secondary); - - &:hover { - background-color: var(--sidebar-hover); - color: var(--sidebar-text); - } - } -} - -// Smooth label show/hide for presentational rows -.nav-label { - display: inline-block; - overflow: hidden; - white-space: nowrap; - max-width: 100%; - transition: - opacity var(--transition-duration-m, 0.25s) var(--ani-standard-timing), - max-width var(--transition-duration-m, 0.25s) var(--ani-standard-timing), - margin var(--transition-duration-m, 0.25s) var(--ani-standard-timing); -} - -.nav-label.label-hidden, -.text.label-hidden { - opacity: 0; - max-width: 0; - margin-left: 0 !important; - pointer-events: none; -} - -.drag-handle { - /* Firefox 1.5-26 */ - position: relative; - @include grabCursor(); - - &:after { - content: ''; - position: absolute; - top: calc(-1 * var(--s)); - left: calc(-1 * var(--s)); - right: calc(-1 * var(--s)); - bottom: calc(-1 * var(--s)); - } -} diff --git a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts b/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts deleted file mode 100644 index 3f0d2539ae..0000000000 --- a/src/app/core-ui/magic-side-nav/nav-item-inner/nav-item-inner.component.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - ElementRef, - inject, - input, - viewChild, -} from '@angular/core'; -import { RouterLink, RouterModule } from '@angular/router'; - -import { - WorkContextCommon, - WorkContextType, -} from '../../../features/work-context/work-context.model'; -import { Project } from '../../../features/project/project.model'; -import { WorkContextMenuComponent } from '../../work-context-menu/work-context-menu.component'; -import { ContextMenuComponent } from '../../../ui/context-menu/context-menu.component'; -import { CdkDragPlaceholder } from '@angular/cdk/drag-drop'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { MatMenuItem } from '@angular/material/menu'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { selectAllDoneIds } from '../../../features/tasks/store/task.selectors'; -import { Store } from '@ngrx/store'; -import { TranslatePipe } from '@ngx-translate/core'; - -@Component({ - selector: 'nav-item-inner', - imports: [ - RouterLink, - RouterModule, - WorkContextMenuComponent, - ContextMenuComponent, - CdkDragPlaceholder, - MatIconButton, - MatIcon, - MatMenuItem, - TranslatePipe, - ], - templateUrl: './nav-item-inner.component.html', - styleUrl: './nav-item-inner.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - class: 'g-multi-btn-wrapper', - // eslint-disable-next-line @typescript-eslint/naming-convention - '[class.hasTasks]': 'workContextHasTasks()', - // eslint-disable-next-line @typescript-eslint/naming-convention - '[class.isActiveContext]': 'isActiveContext()', - // eslint-disable-next-line @typescript-eslint/naming-convention - '[class.isHidden]': 'isHidden()', - // eslint-disable-next-line @typescript-eslint/naming-convention - '[class.variant-nav]': 'isVariantNav()', - // eslint-disable-next-line @typescript-eslint/naming-convention - '[class.compact]': 'isCompact()', - }, - standalone: true, -}) -export class NavItemInnerComponent { - private readonly _store = inject(Store); - - // Mode selection - mode = input<'work' | 'row'>('work'); - - // Work context inputs - workContext = input(null); - type = input(null); - defaultIcon = input('folder_special'); - activeWorkContextId = input(''); - - // Variant styling to integrate into magic-side-nav without deep selectors - variant = input<'default' | 'nav'>('default'); - compact = input(false); - showMoreButton = input(true); - - // Presentational row inputs - label = input(undefined); - icon = input(undefined); - svgIcon = input(undefined); - showLabels = input(true); - showChevron = input(false); - - allUndoneTaskIds = toSignal(this._store.select(selectAllDoneIds), { initialValue: [] }); - nrOfOpenTasks = computed(() => { - const wc = this.workContext(); - if (!wc) return 0; - const allUndoneTaskIds = this.allUndoneTaskIds(); - return wc.taskIds.filter((tid) => !allUndoneTaskIds.includes(tid)).length; - }); - - private readonly _routeBtn = viewChild('routeBtn', { read: ElementRef }); - - workContextHasTasks = computed(() => { - const wc = this.workContext(); - return !!wc && wc.taskIds.length > 0; - }); - - isActiveContext = computed(() => { - const wc = this.workContext(); - return !!wc && wc.id === this.activeWorkContextId(); - }); - - isHidden = computed(() => { - const wc = this.workContext(); - return !!(wc as Project | null)?.isHiddenFromMenu; - }); - - isVariantNav = computed(() => this.variant() === 'nav'); - - isCompact = computed(() => this.compact()); - - focus(): void { - const btn = this._routeBtn(); - if (btn) { - btn.nativeElement.focus(); - } - } -} diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index b70e094fd7..53a8921f08 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -1,87 +1,210 @@ -@switch (item().type) { - @case ('workContext') { - - } +@if (mode() === 'work' && workContext()) { +
- @case ('route') { + + + + + + + + + +} @else { + + @if (container() === 'route') { - + @if (svgIcon()) { + + } @else if (icon()) { + {{ icon() }} + } + @if (label()) { + + {{ label() | translate }} + + } + @if (showChevron()) { + + + + } - } - - @case ('href') { + } @else if (container() === 'href') { - + @if (svgIcon()) { + + } @else if (icon()) { + {{ icon() }} + } + @if (label()) { + + {{ label() | translate }} + + } + @if (showChevron()) { + + + + } - } - - @case ('action') { + } @else if (container() === 'action' || container() === 'group') { - } + } @else { + + @if (svgIcon()) { + + } @else if (icon()) { + {{ icon() }} + } - @case ('group') { - + @if (label()) { + + {{ label() | translate }} + + } + + @if (showChevron()) { + + + + } } } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index dd7373310f..8986887e26 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -1,30 +1,125 @@ +@use 'styles/globals' as *; + :host { - display: contents; -} - -.nav-link, -.nav-child-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; + display: flex !important; + align-items: stretch; position: relative; + width: 100%; - &:hover { - background: var(--sidebar-hover); + button:first-of-type { + white-space: nowrap; + overflow: hidden; + flex: 1; + min-width: 0; + + .nav-label { + overflow: hidden; + text-overflow: ellipsis; + // smooth collapse/expand for title + display: inline-block; + white-space: nowrap; + max-width: 100%; + transition: + opacity var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + max-width var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + margin var(--transition-duration-m, 0.25s) var(--ani-standard-timing); + } + + .badge { + display: none; + } + + .ico-wrapper { + position: relative; + } } - &.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); + .additional-btn { + flex-shrink: 0; + width: 40px !important; + min-width: 40px !important; + } + + &.hasTasks { + button .badge { + display: block; + z-index: 10; + position: absolute; + line-height: 1; + left: 40px; + text-align: center; + bottom: 6px; + font-size: 10px; + padding: 0; + border-radius: 4px; + + // avoid affecting drag handle + pointer-events: none; + + border-color: var(--extra-border-color); + background: var(--bg-lighter); + } + } + + &.isHidden { + display: none !important; + } +} + +:host.isActiveContext button { + font-weight: normal; +} + +// Active state visuals are handled by .nav-link.active styles + +// Styling when used inside magic-side-nav: rely on global .nav-link/.nav-child-link +// Only keep adjustments for the additional button here +:host.variant-nav { + .g-multi-btn-wrapper { + background-color: transparent; + } + + .additional-btn { + color: var(--sidebar-text-secondary); + + &:hover { + background-color: var(--sidebar-hover); + color: var(--sidebar-text); + } + } +} + +// Smooth label show/hide for presentational rows +.nav-label { + display: inline-block; + overflow: hidden; + white-space: nowrap; + max-width: 100%; + transition: + opacity var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + max-width var(--transition-duration-m, 0.25s) var(--ani-standard-timing), + margin var(--transition-duration-m, 0.25s) var(--ani-standard-timing); +} + +.nav-label.label-hidden, +.text.label-hidden { + opacity: 0; + max-width: 0; + margin-left: 0 !important; + pointer-events: none; +} + +.drag-handle { + /* Firefox 1.5-26 */ + position: relative; + @include grabCursor(); + + &:after { + content: ''; + position: absolute; + top: calc(-1 * var(--s)); + left: calc(-1 * var(--s)); + right: calc(-1 * var(--s)); + bottom: calc(-1 * var(--s)); } } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts index 71fe78ad24..675799aad7 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts @@ -1,34 +1,130 @@ -import { Component, computed, input, output } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; -import { NavItemInnerComponent } from '../nav-item-inner/nav-item-inner.component'; -import { NavItem } from '../magic-side-nav.model'; +import { + ChangeDetectionStrategy, + Component, + computed, + ElementRef, + inject, + input, + output, + viewChild, +} from '@angular/core'; +import { RouterLink, RouterModule } from '@angular/router'; + +import { + WorkContextCommon, + WorkContextType, +} from '../../../features/work-context/work-context.model'; +import { Project } from '../../../features/project/project.model'; +import { WorkContextMenuComponent } from '../../work-context-menu/work-context-menu.component'; +import { ContextMenuComponent } from '../../../ui/context-menu/context-menu.component'; +import { CdkDragPlaceholder } from '@angular/cdk/drag-drop'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatMenuItem } from '@angular/material/menu'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { selectAllDoneIds } from '../../../features/tasks/store/task.selectors'; +import { Store } from '@ngrx/store'; +import { TranslatePipe } from '@ngx-translate/core'; @Component({ selector: 'nav-item', - standalone: true, - imports: [CommonModule, RouterModule, NavItemInnerComponent], + imports: [ + RouterLink, + RouterModule, + WorkContextMenuComponent, + ContextMenuComponent, + CdkDragPlaceholder, + MatIconButton, + MatIcon, + MatMenuItem, + TranslatePipe, + ], templateUrl: './nav-item.component.html', styleUrl: './nav-item.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'g-multi-btn-wrapper', + // eslint-disable-next-line @typescript-eslint/naming-convention + '[class.hasTasks]': 'workContextHasTasks()', + // eslint-disable-next-line @typescript-eslint/naming-convention + '[class.isActiveContext]': 'isActiveContext()', + // eslint-disable-next-line @typescript-eslint/naming-convention + '[class.isHidden]': 'isHidden()', + // eslint-disable-next-line @typescript-eslint/naming-convention + '[class.variant-nav]': 'isVariantNav()', + // eslint-disable-next-line @typescript-eslint/naming-convention + '[class.compact]': 'isCompact()', + }, + standalone: true, }) -export class SpNavItemComponent { - item = input.required(); +export class NavItemComponent { + private readonly _store = inject(Store); + + // Mode selection + mode = input<'work' | 'row'>('work'); + + // Container selection for non-work modes + container = input<'route' | 'href' | 'action' | 'group' | null>(null); + navRoute = input(undefined); + navHref = input(undefined); + expanded = input(false); + ariaExpanded = input(null); + ariaControls = input(null); + + // Work context inputs + workContext = input(null); + type = input(null); + defaultIcon = input('folder_special'); + activeWorkContextId = input(''); + + // Variant styling to integrate into magic-side-nav without deep selectors + variant = input<'default' | 'nav'>('default'); + compact = input(false); + showMoreButton = input(true); + + // Presentational row inputs + label = input(undefined); + icon = input(undefined); + svgIcon = input(undefined); showLabels = input(true); - isExpanded = input(false); - activeWorkContextId = input(null); - isChild = input(false); + showChevron = input(false); - itemClick = output(); + // Events + clicked = output(); - // TODO check if needed - linkClass = computed(() => (this.isChild() ? 'nav-child-link' : 'nav-link')); - hasChildren = computed(() => { - const i = this.item(); - return i.type === 'group' && !!i.children?.length; + allUndoneTaskIds = toSignal(this._store.select(selectAllDoneIds), { initialValue: [] }); + nrOfOpenTasks = computed(() => { + const wc = this.workContext(); + if (!wc) return 0; + const allUndoneTaskIds = this.allUndoneTaskIds(); + return wc.taskIds.filter((tid) => !allUndoneTaskIds.includes(tid)).length; }); - shouldShowTooltip = computed(() => !this.showLabels() && !!this.item().label); - onItemClick(): void { - this.itemClick.emit(this.item()); + private readonly _routeBtn = viewChild('routeBtn', { read: ElementRef }); + + workContextHasTasks = computed(() => { + const wc = this.workContext(); + return !!wc && wc.taskIds.length > 0; + }); + + isActiveContext = computed(() => { + const wc = this.workContext(); + return !!wc && wc.id === this.activeWorkContextId(); + }); + + isHidden = computed(() => { + const wc = this.workContext(); + return !!(wc as Project | null)?.isHiddenFromMenu; + }); + + isVariantNav = computed(() => this.variant() === 'nav'); + + isCompact = computed(() => this.compact()); + + focus(): void { + const btn = this._routeBtn(); + if (btn) { + btn.nativeElement.focus(); + } } } diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index 6e2f700471..545ac59968 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -1,22 +1,16 @@
- + @if (showLabels()) { @for (btn of item().additionalButtons; track btn.id) { @@ -47,13 +41,55 @@ class="nav-child-item" role="listitem" > - + @switch (child.type) { + @case ('workContext') { + + } + @case ('route') { + + } + @case ('href') { + + } + @case ('action') { + + } + } } diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts index 761580fef8..8c65d05cd9 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts @@ -4,8 +4,7 @@ import { MatIcon } from '@angular/material/icon'; import { MatIconButton } from '@angular/material/button'; import { MatTooltip } from '@angular/material/tooltip'; import { TranslatePipe } from '@ngx-translate/core'; -import { NavItemInnerComponent } from '../nav-item-inner/nav-item-inner.component'; -import { SpNavItemComponent } from '../nav-item/nav-item.component'; +import { NavItemComponent } from '../nav-item/nav-item.component'; import { NavGroupItem, NavItem } from '../magic-side-nav.model'; @Component({ @@ -17,13 +16,12 @@ import { NavGroupItem, NavItem } from '../magic-side-nav.model'; MatIconButton, MatTooltip, TranslatePipe, - NavItemInnerComponent, - SpNavItemComponent, + NavItemComponent, ], templateUrl: './nav-list.component.html', styleUrl: './nav-list.component.scss', }) -export class SpNavSectionComponent { +export class NavSectionComponent { item = input.required(); showLabels = input(true); isExpanded = input(false); From 43a2f35b9dc12e8dd625388920e05802ea1a00ab Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 8 Sep 2025 20:57:35 +0200 Subject: [PATCH 23/61] refactor(sideNav): properly order styles --- .../magic-side-nav/magic-side-nav.scss | 271 +----------------- .../nav-item/nav-item.component.scss | 112 ++++++++ .../nav-list/nav-list.component.scss | 51 ++++ 3 files changed, 165 insertions(+), 269 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.scss index e73dbc73e2..877fbb972f 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.scss @@ -182,62 +182,6 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); &.collapsed { // width controlled via inline style for smooth transitions - .nav-children { - display: none; - } - - .nav-link { - justify-content: center; - padding: 12px; - - &[title] { - // Ensure tooltip shows on hover for collapsed items - } - } - - .nav-icon { - margin: 0; - } - - // Multi-button wrapper in collapsed mode - .g-multi-btn-wrapper { - justify-content: center; - - .expand-btn { - justify-content: center; - padding: 12px; - position: relative; - - .nav-icon { - margin: 0; - } - - // Show a small indicator when section is expanded in collapsed mode - &.isExpanded::after { - content: ''; - position: absolute; - bottom: 2px; - right: 2px; - width: 4px; - height: 4px; - background-color: var(--sidebar-active-text); - border-radius: 50%; - } - } - - // Hide additional buttons in collapsed mode - .additional-btn { - display: none; - } - } - - // Fix side-nav-item wrapping issues in collapsed mode - ::ng-deep nav-item { - .additional-btn { - display: none !important; - } - } - .sidebar-toggle { right: auto; left: 50%; @@ -363,151 +307,6 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); opacity: 0.7; } -// Navigation links -.nav-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - position: relative; - - &:hover { - background: var(--sidebar-hover); - } - - &.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); - - .nav-icon { - color: var(--sidebar-active-text); - } - } - - &.expanded .nav-chevron { - transform: rotate(180deg); - } -} - -// Navigation icon -.nav-icon { - flex-shrink: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - color: var(--sidebar-text-secondary); - font-size: 18px; - line-height: 1; -} - -// Navigation label -.nav-label { - flex: 1; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -// Fix side-nav-item wrapping issues globally -::ng-deep nav-item { - display: flex !important; - align-items: stretch; - width: 100%; - - button:first-of-type { - white-space: nowrap; - overflow: hidden; - flex: 1; - min-width: 0; - - .nav-label { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .additional-btn { - flex-shrink: 0; - width: 40px !important; - min-width: 40px !important; - } -} - -// Navigation chevron -.nav-chevron { - flex-shrink: 0; - transition: transform 0.2s ease; - color: var(--sidebar-text-secondary); -} - -// Children navigation -.nav-children { - list-style: none; - padding: 0; - margin: 4px 0 0 0; - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease; - - &.visible { - max-height: 500px; - } -} - -.nav-child-item { - //margin-left: var(--s3); -} - -.nav-child-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s) var(--s2); - color: var(--sidebar-text-secondary); - text-decoration: none; - border-radius: 6px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - font-size: 14px; - - &:hover { - background: var(--sidebar-hover); - color: var(--sidebar-text); - } - - &.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); - - .nav-icon { - color: var(--sidebar-active-text); - } - } - - .nav-icon { - width: 20px; - height: 20px; - font-size: 16px; - } -} - // Resize handle .resize-handle { position: absolute; @@ -552,72 +351,6 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); transition: opacity 0.2s ease; } -// Multi-button wrapper styling for expandable sections with additional buttons -.g-multi-btn-wrapper { - display: flex; - align-items: center; - position: relative; +// Multi-button wrapper styling moved to nav-list component - .expand-btn { - flex: 1; - text-align: left; - - &.isExpanded .nav-icon { - transform: rotate(180deg); - } - } - - .additional-btn { - flex-shrink: 0; - width: 32px; - height: 32px; - min-width: 32px; - padding: 4px; - margin-left: var(--s); - color: var(--sidebar-text-secondary); - background: transparent; - border: none; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - background-color: var(--sidebar-hover); - color: var(--sidebar-text); - } - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } - } - - .title { - font-weight: 500; - } -} - -// Media queries -@media (max-width: $mobile-breakpoint) { - .nav-sidebar { - &.collapsed { - width: $sidebar-width-expanded; - - .nav-label, - .nav-chevron, - .nav-children.visible { - display: initial; - } - - .nav-link { - justify-content: flex-start; - padding: 10px 12px; - } - } - } -} - -// Removed deep overrides for side-nav-item; it now supports a 'nav' variant - -// Removed deep styling for nav-entry, nav-item and nav-row +// Media-query rules affecting link rows moved to nav-item component diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index 8986887e26..471d321513 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -123,3 +123,115 @@ bottom: calc(-1 * var(--s)); } } + +// Unified navigation row styles moved from parent +.nav-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s2) var(--s2); + color: var(--sidebar-text); + text-decoration: none; + border-radius: 8px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + position: relative; + + &:hover { + background: var(--sidebar-hover); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + + .nav-icon { + color: var(--sidebar-active-text); + } + } + + &.expanded .nav-chevron { + transform: rotate(180deg); + } +} + +.nav-child-link { + display: flex; + align-items: center; + gap: var(--s2); + padding: var(--s) var(--s2); + color: var(--sidebar-text-secondary); + text-decoration: none; + border-radius: 6px; + transition: all 0.2s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + font-size: 14px; + + &:hover { + background: var(--sidebar-hover); + color: var(--sidebar-text); + } + + &.active { + background: var(--sidebar-active); + color: var(--sidebar-active-text); + + .nav-icon { + color: var(--sidebar-active-text); + } + } + + .nav-icon { + width: 20px; + height: 20px; + font-size: 16px; + } +} + +// Navigation icon and chevron +.nav-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--sidebar-text-secondary); + font-size: 18px; + line-height: 1; +} + +.nav-chevron { + flex-shrink: 0; + transition: transform 0.2s ease; + color: var(--sidebar-text-secondary); +} + +// Collapsed state adjustments from parent sidebar +:host-context(.nav-sidebar.collapsed) { + .nav-link { + justify-content: center; + padding: 12px; + } + + .nav-icon { + margin: 0; + } +} + +@media (max-width: 768px) { + :host-context(.nav-sidebar.collapsed) { + .nav-link { + justify-content: flex-start; + padding: 10px 12px; + } + } +} diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss index a3c0297ba5..a35b05f522 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss @@ -6,6 +6,15 @@ display: flex; align-items: center; position: relative; + + .expand-btn { + flex: 1; + text-align: left; + + &.isExpanded .nav-icon { + transform: rotate(180deg); + } + } } .nav-link { @@ -56,6 +65,12 @@ background-color: var(--sidebar-hover); color: var(--sidebar-text); } + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } } .nav-children { @@ -70,3 +85,39 @@ max-height: 500px; } } + +// Collapsed state behaviors inherited from parent sidebar +:host-context(.nav-sidebar.collapsed) { + .g-multi-btn-wrapper { + justify-content: center; + + .expand-btn { + justify-content: center; + padding: 12px; + position: relative; + + .nav-icon { + margin: 0; + } + + &.isExpanded::after { + content: ''; + position: absolute; + bottom: 2px; + right: 2px; + width: 4px; + height: 4px; + background-color: var(--sidebar-active-text); + border-radius: 50%; + } + } + + .additional-btn { + display: none; + } + } + + .nav-children { + display: none; + } +} From 5bded8ebf68e5fb2494a60c0ea8ac5a6bf3ca4b0 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 09:43:27 +0200 Subject: [PATCH 24/61] refactor(sideNav): further improve --- src/app/app.component.ts | 2 +- ...nav.html => magic-side-nav.component.html} | 12 +- ...nav.scss => magic-side-nav.component.scss} | 0 ...ide-nav.ts => magic-side-nav.component.ts} | 43 ++++--- .../nav-item/nav-item.component.html | 6 +- .../nav-item/nav-item.component.ts | 2 + .../nav-list/nav-list.component.ts | 3 +- src/app/util/ls-util.ts | 108 ++++++++++++++++++ 8 files changed, 150 insertions(+), 26 deletions(-) rename src/app/core-ui/magic-side-nav/{magic-side-nav.html => magic-side-nav.component.html} (90%) rename src/app/core-ui/magic-side-nav/{magic-side-nav.scss => magic-side-nav.component.scss} (100%) rename src/app/core-ui/magic-side-nav/{magic-side-nav.ts => magic-side-nav.component.ts} (89%) create mode 100644 src/app/util/ls-util.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a4a9343c44..aaa0ddd0b6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -46,7 +46,7 @@ import { warpAnimation, warpInAnimation } from './ui/animations/warp.ani'; import { GlobalConfigState } from './features/config/global-config.model'; import { AddTaskBarComponent } from './features/tasks/add-task-bar/add-task-bar.component'; import { Dir } from '@angular/cdk/bidi'; -import { MagicSideNavComponent } from './core-ui/magic-side-nav/magic-side-nav'; +import { MagicSideNavComponent } from './core-ui/magic-side-nav/magic-side-nav.component'; import { MagicNavConfigService } from './core-ui/magic-side-nav/magic-nav-config.service'; import { MainHeaderComponent } from './core-ui/main-header/main-header.component'; import { BannerComponent } from './core/banner/banner/banner.component'; diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.html b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html similarity index 90% rename from src/app/core-ui/magic-side-nav/magic-side-nav.html rename to src/app/core-ui/magic-side-nav/magic-side-nav.component.html index 0cc3b800e5..a553a9b059 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html @@ -87,7 +87,7 @@ > } @@ -120,7 +120,7 @@ [icon]="item.icon" [svgIcon]="item.svgIcon" [label]="item.label" - [showLabels]="isExpanded() || (isMobile() && showMobileMenu())" + [showLabels]="showText()" [compact]="false" (clicked)="onItemClick(item)" > @@ -133,7 +133,7 @@ [icon]="item.icon" [svgIcon]="item.svgIcon" [label]="item.label" - [showLabels]="isExpanded() || (isMobile() && showMobileMenu())" + [showLabels]="showText()" [compact]="false" (clicked)="onItemClick(item)" > @@ -145,7 +145,7 @@ [icon]="item.icon" [svgIcon]="item.svgIcon" [label]="item.label" - [showLabels]="isExpanded() || (isMobile() && showMobileMenu())" + [showLabels]="showText()" [compact]="false" (clicked)="onItemClick(item)" > diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss similarity index 100% rename from src/app/core-ui/magic-side-nav/magic-side-nav.scss rename to src/app/core-ui/magic-side-nav/magic-side-nav.component.scss diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.component.ts similarity index 89% rename from src/app/core-ui/magic-side-nav/magic-side-nav.ts rename to src/app/core-ui/magic-side-nav/magic-side-nav.component.ts index 2f82a10740..a041304e69 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.ts @@ -18,6 +18,7 @@ import { NavSectionComponent } from './nav-list/nav-list.component'; import { NavItem } from './magic-side-nav.model'; import { LS } from '../../core/persistence/storage-keys.const'; import { MagicNavConfigService } from './magic-nav-config.service'; +import { readBoolLS, readNumberLSBounded } from '../../util/ls-util'; const COLLAPSED_WIDTH = 64; @@ -25,8 +26,8 @@ const COLLAPSED_WIDTH = 64; selector: 'magic-side-nav', standalone: true, imports: [CommonModule, RouterModule, NavItemComponent, NavSectionComponent], - templateUrl: './magic-side-nav.html', - styleUrl: './magic-side-nav.scss', + templateUrl: './magic-side-nav.component.html', + styleUrl: './magic-side-nav.component.scss', host: { '[style.width.px]': 'hostWidthSignal()', '[class.animate]': 'animateWidth()', @@ -72,6 +73,11 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // Host width as computed signal: don't reserve space on mobile overlay readonly hostWidthSignal = computed(() => (this.isMobile() ? 0 : this.sidebarWidth())); + // Commonly used derived state for template readability + readonly showText = computed( + () => this.isExpanded() || (this.isMobile() && this.showMobileMenu()), + ); + // Keep stable references for event listeners to ensure proper cleanup private readonly _onDrag: (event: MouseEvent) => void = (event: MouseEvent) => this._handleDrag(event); @@ -103,21 +109,29 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { ngOnInit(): void { // Load saved expansion state or default to config value - const savedExpanded = localStorage.getItem(LS.NAV_SIDEBAR_EXPANDED); - const initialExpanded = - savedExpanded !== null ? savedExpanded === 'true' : this.config().expandedByDefault; - + const initialExpanded = readBoolLS( + LS.NAV_SIDEBAR_EXPANDED, + this.config().expandedByDefault, + ); this.isExpanded.set(initialExpanded); - this.currentWidth.set(this.config().defaultWidth); - // Load saved width from localStorage - const savedWidth = localStorage.getItem(LS.NAV_SIDEBAR_WIDTH); - if (savedWidth) { - const minW = this.config().minWidth; - const maxW = this.config().maxWidth; - const width = Math.max(minW, Math.min(maxW, parseInt(savedWidth))); - this.currentWidth.set(width); + // Load saved width from localStorage or default + const bounded = readNumberLSBounded( + LS.NAV_SIDEBAR_WIDTH, + this.config().minWidth, + this.config().maxWidth, + ); + this.currentWidth.set(bounded ?? this.config().defaultWidth); + + // Initialize expanded groups from stored flags (projects/tags) + const nextExpanded: string[] = []; + if (readBoolLS(LS.IS_PROJECT_LIST_EXPANDED, true)) { + nextExpanded.push('projects'); } + if (readBoolLS(LS.IS_TAG_LIST_EXPANDED, true)) { + nextExpanded.push('tags'); + } + this.expandedGroups.set(nextExpanded); this._checkScreenSize(); } @@ -177,7 +191,6 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { } else { this.expandedGroups.set([...groups, item.id]); } - console.log(item, this.expandedGroups()); } isGroupExpanded(item: NavItem): boolean { diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index 53a8921f08..1e8830dec4 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -8,7 +8,7 @@ #routeBtn [routerLink]="[type() === 'TAG' ? 'tag' : 'project', workContext()!.id, 'tasks']" routerLinkActive="active" - [class]="compact() ? 'nav-child-link' : 'nav-link'" + [class]="rowClass()" [attr.title]="!showLabels() ? workContext()!.title : null" mat-menu-item > @@ -53,7 +53,7 @@ [routerLink]="navRoute()" routerLinkActive="active" mat-menu-item - [class]="compact() ? 'nav-child-link' : 'nav-link'" + [class]="rowClass()" [class.expanded]="expanded()" [attr.title]="!showLabels() ? (label() | translate) : null" (click)="clicked.emit()" @@ -95,7 +95,7 @@ target="_blank" mat-menu-item rel="noopener noreferrer" - [class]="compact() ? 'nav-child-link' : 'nav-link'" + [class]="rowClass()" [class.expanded]="expanded()" [attr.title]="!showLabels() ? (label() | translate) : null" (click)="clicked.emit()" diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts index 675799aad7..60b63f690a 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts @@ -90,6 +90,8 @@ export class NavItemComponent { showChevron = input(false); // Events + // Common row class for compact/default variants + readonly rowClass = computed(() => (this.compact() ? 'nav-child-link' : 'nav-link')); clicked = output(); allUndoneTaskIds = toSignal(this._store.select(selectAllDoneIds), { initialValue: [] }); diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts index 8c65d05cd9..88e930a78a 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts @@ -1,4 +1,4 @@ -import { Component, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIcon } from '@angular/material/icon'; import { MatIconButton } from '@angular/material/button'; @@ -20,6 +20,7 @@ import { NavGroupItem, NavItem } from '../magic-side-nav.model'; ], templateUrl: './nav-list.component.html', styleUrl: './nav-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NavSectionComponent { item = input.required(); diff --git a/src/app/util/ls-util.ts b/src/app/util/ls-util.ts new file mode 100644 index 0000000000..a47dd16011 --- /dev/null +++ b/src/app/util/ls-util.ts @@ -0,0 +1,108 @@ +/** + * localStorage utility functions for Super Productivity + */ + +/** + * Check if a key exists in localStorage + */ +export const lsHasKey = (key: string): boolean => { + return localStorage.getItem(key) !== null; +}; + +/** + * Clear localStorage completely + */ +export const clearLS = (): void => { + localStorage.clear(); +}; + +/** + * Get item from localStorage with default value + */ +export const lsGetItem = ( + key: string, + defaultValue?: T, +): T | string | null => { + const item = localStorage.getItem(key); + return item ?? defaultValue ?? null; +}; + +/** + * Get item from localStorage and parse as number, with optional default + */ +export const lsGetNumber = (key: string, defaultValue = 0): number => { + const item = localStorage.getItem(key); + return item ? +item : defaultValue; +}; + +/** + * Get item from localStorage and parse as boolean, with optional default + */ +export const lsGetBoolean = (key: string, defaultValue = false): boolean => { + const item = localStorage.getItem(key); + if (item === null) return defaultValue; + return item === 'true'; +}; + +/** + * Get item from localStorage and parse as JSON, with optional default + */ +export const lsGetJSON = (key: string, defaultValue?: T): T | null => { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : (defaultValue ?? null); + } catch { + return defaultValue ?? null; + } +}; + +/** + * Set item in localStorage + */ +export const lsSetItem = (key: string, value: string | number | boolean): void => { + localStorage.setItem(key, String(value)); +}; + +/** + * Set item in localStorage as JSON + */ +export const lsSetJSON = (key: string, value: unknown): void => { + localStorage.setItem(key, JSON.stringify(value)); +}; + +/** + * Remove item from localStorage + */ +export const lsRemoveItem = (key: string): void => { + localStorage.removeItem(key); +}; + +/** + * Remove multiple items from localStorage + */ +export const lsRemoveItems = (keys: string[]): void => { + keys.forEach((key) => localStorage.removeItem(key)); +}; + +/** + * Get boolean value from localStorage with fallback + */ +export const readBoolLS = (key: string, fallback: boolean): boolean => { + const v = localStorage.getItem(key); + return v === null ? fallback : v === 'true'; +}; + +/** + * Get number from localStorage with bounds checking + */ +export const readNumberLSBounded = ( + key: string, + min: number, + max: number, +): number | null => { + const v = localStorage.getItem(key); + if (v == null) return null; + const n = parseInt(v, 10); + if (Number.isNaN(n)) return null; + return Math.max(min, Math.min(max, n)); +}; From 2fd9e7cce9a09f8e575cbf01f8e24fc3f34fffc1 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 10:06:56 +0200 Subject: [PATCH 25/61] feat(sideNav): help as mat-menu --- .../magic-nav-config.service.ts | 4 +-- .../magic-side-nav.component.html | 7 ++++ .../magic-side-nav.component.ts | 15 +++++++- .../magic-side-nav/magic-side-nav.model.ts | 10 +++++- .../nav-item/nav-item.component.html | 1 + .../nav-item/nav-item.component.ts | 5 ++- .../nav-mat-menu/nav-mat-menu.component.html | 35 +++++++++++++++++++ .../nav-mat-menu/nav-mat-menu.component.scss | 4 +++ .../nav-mat-menu/nav-mat-menu.component.ts | 34 ++++++++++++++++++ 9 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.html create mode 100644 src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.scss create mode 100644 src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.ts diff --git a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index 834c43605e..d830c42a7a 100644 --- a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -136,9 +136,9 @@ export class MagicNavConfigService { route: '/scheduled-list', }, - // Help Menu + // Help Menu (rendered as mat-menu) { - type: 'group', + type: 'menu', id: 'help', label: T.MH.HELP, icon: 'help_center', diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html index a553a9b059..e0faaa1671 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html @@ -94,6 +94,13 @@ > } + @case ('menu') { + + } @default { diff --git a/src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.scss b/src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.scss new file mode 100644 index 0000000000..d14a46498d --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.scss @@ -0,0 +1,4 @@ +/* Reuse existing nav-link and nav-icon classes from side nav styles */ +:host { + display: contents; +} diff --git a/src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.ts b/src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.ts new file mode 100644 index 0000000000..0d629f069f --- /dev/null +++ b/src/app/core-ui/magic-side-nav/nav-mat-menu/nav-mat-menu.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatIcon } from '@angular/material/icon'; +import { RouterModule } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; +import { NavItem, NavMenuItem } from '../magic-side-nav.model'; +import { NavItemComponent } from '../nav-item/nav-item.component'; + +@Component({ + selector: 'nav-mat-menu', + standalone: true, + imports: [ + CommonModule, + MatMenuModule, + MatIcon, + RouterModule, + TranslatePipe, + NavItemComponent, + ], + templateUrl: './nav-mat-menu.component.html', + styleUrl: './nav-mat-menu.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NavMatMenuComponent { + item = input.required(); + showLabels = input(true); + + itemClick = output(); + + onChildClick(child: NavItem): void { + this.itemClick.emit(child); + } +} From 21900c999f1f00973ee79f40079ee190aeb92a21 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 10:10:56 +0200 Subject: [PATCH 26/61] refactor(sideNav): simplify --- .../magic-side-nav/nav-item/nav-item.component.html | 4 ++-- .../magic-side-nav/nav-item/nav-item.component.ts | 9 +++------ .../magic-side-nav/nav-list/nav-list.component.html | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index 304258fc98..7731963841 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -133,11 +133,11 @@ } @else if (container() === 'action' || container() === 'group') { } @else { @if (svgIcon()) { } @else if (icon()) { - {{ icon() }} + + {{ icon() }} + } @if (label()) { @@ -193,19 +178,5 @@ {{ label() | translate }} } - - @if (showChevron()) { - - - - } } } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index 807f6ed5f8..ee2358ef6e 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -215,6 +215,14 @@ color: var(--sidebar-text-secondary); } +// Rotate the expand_more icon for group headers instead of showing a separate chevron +.nav-link .expand-icon { + transition: transform 0.2s ease; +} +.nav-link.expanded .expand-icon { + transform: rotate(180deg); +} + // Collapsed state adjustments from parent sidebar :host-context(.nav-sidebar.collapsed) { .nav-link { diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts index 05fb7fc234..3994067fee 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.ts @@ -87,7 +87,6 @@ export class NavItemComponent { icon = input(undefined); svgIcon = input(undefined); showLabels = input(true); - showChevron = input(false); // Optional: menu trigger for dropdown menuTriggerFor = input(null); diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index fd58520091..b5de38cf29 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -7,7 +7,7 @@ [svgIcon]="item().svgIcon" [label]="item().label" [showLabels]="showLabels()" - [showChevron]="item().children && item().children.length > 0 && showLabels()" + [showChevron]="false" (clicked)="onHeaderClick()" > From 833ba04c9b12e9e9d8f33cf92a96654e6675ad21 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 11:01:12 +0200 Subject: [PATCH 29/61] refactor(sideNav): cleanup unused styles --- .../magic-side-nav.component.scss | 31 +--------- .../nav-item/nav-item.component.scss | 10 +--- .../nav-list/nav-list.component.scss | 60 ------------------- 3 files changed, 3 insertions(+), 98 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index 877fbb972f..7a8e35e4e0 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -141,24 +141,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); -webkit-backdrop-filter: blur(20px) saturate(180%); // Safari support } - // Threshold zone visual feedback - //&.collapse-zone { - // border-left: 3px solid #ef4444; - // box-shadow: inset 3px 0 0 rgba(239, 68, 68, 0.1); - // - // .resize-handle { - // background-color: #ef4444 !important; - // } - //} - - &.expand-zone { - border-left: 3px solid var(--c-primary); - box-shadow: inset 3px 0 0 rgba(16, 185, 129, 0.1); - - .resize-handle { - background-color: var(--c-primary) !important; - } - } + // removed unused expand/collapse zone visuals // Scrollbar styling &::-webkit-scrollbar { @@ -239,17 +222,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); right: auto; } - &.collapse-zone { - border-left: none; - border-right: 3px solid #ef4444; - box-shadow: inset -3px 0 0 rgba(239, 68, 68, 0.1); - } - - &.expand-zone { - border-left: none; - border-right: 3px solid #10b981; - box-shadow: inset -3px 0 0 rgba(16, 185, 129, 0.1); - } + // removed unused collapse/expand zone visuals for right-position } // Note: Theme colors are handled by CSS variables above diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index ee2358ef6e..b9c2014d9f 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -153,10 +153,6 @@ color: var(--sidebar-active-text); } } - - &.expanded .nav-chevron { - transform: rotate(180deg); - } } .nav-child-link { @@ -209,11 +205,7 @@ line-height: 1; } -.nav-chevron { - flex-shrink: 0; - transition: transform 0.2s ease; - color: var(--sidebar-text-secondary); -} +// Removed legacy chevron styles; we rotate expand_more icon instead // Rotate the expand_more icon for group headers instead of showing a separate chevron .nav-link .expand-icon { diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss index a35b05f522..6f49b37b73 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss @@ -6,45 +6,6 @@ display: flex; align-items: center; position: relative; - - .expand-btn { - flex: 1; - text-align: left; - - &.isExpanded .nav-icon { - transform: rotate(180deg); - } - } -} - -.nav-link { - display: flex; - align-items: center; - gap: var(--s2); - padding: var(--s2) var(--s2); - color: var(--sidebar-text); - text-decoration: none; - border-radius: 8px; - transition: all 0.2s ease; - cursor: pointer; - background: transparent; - border: none; - width: 100%; - text-align: left; - position: relative; - - &:hover { - background: var(--sidebar-hover); - } - - &.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); - } - - &.expanded .nav-chevron { - transform: rotate(180deg); - } } .additional-btn { @@ -91,27 +52,6 @@ .g-multi-btn-wrapper { justify-content: center; - .expand-btn { - justify-content: center; - padding: 12px; - position: relative; - - .nav-icon { - margin: 0; - } - - &.isExpanded::after { - content: ''; - position: absolute; - bottom: 2px; - right: 2px; - width: 4px; - height: 4px; - background-color: var(--sidebar-active-text); - border-radius: 50%; - } - } - .additional-btn { display: none; } From 648687a8b132b690229d2c479929a75f5535c36e Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 11:19:48 +0200 Subject: [PATCH 30/61] feat(sideNav): improve collapsed style --- .../nav-item/nav-item.component.scss | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index b9c2014d9f..5ab3fd270e 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -40,18 +40,25 @@ min-width: 40px !important; } + &:hover .badge { + display: none !important; + } + &.hasTasks { button .badge { display: block; z-index: 10; position: absolute; line-height: 1; - left: 40px; text-align: center; - bottom: 6px; font-size: 10px; padding: 0; border-radius: 4px; + left: auto; + bottom: auto; + right: 16px; + top: 50%; + transform: translateY(-50%); // avoid affecting drag handle pointer-events: none; @@ -211,6 +218,7 @@ .nav-link .expand-icon { transition: transform 0.2s ease; } + .nav-link.expanded .expand-icon { transform: rotate(180deg); } @@ -230,6 +238,22 @@ .additional-btn { display: none !important; } + + // Hide Material's menu item text wrapper (keeps icon centered, avoids jump) + a:first-of-type ::ng-deep .mat-mdc-menu-item-text, + button:first-of-type ::ng-deep .mat-mdc-menu-item-text { + display: none !important; + } + + &.hasTasks { + button .badge { + left: 50%; + bottom: 6px; + right: auto; + top: auto; + transform: translateX(-50%); + } + } } @media (max-width: 768px) { From cd1ed4a9e1e023af40ae96d82d4be1f73997571a Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 11:35:32 +0200 Subject: [PATCH 31/61] feat(sideNav): make early on icon a little smaller --- src/assets/icons/early-on.svg | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/assets/icons/early-on.svg b/src/assets/icons/early-on.svg index 9bcd7c0c93..520c58ac95 100644 --- a/src/assets/icons/early-on.svg +++ b/src/assets/icons/early-on.svg @@ -1,7 +1,5 @@ - - + + + + From 1919d703628ece1ff5f2a0ff763a206b054846b0 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 12:07:07 +0200 Subject: [PATCH 32/61] feat(sideNav): improve nav active styles --- .../magic-side-nav.component.scss | 11 ++------ .../nav-item/nav-item.component.html | 6 ++++ .../nav-item/nav-item.component.scss | 28 ++++++++++++++++--- src/styles/_css-variables.scss | 2 ++ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index 7a8e35e4e0..a1b6670285 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -17,17 +17,10 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); // Use app's CSS variables for consistent theming --sidebar-bg: var(--palette-background-card); --sidebar-border: var(--divider-color); - --sidebar-text: rgba( - var(--palette-foreground-text), - var(--palette-foreground-text-alpha) - ); - --sidebar-text-secondary: rgba( - var(--palette-foreground-secondary-text), - var(--palette-foreground-secondary-text-alpha) - ); + --sidebar-hover: var(--palette-background-hover); - --sidebar-active: var(--c-primary-10, rgba(var(--palette-primary-500-rgb), 0.04)); --sidebar-active-text: var(--palette-primary-800); + --mat-menu-item-spacing: 4px; // Dark theme overrides body.isDarkTheme & { diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index b0876352f5..22a5e3129e 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -62,6 +62,8 @@ } @else if (icon()) { @@ -97,6 +99,8 @@ } @else if (icon()) { @@ -132,6 +136,8 @@ } @else if (icon()) { diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index 5ab3fd270e..f69a574d9d 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -53,7 +53,6 @@ text-align: center; font-size: 10px; padding: 0; - border-radius: 4px; left: auto; bottom: auto; right: 16px; @@ -62,9 +61,6 @@ // avoid affecting drag handle pointer-events: none; - - border-color: var(--extra-border-color); - background: var(--bg-lighter); } } @@ -254,6 +250,30 @@ transform: translateX(-50%); } } + + // Unified navigation row styles moved from parent + .nav-link { + &:hover { + background: var(--sidebar-hover); + } + + &.active { + color: var(--sidebar-active-text); + border-radius: 48px; + background: var(--sidebar-active); + border-top: 1px solid var(--separator-color); + border-bottom: 1px solid var(--separator-color); + + &::after { + position: absolute; + inset: 4px; + } + + .nav-icon { + color: var(--sidebar-active-text); + } + } + } } @media (max-width: 768px) { diff --git a/src/styles/_css-variables.scss b/src/styles/_css-variables.scss index 17398423fc..daf8220c6f 100644 --- a/src/styles/_css-variables.scss +++ b/src/styles/_css-variables.scss @@ -333,6 +333,7 @@ body { // Navigation & Panel backgrounds --sidebar-bg: var(--bg); + --sidebar-active: rgba(255, 255, 255, 0.5); --right-panel-bg: #fff; // Text colors @@ -448,6 +449,7 @@ body.isDarkTheme { // Navigation & Panel backgrounds --sidebar-bg: var(--dark1); + --sidebar-active: rgba(255, 255, 255, 0.075); --right-panel-bg: var(--dark5); // Text colors From 38f983b30aad240b059c9ad53d987c86f8773743 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 12:11:40 +0200 Subject: [PATCH 33/61] feat(sideNav): improve nav active styles --- .../nav-item/nav-item.component.html | 8 ++-- .../nav-item/nav-item.component.scss | 46 +++++++++---------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index 22a5e3129e..788e2181eb 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -12,9 +12,11 @@ [attr.title]="!showLabels() ? workContext()!.title : null" mat-menu-item > - {{ workContext()!.icon || defaultIcon() }} - + {{ workContext()!.icon || defaultIcon() }} Date: Tue, 9 Sep 2025 12:24:14 +0200 Subject: [PATCH 34/61] feat(sideNav): improve task count style --- .../nav-item/nav-item.component.html | 3 +- .../nav-item/nav-item.component.scss | 55 +++++++------------ 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index 788e2181eb..08888ed563 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -24,9 +24,10 @@ > {{ workContext()!.title }} - {{ nrOfOpenTasks() }} +
{{ nrOfOpenTasks() }}
+ } @@ -171,7 +171,7 @@ class="resize-handle" [class.position-left]="config().position !== 'right'" [class.position-right]="config().position === 'right'" - [class.collapsed-handle]="!isExpanded()" + [class.compactMode-handle]="!isFullMode()" (mousedown)="onResizeStart($event)" [attr.aria-label]="'Resize sidebar'" role="separator" diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index a1b6670285..8c0228004f 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -28,7 +28,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); } } -// Enable host width transition only when toggling collapsed/expanded +// Enable host width transition only when toggling compactMode/fullMode :host(.animate) { transition: width $transition-duration $transition-timing; } @@ -154,8 +154,8 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); } } - // Collapsed state (desktop) - &.collapsed { + // CompactMode state (desktop) + &.compactMode { // width controlled via inline style for smooth transitions .sidebar-toggle { @@ -164,8 +164,8 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); transform: translateX(-50%); } - // Show resize handle in collapsed mode but style differently - .resize-handle.collapsed-handle { + // Show resize handle in compactMode but style differently + .resize-handle.compactMode-handle { display: block; width: 8px; right: -4px; diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.component.ts index 462e500b76..da5a7511d1 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.ts @@ -59,14 +59,14 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { mobileVisible = input(null); mobileVisibleChange = output(); - isExpanded = signal(true); + isFullMode = signal(true); isMobile = signal(false); showMobileMenu = signal(false); // Track expanded groups as array for better signal change detection expandedGroups = signal([]); // Merge service-controlled and local expanded ids for reactive checks - // Animate only for collapsed/expanded toggle + // Animate only for compactMode/fullMode toggle animateWidth = signal(false); private _animateTimeoutId: number | null = null; @@ -80,7 +80,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // Computed values sidebarWidth = computed(() => { if (this.isMobile()) return 260; - if (!this.isExpanded()) return COLLAPSED_WIDTH; + if (!this.isFullMode()) return COLLAPSED_WIDTH; return this.currentWidth(); }); @@ -89,7 +89,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // Commonly used derived state for template readability readonly showText = computed( - () => this.isExpanded() || (this.isMobile() && this.showMobileMenu()), + () => this.isFullMode() || (this.isMobile() && this.showMobileMenu()), ); // Keep stable references for event listeners to ensure proper cleanup @@ -122,12 +122,12 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { } ngOnInit(): void { - // Load saved expansion state or default to config value - const initialExpanded = readBoolLS( + // Load saved fullMode/compactMode state or default to config value + const initialFullMode = readBoolLS( LS.NAV_SIDEBAR_EXPANDED, - this.config().expandedByDefault, + this.config().fullModeByDefault, ); - this.isExpanded.set(initialExpanded); + this.isFullMode.set(initialFullMode); // Load saved width from localStorage or default const bounded = readNumberLSBounded( @@ -172,7 +172,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { if (currentMobile) { this.showMobileMenu.set(false); } else { - this.isExpanded.set(this.config().expandedByDefault); + this.isFullMode.set(this.config().fullModeByDefault); } } } @@ -182,10 +182,10 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { this.showMobileMenu.update((show) => !show); } else { this._enableWidthAnimation(); - const newExpanded = !this.isExpanded(); - this.isExpanded.set(newExpanded); - // Save expansion state to localStorage - localStorage.setItem(LS.NAV_SIDEBAR_EXPANDED, newExpanded.toString()); + const newFullMode = !this.isFullMode(); + this.isFullMode.set(newFullMode); + // Save fullMode/compactMode state to localStorage + localStorage.setItem(LS.NAV_SIDEBAR_EXPANDED, newFullMode.toString()); // handled internally } } @@ -265,7 +265,7 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { this.isResizing.set(true); this.startX.set(event.clientX); - this.startWidth.set(this.isExpanded() ? this.currentWidth() : COLLAPSED_WIDTH); + this.startWidth.set(this.isFullMode() ? this.currentWidth() : COLLAPSED_WIDTH); document.addEventListener('mousemove', this._onDrag); document.addEventListener('mouseup', this._onDragEnd); @@ -291,14 +291,14 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { const maxW = cfg.maxWidth; // Handle seamless mode transitions - if (this.isExpanded()) { - // Currently expanded - check for collapse threshold + if (this.isFullMode()) { + // Currently fullMode - check for compactMode threshold if (potentialWidth < collapseTh) { - // Auto-collapse without releasing mouse - this.isExpanded.set(false); + // Auto-switch to compactMode without releasing mouse + this.isFullMode.set(false); // handled internally - // Reset starting point for continued dragging from collapsed state + // Reset starting point for continued dragging from compactMode state this.startWidth.set(COLLAPSED_WIDTH); this.startX.set(event.clientX); @@ -308,23 +308,23 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // No visual feedback when approaching collapse threshold - // Normal resize when expanded + // Normal resize when fullMode const newWidth = Math.max(minW, Math.min(maxW, potentialWidth)); this.currentWidth.set(newWidth); } else { - // Currently collapsed - check for expand threshold - const collapsedWidth = COLLAPSED_WIDTH; - const draggedWidth = collapsedWidth + deltaX; + // Currently compactMode - check for fullMode threshold + const compactModeWidth = COLLAPSED_WIDTH; + const draggedWidth = compactModeWidth + deltaX; if (draggedWidth > expandTh) { - // Auto-expand without releasing mouse - this.isExpanded.set(true); + // Auto-switch to fullMode without releasing mouse + this.isFullMode.set(true); const newWidth = expandTh + 20; // Start slightly beyond threshold this.currentWidth.set(newWidth); // handled internally - // Reset starting point for continued dragging from expanded state + // Reset starting point for continued dragging from fullMode state this.startWidth.set(this.currentWidth()); this.startX.set(event.clientX); @@ -348,8 +348,8 @@ export class MagicSideNavComponent implements OnInit, OnDestroy { // No visual feedback to reset - // Save width to localStorage (only save if expanded) - if (this.isExpanded()) { + // Save width to localStorage (only save if fullMode) + if (this.isFullMode()) { localStorage.setItem(LS.NAV_SIDEBAR_WIDTH, this.currentWidth().toString()); } } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts b/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts index 2dc9d18430..eccc6b8e8b 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.model.ts @@ -89,7 +89,7 @@ export interface NavAdditionalButton { } export interface NavConfig { items: NavItem[]; - expandedByDefault: boolean; + fullModeByDefault: boolean; showLabels: boolean; mobileBreakpoint: number; position: 'left' | 'right'; diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index 2b8b054a30..c8e234b4fe 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -209,12 +209,12 @@ } // Collapsed state adjustments from parent sidebar -:host-context(.nav-sidebar.collapsed) { +:host-context(.nav-sidebar.compactMode) { .nav-link { justify-content: center; padding: 12px; - // Keep dedicated collapsed hover/active feel + // Keep dedicated compactMode hover/active feel &:hover { background: var(--sidebar-hover); } @@ -241,7 +241,7 @@ margin: 0; } - // Hide the overflow menu button entirely when collapsed + // Hide the overflow menu button entirely when compactMode .additional-btn { display: none !important; } @@ -260,7 +260,7 @@ } @media (max-width: 768px) { - :host-context(.nav-sidebar.collapsed) { + :host-context(.nav-sidebar.compactMode) { .nav-link { justify-content: flex-start; padding: 10px 12px; diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss index c4e20f7f91..c8f823c95f 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss @@ -91,8 +91,8 @@ transition: none; } -// Collapsed state behaviors inherited from parent sidebar -:host-context(.nav-sidebar.collapsed) { +// CompactMode state behaviors inherited from parent sidebar +:host-context(.nav-sidebar.compactMode) { .g-multi-btn-wrapper { justify-content: center; @@ -101,11 +101,11 @@ } } - // Don't completely hide nav-children in collapsed mode + // Don't completely hide nav-children in compactMode // The service already handles filtering to show only active items // This allows groups to still expand and show active context .nav-children { - // Keep children visible when group is expanded, even in collapsed sidebar + // Keep children visible when group is expanded, even in compactMode sidebar // The filtering logic in the service will handle what items to show } } From d65936ec94856a9611a50a55872589e0dc6d673e Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 13:25:22 +0200 Subject: [PATCH 38/61] feat(sideNav): make collapse expand children animation work again --- .../magic-nav-config.service.ts | 11 +++------ .../nav-list/nav-list.component.html | 6 ++--- .../nav-list/nav-list.component.scss | 24 ++++--------------- .../nav-list/nav-list.component.ts | 3 +++ 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index f8d91f07f6..5612c042d7 100644 --- a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -325,14 +325,9 @@ export class MagicNavConfigService { })); } - // Public methods to get expansion state (for component to check) - isProjectsExpanded(): boolean { - return this._isProjectsExpanded(); - } - - isTagsExpanded(): boolean { - return this._isTagsExpanded(); - } + // Public computed signals for expansion state (for component to check) + readonly isProjectsExpanded = computed(() => this._isProjectsExpanded()); + readonly isTagsExpanded = computed(() => this._isTagsExpanded()); // Simple toggle functions private _toggleProjectsExpanded(): void { diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index f5469c6e4e..e08dff0be4 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -27,11 +27,11 @@ }
-@if (item().children && item().children.length > 0) { +@if (item().children && item().children.length > 0 && isExpanded()) {
@@ -101,3 +115,22 @@ } } + + + + + @for (project of allProjectsExceptInbox(); track project.id) { + + } + + diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss index b28fc84096..7a78349282 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss @@ -6,21 +6,59 @@ display: flex; align-items: center; position: relative; + + // Hide additional buttons by default on desktop + .additional-btns { + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + } + + // Show additional buttons on hover (desktop) + &:hover .additional-btns { + opacity: 1; + visibility: visible; + } +} + +// Always show buttons on mobile/touch devices +@media (hover: none) { + .g-multi-btn-wrapper .additional-btns { + opacity: 1; + visibility: visible; + } +} + +.additional-btns { + display: flex; + position: absolute; + right: 0; + top: 0; + bottom: 0; } .additional-btn { flex-shrink: 0; - width: 32px; - height: 32px; - min-width: 32px; - padding: 4px; + width: 32px !important; + height: 32px !important; + min-width: 32px !important; + padding: 4px !important; margin-left: var(--s); color: var(--sidebar-text-secondary); background: transparent; border: none; - border-radius: 4px; + border-radius: 0 !important; // Override Material Design rounded corners cursor: pointer; - transition: all 0.2s ease; + + // Override Material Design button styles + &.mat-mdc-icon-button { + --mdc-icon-button-state-layer-size: 32px; + --mdc-icon-button-icon-size: 18px; + border-radius: 0 !important; + width: 32px !important; + height: 32px !important; + padding: 4px !important; + } &:hover { background-color: var(--sidebar-hover); diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts index 3682461126..a7ed9f31b6 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts @@ -1,8 +1,9 @@ -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIcon } from '@angular/material/icon'; import { MatIconButton } from '@angular/material/button'; import { MatTooltip } from '@angular/material/tooltip'; +import { MatMenuModule } from '@angular/material/menu'; import { TranslatePipe } from '@ngx-translate/core'; import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; import { NavItemComponent } from '../nav-item/nav-item.component'; @@ -11,6 +12,7 @@ import { DRAG_DELAY_FOR_TOUCH_LONGER } from '../../../app.constants'; import { IS_TOUCH_PRIMARY } from '../../../util/is-mouse-primary'; import { standardListAnimation } from '../../../ui/animations/standard-list.ani'; import { expandFadeAnimation } from '../../../ui/animations/expand.ani'; +import { MagicNavConfigService } from '../magic-nav-config.service'; @Component({ selector: 'nav-list', @@ -20,6 +22,7 @@ import { expandFadeAnimation } from '../../../ui/animations/expand.ani'; MatIcon, MatIconButton, MatTooltip, + MatMenuModule, TranslatePipe, CdkDropList, CdkDrag, @@ -31,6 +34,8 @@ import { expandFadeAnimation } from '../../../ui/animations/expand.ani'; animations: [standardListAnimation, expandFadeAnimation], }) export class NavSectionComponent { + private readonly _navConfigService = inject(MagicNavConfigService); + item = input.required(); showLabels = input(true); isExpanded = input(false); @@ -45,10 +50,17 @@ export class NavSectionComponent { readonly IS_TOUCH_PRIMARY = IS_TOUCH_PRIMARY; readonly DRAG_DELAY_FOR_TOUCH_LONGER = DRAG_DELAY_FOR_TOUCH_LONGER; + // Access to service methods and data for visibility menu + readonly allProjectsExceptInbox = this._navConfigService.allProjectsExceptInbox; + onHeaderClick(): void { this.itemClick.emit(this.item()); } + toggleProjectVisibility(projectId: string): void { + this._navConfigService.toggleProjectVisibility(projectId); + } + onDrop(event: CdkDragDrop): void { // Early exit if no actual movement if ( From 13a35fcf46eac4d55e30801399e8f5f612bf18cf Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 16:22:22 +0200 Subject: [PATCH 47/61] feat(sideNav): move certain nav parts to the bottom --- src/app/core-ui/magic-side-nav/magic-nav-config.service.ts | 6 +++--- .../core-ui/magic-side-nav/magic-side-nav.component.html | 5 ++++- .../core-ui/magic-side-nav/magic-side-nav.component.scss | 4 ++++ src/app/core-ui/magic-side-nav/magic-side-nav.model.ts | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index de52e2f28f..8b58ecadc6 100644 --- a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -16,9 +16,9 @@ import { LS } from '../../core/persistence/storage-keys.const'; import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component'; import { getGithubErrorUrl } from '../../core/error-handler/global-error-handler.util'; import { - selectUnarchivedVisibleProjects, - selectUnarchivedHiddenProjectIds, selectAllProjectsExceptInbox, + selectUnarchivedHiddenProjectIds, + selectUnarchivedVisibleProjects, } from '../../features/project/store/project.selectors'; import { toggleHideFromMenu } from '../../features/project/store/project.actions'; import { NavConfig, NavItem, NavWorkContextItem } from './magic-side-nav.model'; @@ -138,7 +138,7 @@ export class MagicNavConfigService { }, // Separator - { type: 'separator', id: 'sep-3' }, + { type: 'separator', id: 'sep-3', mtAuto: true }, // App Section { diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html index b434213fc7..d02697824b 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html @@ -79,7 +79,10 @@ @for (item of config().items; track item.id) { @switch (item.type) { @case ('separator') { - + } @case ('group') {
  • Date: Tue, 9 Sep 2025 17:17:58 +0200 Subject: [PATCH 48/61] feat(sideNav): make mobile nav work --- src/app/app.component.scss | 2 +- .../magic-nav-config.service.ts | 2 +- .../magic-side-nav.component.scss | 23 ++++++++++--------- .../nav-item/nav-item.component.scss | 20 ++++++++-------- .../main-header/main-header.component.html | 11 --------- .../main-header/main-header.component.scss | 19 +-------------- .../mobile-side-panel-menu.component.scss | 2 +- .../work-context-title.component.ts | 2 ++ src/assets/themes/arc.css | 2 +- src/assets/themes/dark-base.css | 2 +- src/assets/themes/dracula.css | 2 +- src/assets/themes/everforest.css | 4 ++-- src/assets/themes/glass.css | 2 +- src/assets/themes/nord-polar-night.css | 2 +- src/assets/themes/nord-snow-storm.css | 2 +- src/assets/themes/rainbow.css | 2 +- src/styles/_css-variables.scss | 10 ++++---- src/styles/util.scss | 6 +++++ 18 files changed, 49 insertions(+), 66 deletions(-) diff --git a/src/app/app.component.scss b/src/app/app.component.scss index a5a9b6f217..071b208689 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -138,7 +138,7 @@ mat-sidenav-container { mat-sidenav ::ng-deep .mat-drawer-inner-container { @include scrollYImportant(); - background: var(--sidebar-bg); + background: var(--sidenav-bg); } mat-sidenav { diff --git a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts index 8b58ecadc6..c164e333b4 100644 --- a/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts +++ b/src/app/core-ui/magic-side-nav/magic-nav-config.service.ts @@ -232,7 +232,7 @@ export class MagicNavConfigService { ], fullModeByDefault: true, showLabels: true, - mobileBreakpoint: 768, + mobileBreakpoint: 600, position: 'left', theme: 'light', resizable: true, diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index 7947c61300..e4926fa5a8 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -15,16 +15,15 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); position: relative; z-index: var(--z-side-nav); // Use app's CSS variables for consistent theming - --sidebar-bg: var(--palette-background-card); --sidebar-border: var(--divider-color); --sidebar-hover: var(--palette-background-hover); - --sidebar-active-text: var(--palette-primary-800); + --sidenav-active-text: var(--palette-primary-800); --mat-menu-item-spacing: 4px; // Dark theme overrides body.isDarkTheme & { - --sidebar-active-text: var(--c-primary); + --sidenav-active-text: var(--c-primary); } } @@ -42,15 +41,16 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); .mobile-menu-toggle { display: none; position: fixed; - top: 16px; - left: 16px; - z-index: 1001; - background: var(--sidebar-bg); + top: 8px; + left: 8px; + z-index: 221001; + background: var(--sidenav-bg); border: 1px solid var(--sidebar-border); border-radius: 8px; padding: 8px; cursor: pointer; transition: var(--transition-standard); + color: var(--text-color-less-intense); &.visible { display: flex; @@ -101,7 +101,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); left: 0; bottom: 0; width: $sidebar-width-expanded; - background: var(--sidebar-bg); + background: var(--sidenav-bg); border-right: 1px solid var(--sidebar-border); // Default: don't animate width transition: none; @@ -171,7 +171,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); right: -4px; &:hover { - background-color: var(--sidebar-active-text); + background-color: var(--sidenav-active-text); } .resize-handle-line { @@ -189,6 +189,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); box-shadow: 4px 0 16px rgba(0, 0, 0, 0.3); transform: translateX(-100%); width: $sidebar-width-expanded; + background: var(--sidenav-bg-mobile); &.mobile-visible { transform: translateX(0); @@ -296,7 +297,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); } &:hover { - background-color: var(--sidebar-active-text); + background-color: var(--sidenav-active-text); .resize-handle-line { opacity: 1; @@ -304,7 +305,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); } &:active { - background-color: var(--sidebar-active-text); + background-color: var(--sidenav-active-text); } } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index 6cbcfe68b2..d8dc35cff0 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -145,11 +145,11 @@ } &.active { - background: var(--sidebar-active); - color: var(--sidebar-active-text); + background: var(--sidenav-active); + color: var(--sidenav-active-text); .nav-icon { - color: var(--sidebar-active-text); + color: var(--sidenav-active-text); } } @@ -189,8 +189,8 @@ :host-context(.isLightTheme .nav-sidebar.compactMode) { .nav-link { &.active { - color: var(--sidebar-active-text); - background: var(--sidebar-active); + color: var(--sidenav-active-text); + background: var(--sidenav-active); border-radius: 48px; border-bottom: 1px solid var(--separator-color); //border: 1px solid var(--separator-color); @@ -202,7 +202,7 @@ } .nav-icon { - color: var(--sidebar-active-text); + color: var(--sidenav-active-text); } } } @@ -219,8 +219,8 @@ } &.active { - color: var(--sidebar-active-text); - background: var(--sidebar-active); + color: var(--sidenav-active-text); + background: var(--sidenav-active); &::after { position: absolute; @@ -228,7 +228,7 @@ } .nav-icon { - color: var(--sidebar-active-text); + color: var(--sidenav-active-text); } } } @@ -255,7 +255,7 @@ // (no duplicate .nav-link styles here; use global ones) } -@media (max-width: 768px) { +@include mq(xs, max) { :host-context(.nav-sidebar.compactMode) { .nav-link { justify-content: flex-start; diff --git a/src/app/core-ui/main-header/main-header.component.html b/src/app/core-ui/main-header/main-header.component.html index e9ff43618b..2cf9515a03 100644 --- a/src/app/core-ui/main-header/main-header.component.html +++ b/src/app/core-ui/main-header/main-header.component.html @@ -1,15 +1,4 @@
    - @if (!isNavAlwaysVisible()) { - - } - Date: Tue, 9 Sep 2025 17:24:23 +0200 Subject: [PATCH 49/61] feat(sideNav): improve mobile --- .../magic-side-nav.component.scss | 38 +++++++++---------- .../nav-item/nav-item.component.scss | 14 +++++++ .../nav-list/nav-list.component.scss | 2 +- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index e4926fa5a8..b404c13f6c 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -1,12 +1,14 @@ -// Use app's CSS variables for width and spacing -$sidebar-width-expanded: var(--side-nav-width, 260px); -$mobile-breakpoint: 768px; -// Use duration/timing variables directly; --transition-standard is a shorthand -$transition-duration: var(--transition-duration-m, 0.3s); -$transition-timing: var(--ani-standard-timing, cubic-bezier(0.4, 0, 0.2, 1)); -$overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); - :host { + --sidebar-width-expanded: var(--side-nav-width, 260px); + --mobile-breakpoint: 768px; + --sidebar-transition-duration: var(--transition-duration-m, 0.3s); + --sidebar-transition-timing: var(--ani-standard-timing, cubic-bezier(0.4, 0, 0.2, 1)); + --overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); + --sidebar-border: var(--divider-color); + --sidebar-hover: var(--palette-background-hover); + --sidenav-active-text: var(--palette-primary-800); + --mat-menu-item-spacing: 4px; + // Ensure the component itself participates in layout and animates width display: block; flex: 0 0 auto; @@ -15,11 +17,6 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); position: relative; z-index: var(--z-side-nav); // Use app's CSS variables for consistent theming - --sidebar-border: var(--divider-color); - - --sidebar-hover: var(--palette-background-hover); - --sidenav-active-text: var(--palette-primary-800); - --mat-menu-item-spacing: 4px; // Dark theme overrides body.isDarkTheme & { @@ -29,7 +26,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); // Enable host width transition only when toggling compactMode/fullMode :host(.animate) { - transition: width $transition-duration $transition-timing; + transition: width var(--sidebar-transition-duration) var(--sidebar-transition-timing); } // Disable host transition during resize drags @@ -83,10 +80,10 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); left: 0; right: 0; bottom: 0; - background: $overlay-bg; + background: var(--overlay-bg); z-index: var(--z-backdrop, 1001); opacity: 0; - transition: opacity $transition-duration $transition-timing; + transition: opacity var(--sidebar-transition-duration) var(--sidebar-transition-timing); &.visible { display: block; @@ -100,7 +97,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); top: 0; left: 0; bottom: 0; - width: $sidebar-width-expanded; + width: var(--sidebar-width-expanded); background: var(--sidenav-bg); border-right: 1px solid var(--sidebar-border); // Default: don't animate width @@ -112,7 +109,7 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); // Slight elevation and clear background for overlay/mobile mode is defined in the mobile block below &.animate { - transition: width $transition-duration $transition-timing; + transition: width var(--sidebar-transition-duration) var(--sidebar-transition-timing); } &.resizing { @@ -184,11 +181,12 @@ $overlay-bg: var(--c-backdrop, rgba(0, 0, 0, 0.6)); // Mobile specific &.mobile { - transition: transform $transition-duration $transition-timing; + transition: transform var(--sidebar-transition-duration) + var(--sidebar-transition-timing); will-change: transform; box-shadow: 4px 0 16px rgba(0, 0, 0, 0.3); transform: translateX(-100%); - width: $sidebar-width-expanded; + width: var(--sidebar-width-expanded); background: var(--sidenav-bg-mobile); &.mobile-visible { diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index d8dc35cff0..cb02dfedf0 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -54,6 +54,16 @@ display: none !important; } +@media (hover: none) { + .additional-btn { + opacity: 1; + visibility: visible; + } + .task-count { + display: none !important; + } +} + .task-count { position: absolute; top: 50%; @@ -66,6 +76,10 @@ align-items: center; // avoid affecting drag handle pointer-events: none; + + @include mq(xs, max) { + display: none; + } } // Active state visuals are handled by .nav-link.active styles diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss index 7a78349282..9fac2c552c 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.scss @@ -23,7 +23,7 @@ // Always show buttons on mobile/touch devices @media (hover: none) { - .g-multi-btn-wrapper .additional-btns { + .additional-btns { opacity: 1; visibility: visible; } From 504f15bb133e7c39091067e487ad0c518d7a5772 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 17:32:56 +0200 Subject: [PATCH 50/61] feat(sideNav): always close mobile nav when item was clicked --- .../core-ui/magic-side-nav/magic-side-nav.component.html | 1 + .../core-ui/magic-side-nav/magic-side-nav.component.scss | 2 +- .../magic-side-nav/nav-item/nav-item.component.html | 1 + .../magic-side-nav/nav-list/nav-list.component.html | 7 ++++--- .../core-ui/magic-side-nav/nav-list/nav-list.component.ts | 4 ++++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html index d02697824b..50275e580d 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html @@ -121,6 +121,7 @@ [variant]="'nav'" [showLabels]="showText()" [showMoreButton]="showText()" + (clicked)="onItemClick(item)" > } @case ('route') { diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index b404c13f6c..21a1a66954 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -184,12 +184,12 @@ transition: transform var(--sidebar-transition-duration) var(--sidebar-transition-timing); will-change: transform; - box-shadow: 4px 0 16px rgba(0, 0, 0, 0.3); transform: translateX(-100%); width: var(--sidebar-width-expanded); background: var(--sidenav-bg-mobile); &.mobile-visible { + box-shadow: 4px 0 16px rgba(0, 0, 0, 0.3); transform: translateX(0); } } diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html index 65c90c872b..bcf21f70cd 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.html @@ -11,6 +11,7 @@ class="nav-link" [class.has-tasks]="nrOfOpenTasks() > 0" mat-menu-item + (click)="clicked.emit()" > {{ workContext()!.icon || defaultIcon() diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html index f2b741beac..2bd1c77b18 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.html @@ -69,6 +69,7 @@ [variant]="'nav'" [showLabels]="true" [showMoreButton]="true" + (clicked)="onChildClick(child)" >
  • } @else { @@ -85,7 +86,7 @@ [svgIcon]="child.svgIcon" [label]="child.label" [showLabels]="true" - (clicked)="itemClick.emit(child)" + (clicked)="onChildClick(child)" > } @case ('href') { @@ -96,7 +97,7 @@ [svgIcon]="child.svgIcon" [label]="child.label" [showLabels]="true" - (clicked)="itemClick.emit(child)" + (clicked)="onChildClick(child)" > } @case ('action') { @@ -106,7 +107,7 @@ [svgIcon]="child.svgIcon" [label]="child.label" [showLabels]="true" - (clicked)="itemClick.emit(child)" + (clicked)="onChildClick(child)" > } } diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts index a7ed9f31b6..9ce4a4c219 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list.component.ts @@ -57,6 +57,10 @@ export class NavSectionComponent { this.itemClick.emit(this.item()); } + onChildClick(child: NavItem): void { + this.itemClick.emit(child); + } + toggleProjectVisibility(projectId: string): void { this._navConfigService.toggleProjectVisibility(projectId); } From 2c262c5721c2c86dc533de84fd56c69bebe86720 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 17:40:15 +0200 Subject: [PATCH 51/61] feat(sideNav): make backdrop work --- src/app/app.component.scss | 10 ---------- .../magic-side-nav/magic-side-nav.component.html | 4 +--- .../magic-side-nav/magic-side-nav.component.scss | 13 +++++-------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 071b208689..aea7f5c1d2 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -35,16 +35,6 @@ min-height: -webkit-fill-available !important; z-index: auto !important; } - - // Header provides a toggle on small screens; keep native toggle hidden on desktop - ::ng-deep .mobile-menu-toggle { - display: none !important; - } - - // Overlay not needed on desktop - ::ng-deep .nav-overlay { - display: none !important; - } } } diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html index 50275e580d..006b461d8e 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html @@ -28,12 +28,10 @@ @if (isMobile() && showMobileMenu()) { } -
    From 001b9c9301d33b931ba184890aa0218144414775 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 19:07:16 +0200 Subject: [PATCH 57/61] feat(sideNav): remove side nav layout store stuff --- src/app/core-ui/layout/layout.service.ts | 37 +------------------ .../core-ui/layout/store/layout.actions.ts | 4 -- .../core-ui/layout/store/layout.reducer.ts | 16 -------- .../plugins/allowed-plugin-actions.const.ts | 4 -- 4 files changed, 2 insertions(+), 59 deletions(-) diff --git a/src/app/core-ui/layout/layout.service.ts b/src/app/core-ui/layout/layout.service.ts index 8b195f91d4..700a585a7c 100644 --- a/src/app/core-ui/layout/layout.service.ts +++ b/src/app/core-ui/layout/layout.service.ts @@ -3,28 +3,24 @@ import { hideAddTaskBar, hideIssuePanel, hideNonTaskSidePanelContent, - hideMobileSideNav, hideTaskViewCustomizerPanel, showAddTaskBar, toggleIssuePanel, toggleShowNotes, - toggleMobileSideNav, toggleTaskViewCustomizerPanel, } from './store/layout.actions'; -import { merge, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { LayoutState, selectIsShowAddTaskBar, selectIsShowIssuePanel, selectIsShowNotes, - selectIsShowSideNav, selectIsShowTaskViewCustomizerPanel, } from './store/layout.reducer'; import { filter, map, startWith } from 'rxjs/operators'; import { BreakpointObserver } from '@angular/cdk/layout'; -import { NavigationEnd, NavigationStart, Router } from '@angular/router'; -import { WorkContextService } from '../../features/work-context/work-context.service'; +import { NavigationEnd, Router } from '@angular/router'; import { selectMiscConfig } from '../../features/config/store/global-config.reducer'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -39,7 +35,6 @@ const VERY_BIG_SCREEN = NAV_ALWAYS_VISIBLE; export class LayoutService { private _store$ = inject>(Store); private _router = inject(Router); - private _workContextService = inject(WorkContextService); private _breakPointObserver = inject(BreakpointObserver); private _previouslyFocusedElement: HTMLElement | null = null; @@ -47,9 +42,6 @@ export class LayoutService { readonly isShowAddTaskBar$: Observable = this._store$.pipe( select(selectIsShowAddTaskBar), ); - readonly isShowMobileSideNav$: Observable = this._store$.pipe( - select(selectIsShowSideNav), - ); readonly isShowIssuePanel$: Observable = this._store$.pipe( select(selectIsShowIssuePanel), ); @@ -129,10 +121,6 @@ export class LayoutService { return !this.isRightPanelNextNavOver(); }); - private readonly isShowMobileSideNav = toSignal(this.isShowMobileSideNav$, { - initialValue: false, - }); - readonly isShowNotes = toSignal(this._store$.pipe(select(selectIsShowNotes)), { initialValue: false, }); @@ -144,19 +132,6 @@ export class LayoutService { readonly isShowIssuePanel = toSignal(this.isShowIssuePanel$, { initialValue: false }); - // Subscribe to navigation events to hide sidenav - constructor() { - // Only hide sidenav on actual navigation, not on initial load - merge( - this._router.events.pipe(filter((ev) => ev instanceof NavigationStart)), - this._workContextService.onWorkContextChange$, - ).subscribe(() => { - if (this.isNavOver() && this.isShowMobileSideNav()) { - this.hideSideNav(); - } - }); - } - showAddTaskBar(): void { // Store currently focused element if it's a task const activeElement = document.activeElement as HTMLElement; @@ -182,14 +157,6 @@ export class LayoutService { } } - toggleSideNav(): void { - this._store$.dispatch(toggleMobileSideNav()); - } - - hideSideNav(): void { - this._store$.dispatch(hideMobileSideNav()); - } - toggleNotes(): void { this._store$.dispatch(toggleShowNotes()); } diff --git a/src/app/core-ui/layout/store/layout.actions.ts b/src/app/core-ui/layout/store/layout.actions.ts index c9e604133e..6930d6c831 100644 --- a/src/app/core-ui/layout/store/layout.actions.ts +++ b/src/app/core-ui/layout/store/layout.actions.ts @@ -16,10 +16,6 @@ export const showAddTaskBar = createAction('[Layout] Show AddTaskBar'); export const hideAddTaskBar = createAction('[Layout] Hide AddTaskBar'); -export const hideMobileSideNav = createAction('[Layout] Hide Mobile SideBar'); - -export const toggleMobileSideNav = createAction('[Layout] Toggle Mobile SideBar'); - export const toggleShowNotes = createAction('[Layout] ToggleShow Notes'); export const hideNonTaskSidePanelContent = createAction( diff --git a/src/app/core-ui/layout/store/layout.reducer.ts b/src/app/core-ui/layout/store/layout.reducer.ts index 2950e01b2c..2a4fa07e41 100644 --- a/src/app/core-ui/layout/store/layout.reducer.ts +++ b/src/app/core-ui/layout/store/layout.reducer.ts @@ -2,13 +2,11 @@ import { hideAddTaskBar, hideNonTaskSidePanelContent, hidePluginPanel, - hideMobileSideNav, showAddTaskBar, showPluginPanel, toggleIssuePanel, togglePluginPanel, toggleShowNotes, - toggleMobileSideNav, toggleTaskViewCustomizerPanel, } from './layout.actions'; import { @@ -25,7 +23,6 @@ export interface LayoutState { isShowAddTaskBar: boolean; isShowNotes: boolean; isShowIssuePanel: boolean; - isShowMobileSideNav: boolean; isShowCelebrate: boolean; isShowTaskViewCustomizerPanel: boolean; isShowPluginPanel: boolean; @@ -34,7 +31,6 @@ export interface LayoutState { export const INITIAL_LAYOUT_STATE: LayoutState = { isShowAddTaskBar: false, - isShowMobileSideNav: false, isShowNotes: false, isShowIssuePanel: false, isShowCelebrate: false, @@ -51,11 +47,6 @@ export const selectIsShowAddTaskBar = createSelector( (state) => state.isShowAddTaskBar, ); -export const selectIsShowSideNav = createSelector( - selectLayoutFeatureState, - (state) => state.isShowMobileSideNav, -); - export const selectIsShowNotes = createSelector( selectLayoutFeatureState, (state) => state.isShowNotes, @@ -101,13 +92,6 @@ const _reducer = createReducer( on(hideAddTaskBar, (state) => ({ ...state, isShowAddTaskBar: false })), - on(hideMobileSideNav, (state) => ({ ...state, isShowMobileSideNav: false })), - - on(toggleMobileSideNav, (state) => ({ - ...state, - isShowSideNavisShowMobileSideNav: !state.isShowMobileSideNav, - })), - on(toggleShowNotes, (state) => ({ ...state, ...ALL_PANEL_CONTENT_HIDDEN, diff --git a/src/app/plugins/allowed-plugin-actions.const.ts b/src/app/plugins/allowed-plugin-actions.const.ts index 824a9ec650..8f7a4da291 100644 --- a/src/app/plugins/allowed-plugin-actions.const.ts +++ b/src/app/plugins/allowed-plugin-actions.const.ts @@ -20,11 +20,9 @@ import { hideAddTaskBar, hideNonTaskSidePanelContent, hidePluginPanel, - hideMobileSideNav, showAddTaskBar, toggleIssuePanel, toggleShowNotes, - toggleMobileSideNav, } from '../core-ui/layout/store/layout.actions'; /** @@ -36,8 +34,6 @@ export const ALLOWED_PLUGIN_ACTIONS = [ // Layout showAddTaskBar, hideAddTaskBar, - hideMobileSideNav, - toggleMobileSideNav, toggleShowNotes, hideNonTaskSidePanelContent, toggleIssuePanel, From 7cec20a77b33a232bd2474f0b2018abf037e86d2 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 19:15:13 +0200 Subject: [PATCH 58/61] feat(sideNav): make shepherd kinda work and throw out more layout stuff --- src/app/core-ui/layout/layout.service.ts | 31 ++------------- .../main-header/main-header.component.ts | 1 - .../features/shepherd/shepherd-steps.const.ts | 38 ++++++++++++++----- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/app/core-ui/layout/layout.service.ts b/src/app/core-ui/layout/layout.service.ts index 700a585a7c..525babb0c2 100644 --- a/src/app/core-ui/layout/layout.service.ts +++ b/src/app/core-ui/layout/layout.service.ts @@ -21,12 +21,10 @@ import { import { filter, map, startWith } from 'rxjs/operators'; import { BreakpointObserver } from '@angular/cdk/layout'; import { NavigationEnd, Router } from '@angular/router'; -import { selectMiscConfig } from '../../features/config/store/global-config.reducer'; import { toSignal } from '@angular/core/rxjs-interop'; -const NAV_ALWAYS_VISIBLE = 1200; -const NAV_OVER_RIGHT_PANEL_NEXT = 800; -const BOTH_OVER = 720; +const NAV_ALWAYS_VISIBLE = 600; +const RIGHT_PANEL_OVER = 720; const VERY_BIG_SCREEN = NAV_ALWAYS_VISIBLE; @Injectable({ @@ -50,23 +48,16 @@ export class LayoutService { readonly isScrolled = signal(false); readonly isShowAddTaskBar = toSignal(this.isShowAddTaskBar$, { initialValue: false }); - readonly isNavAlwaysVisible = toSignal( + readonly isMobileNav = toSignal( this._breakPointObserver .observe([`(min-width: ${NAV_ALWAYS_VISIBLE}px)`]) .pipe(map((result) => result.matches)), { initialValue: false }, ); - readonly isRightPanelNextNavOver = toSignal( - this._breakPointObserver - .observe([`(min-width: ${NAV_OVER_RIGHT_PANEL_NEXT}px)`]) - .pipe(map((result) => result.matches)), - { initialValue: false }, - ); - readonly isRightPanelOver = toSignal( this._breakPointObserver - .observe([`(min-width: ${BOTH_OVER}px)`]) + .observe([`(min-width: ${RIGHT_PANEL_OVER}px)`]) .pipe(map((result) => !result.matches)), { initialValue: false }, ); @@ -107,20 +98,6 @@ export class LayoutService { return url.includes('/active/') || url.includes('/tag/') || url.includes('/project/'); } - // Convert misc config to signal - private readonly _miscConfig = toSignal(this._store$.select(selectMiscConfig), { - initialValue: undefined, - }); - - // Computed signal for nav over state - readonly isNavOver = computed(() => { - const miscCfg = this._miscConfig(); - if (miscCfg?.isUseMinimalNav) { - return false; - } - return !this.isRightPanelNextNavOver(); - }); - readonly isShowNotes = toSignal(this._store$.pipe(select(selectIsShowNotes)), { initialValue: false, }); diff --git a/src/app/core-ui/main-header/main-header.component.ts b/src/app/core-ui/main-header/main-header.component.ts index 62160234ee..b52e2351ec 100644 --- a/src/app/core-ui/main-header/main-header.component.ts +++ b/src/app/core-ui/main-header/main-header.component.ts @@ -146,7 +146,6 @@ export class MainHeaderComponent implements OnDestroy { this.workContextService.activeWorkContextTypeAndId$, ); activeWorkContextTitle = toSignal(this.workContextService.activeWorkContextTitle$); - isNavAlwaysVisible = computed(() => this.layoutService.isNavAlwaysVisible()); currentTask = toSignal(this.taskService.currentTask$); currentTaskId = this.taskService.currentTaskId; pomodoroIsEnabled = toSignal(this.pomodoroService.isEnabled$); diff --git a/src/app/features/shepherd/shepherd-steps.const.ts b/src/app/features/shepherd/shepherd-steps.const.ts index ba6b6129ae..cd61f0c99b 100644 --- a/src/app/features/shepherd/shepherd-steps.const.ts +++ b/src/app/features/shepherd/shepherd-steps.const.ts @@ -436,16 +436,26 @@ export const SHEPHERD_STEPS = ( text: 'Open the menu (menu)', beforeShowPromise: () => { // If nav is always visible, skip this step - if (layoutService.isNavAlwaysVisible()) { + if (layoutService.isMobileNav()) { setTimeout(() => shepherdService.next(), 0); return Promise.resolve(); } return Promise.resolve(); }, - when: nextOnObs( - layoutService.isShowMobileSideNav$.pipe(filter((v) => !!v)), - shepherdService, - ), + when: { + show: () => { + // If nav is always visible, skip immediately + if (layoutService.isMobileNav()) { + setTimeout(() => shepherdService.next(), 0); + } else { + // For mobile, wait for the burger button to be clicked + // We'll use a simple timeout or manual next for now + // since the mobile nav opening doesn't have an observable + // TODO better implementation + setTimeout(() => shepherdService.next(), 8000); + } + }, + }, }, // ------------------------------ @@ -491,16 +501,24 @@ export const SHEPHERD_STEPS = ( beforeShowPromise: () => { return router.navigate(['']).then(() => { // If nav is always visible, skip this step - if (layoutService.isNavAlwaysVisible()) { + if (layoutService.isMobileNav()) { setTimeout(() => shepherdService.next(), 0); } }); }, text: 'Open the menu (menu)', - when: nextOnObs( - layoutService.isShowMobileSideNav$.pipe(filter((v) => !!v)), - shepherdService, - ), + when: { + show: () => { + // If nav is always visible, skip immediately + if (layoutService.isMobileNav()) { + setTimeout(() => shepherdService.next(), 0); + } else { + // For mobile, wait for the burger button to be clicked + // TODO better implementation + setTimeout(() => shepherdService.next(), 8000); + } + }, + }, }, { title: 'Configure Sync', From 7c3d4df1409f8533ac836846e17fbcfb011fe7cd Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 19:19:39 +0200 Subject: [PATCH 59/61] feat(sideNav): make text color work better --- src/app/core-ui/magic-side-nav/magic-side-nav.component.scss | 2 +- src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index 84967113ac..ed37ab1846 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -38,7 +38,7 @@ padding: 8px; cursor: pointer; transition: var(--transition-standard); - color: var(--text-color-less-intense); + color: var(--text-color); &:hover { background: var(--sidebar-hover); diff --git a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss index cb02dfedf0..2ae13e2ef0 100644 --- a/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss +++ b/src/app/core-ui/magic-side-nav/nav-item/nav-item.component.scss @@ -5,6 +5,7 @@ align-items: stretch; position: relative; width: 100%; + color: var(--text-color); button:first-of-type { white-space: nowrap; @@ -18,6 +19,7 @@ } .nav-label { + color: var(--text-color); overflow: hidden; text-overflow: ellipsis; // smooth collapse/expand for title From 899ee1d8cfa0776e09b84c2594e905fc66f9e1d0 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 19:24:08 +0200 Subject: [PATCH 60/61] feat(sideNav): move drag handle right of scrollbars --- .../magic-side-nav.component.html | 28 +++++----- .../magic-side-nav.component.scss | 51 ++++++------------- 2 files changed, 28 insertions(+), 51 deletions(-) diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html index 4c8c8d6f36..6164203f6a 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.html +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.html @@ -153,19 +153,17 @@ } } - - - @if (config().resizable && !isMobile()) { - - } + + +@if (config().resizable && !isMobile()) { + +} diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index ed37ab1846..ed2fb6ce33 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -142,23 +142,6 @@ left: 50%; transform: translateX(-50%); } - - // Show resize handle in compactMode but style differently - .resize-handle.compactMode-handle { - display: block; - width: 8px; - right: -4px; - - &:hover { - background-color: var(--sidenav-active-text); - } - - .resize-handle-line { - width: 3px; - height: 60px; - opacity: 0.7; - } - } } // Mobile specific @@ -263,14 +246,14 @@ opacity: 0.7; } -// Resize handle +// Resize handle (now positioned relative to host component) .resize-handle { position: absolute; top: 0; bottom: 0; width: 4px; cursor: col-resize; - z-index: 1002; + z-index: 1003; // Above the nav-sidebar transition: background-color 0.2s ease; &.position-left { @@ -281,12 +264,21 @@ left: -2px; } + // CompactMode specific styling + &.compactMode-handle { + width: 8px; + + &.position-left { + right: -4px; + } + + &.position-right { + left: -4px; + } + } + &:hover { background-color: var(--sidenav-active-text); - - .resize-handle-line { - opacity: 1; - } } &:active { @@ -294,19 +286,6 @@ } } -.resize-handle-line { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 2px; - height: 40px; - background: var(--sidebar-text-secondary); - border-radius: 1px; - opacity: 0.5; - transition: opacity 0.2s ease; -} - // Multi-button wrapper styling moved to nav-list component // Media-query rules affecting link rows moved to nav-item component From 05284297b927d3e0323c0e8822d1a234ec489c3c Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 9 Sep 2025 19:28:57 +0200 Subject: [PATCH 61/61] feat: make scrollbars a bit more subtle --- src/styles/_css-variables.scss | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/styles/_css-variables.scss b/src/styles/_css-variables.scss index bff1650764..5a967695e8 100644 --- a/src/styles/_css-variables.scss +++ b/src/styles/_css-variables.scss @@ -382,9 +382,9 @@ body { --task-selected-shadow: var(--whiteframe-shadow-3dp); // Scrollbar colors - --scrollbar-thumb: #888; - --scrollbar-thumb-hover: #555; - --scrollbar-track: #f1f1f1; + --scrollbar-thumb: rgba(136, 136, 136, 0.5); + --scrollbar-thumb-hover: rgba(85, 85, 85, 0.7); + --scrollbar-track: rgba(241, 241, 241, 0.3); // Chip colors --chip-outline-color: rgba(125, 125, 125, 0.4); @@ -469,7 +469,7 @@ body.isDarkTheme { // Text colors --text-color: rgb(230, 230, 230); --text-color-less-intense: rgba(235, 235, 235, 0.9); - --text-color-muted: rgba(235, 235, 235, 0.6); + --text-color-muted: rgba(235, 235, 235, 0.75); --text-color-more-intense: rgb(245, 245, 245); --text-color-most-intense: rgb(255, 255, 255); @@ -507,9 +507,9 @@ body.isDarkTheme { --task-selected-shadow: var(--whiteframe-shadow-4dp); // Scrollbar colors - --scrollbar-thumb: #333; - --scrollbar-thumb-hover: #444; - --scrollbar-track: #222; + --scrollbar-thumb: rgba(51, 51, 51, 0.6); + --scrollbar-thumb-hover: rgba(68, 68, 68, 0.8); + --scrollbar-track: rgba(34, 34, 34, 0.4); // Grid colors --grid-color: var(--c-light-10);