diff --git a/src/app/core-ui/layout/layout.service.ts b/src/app/core-ui/layout/layout.service.ts index edac6e54e7..bff8916d9f 100644 --- a/src/app/core-ui/layout/layout.service.ts +++ b/src/app/core-ui/layout/layout.service.ts @@ -8,6 +8,8 @@ import { toggleIssuePanel, toggleShowNotes, toggleTaskViewCustomizerPanel, + toggleScheduleDayPanel, + hideScheduleDayPanel, } from './store/layout.actions'; import { Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; @@ -17,6 +19,7 @@ import { selectIsShowIssuePanel, selectIsShowNotes, selectIsShowTaskViewCustomizerPanel, + selectIsShowScheduleDayPanel, } from './store/layout.reducer'; import { map } from 'rxjs/operators'; import { BreakpointObserver } from '@angular/cdk/layout'; @@ -85,6 +88,11 @@ export class LayoutService { readonly isShowIssuePanel = toSignal(this.isShowIssuePanel$, { initialValue: false }); + readonly isShowScheduleDayPanel = toSignal( + this._store$.pipe(select(selectIsShowScheduleDayPanel)), + { initialValue: false }, + ); + showAddTaskBar(): void { // Store currently focused element if it's a task const activeElement = document.activeElement as HTMLElement; @@ -138,4 +146,13 @@ export class LayoutService { // Trigger the focus signal - components listening to this signal will handle the focus this._focusSideNavTrigger.update((value) => value + 1); } + + // Schedule Day Panel controls + toggleScheduleDayPanel(): void { + this._store$.dispatch(toggleScheduleDayPanel()); + } + + hideScheduleDayPanel(): void { + this._store$.dispatch(hideScheduleDayPanel()); + } } diff --git a/src/app/core-ui/layout/store/layout.actions.ts b/src/app/core-ui/layout/store/layout.actions.ts index 6930d6c831..7dc4787d3f 100644 --- a/src/app/core-ui/layout/store/layout.actions.ts +++ b/src/app/core-ui/layout/store/layout.actions.ts @@ -37,3 +37,8 @@ export const togglePluginPanel = createAction( '[Layout] Toggle PluginPanel', (pluginId: string) => ({ pluginId }), ); + +// Schedule Day Panel +export const toggleScheduleDayPanel = createAction('[Layout] Toggle ScheduleDayPanel'); + +export const hideScheduleDayPanel = createAction('[Layout] Hide ScheduleDayPanel'); diff --git a/src/app/core-ui/layout/store/layout.effects.ts b/src/app/core-ui/layout/store/layout.effects.ts index ae0f73d8c7..e6952ab52a 100644 --- a/src/app/core-ui/layout/store/layout.effects.ts +++ b/src/app/core-ui/layout/store/layout.effects.ts @@ -3,6 +3,7 @@ import { Actions, createEffect, ofType } from '@ngrx/effects'; import { hideNonTaskSidePanelContent } from './layout.actions'; import { filter, mapTo } from 'rxjs/operators'; import { setSelectedTask } from '../../../features/tasks/store/task.actions'; +import { TaskDetailTargetPanel } from '../../../features/tasks/task.model'; import { LayoutService } from '../layout.service'; import { NavigationStart, NavigationEnd, Router } from '@angular/router'; @@ -23,7 +24,12 @@ export class LayoutEffects { hideNotesWhenTaskIsSelected$ = createEffect(() => this.actions$.pipe( ofType(setSelectedTask), - filter(({ id }) => id !== null), + filter(({ id, taskDetailTargetPanel, isSkipToggle }) => { + // Do not hide side content when opening modal (DONT_OPEN_PANEL) or when explicitly skipped + if (id === null) return false; + if (isSkipToggle) return false; + return taskDetailTargetPanel !== TaskDetailTargetPanel.DONT_OPEN_PANEL; + }), mapTo(hideNonTaskSidePanelContent()), ), ); diff --git a/src/app/core-ui/layout/store/layout.reducer.ts b/src/app/core-ui/layout/store/layout.reducer.ts index 2a4fa07e41..905f5661fd 100644 --- a/src/app/core-ui/layout/store/layout.reducer.ts +++ b/src/app/core-ui/layout/store/layout.reducer.ts @@ -9,6 +9,7 @@ import { toggleShowNotes, toggleTaskViewCustomizerPanel, } from './layout.actions'; +import { toggleScheduleDayPanel, hideScheduleDayPanel } from './layout.actions'; import { Action, createFeatureSelector, @@ -27,6 +28,7 @@ export interface LayoutState { isShowTaskViewCustomizerPanel: boolean; isShowPluginPanel: boolean; activePluginId: string | null; + isShowScheduleDayPanel: boolean; } export const INITIAL_LAYOUT_STATE: LayoutState = { @@ -37,6 +39,7 @@ export const INITIAL_LAYOUT_STATE: LayoutState = { isShowTaskViewCustomizerPanel: false, isShowPluginPanel: false, activePluginId: null, + isShowScheduleDayPanel: false, }; export const selectLayoutFeatureState = @@ -77,12 +80,18 @@ export const selectActivePluginId = createSelector( (state) => state.activePluginId, ); +export const selectIsShowScheduleDayPanel = createSelector( + selectLayoutFeatureState, + (state) => state.isShowScheduleDayPanel, +); + const ALL_PANEL_CONTENT_HIDDEN: Partial = { isShowNotes: false, isShowIssuePanel: false, isShowTaskViewCustomizerPanel: false, isShowPluginPanel: false, activePluginId: null, + isShowScheduleDayPanel: false, }; const _reducer = createReducer( @@ -138,6 +147,16 @@ const _reducer = createReducer( activePluginId: isCurrentlyActive ? null : pluginId, }; }), + on(toggleScheduleDayPanel, (state) => ({ + ...state, + ...ALL_PANEL_CONTENT_HIDDEN, + isShowScheduleDayPanel: !state.isShowScheduleDayPanel, + })), + on(hideScheduleDayPanel, (state) => ({ + ...state, + ...ALL_PANEL_CONTENT_HIDDEN, + isShowScheduleDayPanel: false, + })), ); export const layoutReducer = ( diff --git a/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts b/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts index 443da1f879..a7cddb8790 100644 --- a/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts +++ b/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts @@ -13,6 +13,17 @@ import { KeyboardConfig } from '../../../features/config/keyboard-config.model'; standalone: true, imports: [MatIconButton, MatIcon, MatTooltip, TranslatePipe], template: ` + + +
+ +
+ +} diff --git a/src/app/ui/help-box/help-box.component.scss b/src/app/ui/help-box/help-box.component.scss new file mode 100644 index 0000000000..c055cd635d --- /dev/null +++ b/src/app/ui/help-box/help-box.component.scss @@ -0,0 +1,33 @@ +:host { + display: block; + position: sticky; + top: 16px; + background: var(--bg); + z-index: 200; + margin: 8px; + border: 2px solid var(--separator-color); + border-radius: var(--card-border-radius); + + .help-box { + position: relative; + padding: var(--s); + border: 1px solid var(--color-border); + border-radius: 4px; + background-color: var(--color-bg-info); + } + + .close-btn { + position: absolute; + top: 4px; + right: 4px; + } + + //.help-box-content {} + + :host-context([dir='rtl']) { + .close-btn { + right: unset; + left: 4px; + } + } +} diff --git a/src/app/ui/help-box/help-box.component.spec.ts b/src/app/ui/help-box/help-box.component.spec.ts new file mode 100644 index 0000000000..822a55cfa0 --- /dev/null +++ b/src/app/ui/help-box/help-box.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HelpBoxComponent } from './help-box.component'; +import { provideAnimations } from '@angular/platform-browser/animations'; + +describe('HelpBoxComponent', () => { + let component: HelpBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HelpBoxComponent], + providers: [provideAnimations()], + }).compileComponents(); + + fixture = TestBed.createComponent(HelpBoxComponent); + component = fixture.componentInstance; + component.lsKey = 'TEST_HELP_BOX'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show the help box by default', () => { + expect(component.isVisible()).toBe(true); + }); + + it('should hide the help box when close is clicked', () => { + component.onClose(); + expect(component.isVisible()).toBe(false); + }); +}); diff --git a/src/app/ui/help-box/help-box.component.ts b/src/app/ui/help-box/help-box.component.ts new file mode 100644 index 0000000000..d3e341b07b --- /dev/null +++ b/src/app/ui/help-box/help-box.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, Input, Signal, signal } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; +import { MatIconButton } from '@angular/material/button'; +import { expandFadeAnimation } from '../animations/expand.ani'; +import { lsGetBoolean, lsSetItem } from '../../util/ls-util'; + +@Component({ + selector: 'help-box', + templateUrl: './help-box.component.html', + styleUrls: ['./help-box.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [expandFadeAnimation], + imports: [MatIcon, MatIconButton], +}) +export class HelpBoxComponent { + @Input({ required: true }) lsKey!: string; + + isVisible: Signal = signal(true); + + ngOnInit(): void { + // Check localStorage to determine if the help box should be shown + const isDismissed = lsGetBoolean(this.lsKey, false); + (this.isVisible as any).set(!isDismissed); + } + + onClose(): void { + // Set the localStorage key to true to indicate the box was dismissed + lsSetItem(this.lsKey, true); + (this.isVisible as any).set(false); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 39989effac..ae25c8a444 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -939,8 +939,9 @@ "MONTH": "Month", "NO_TASKS": "Currently there are no tasks. Please add some tasks via the + Button in the top bar.", "NOW": "Now", - "PLAN_END_DAY": "Plan at end of day", - "PLAN_START_DAY": "Plan at start of day", + "INSERT_BEFORE": "Before", + "PLAN_END_DAY": "End of {{date}}", + "PLAN_START_DAY": "Start of {{date}}", "SHIFT_KEY_INFO": "Hold Shift to toggle day planning mode", "START": "Work Start", "TASK_PROJECTION_INFO": "Future projection of a scheduled repeatable task", diff --git a/src/styles/components/_overwrite-material.scss b/src/styles/components/_overwrite-material.scss index afdc55218e..1ae7da9908 100644 --- a/src/styles/components/_overwrite-material.scss +++ b/src/styles/components/_overwrite-material.scss @@ -163,6 +163,13 @@ mat-icon.mat-icon[svgicon] { justify-content: center; } +// Ensure icon flex behaviour stays consistent even before Angular Material +// injects the MatButton styles (which set min-height: fit-content lazily). +body .mat-mdc-button-base .mat-icon { + //flex-shrink: 1 !important; + min-height: auto !important; +} + // AUTOCOMPLETE // ----------------- .mdc-list-item__primary-text { @@ -186,6 +193,7 @@ mat-icon.mat-icon[svgicon] { box-shadow: var(--whiteframe-shadow-8dp) !important; cursor: grabbing !important; overflow: hidden; + pointer-events: none !important; &:hover, &:active { @@ -195,6 +203,28 @@ mat-icon.mat-icon[svgicon] { background: var(--bg-lightest) !important; } +// Schedule-style preview should always use accent border while dragging +.cdk-drag-preview.as-schedule-event-preview { +} + +.drag-preview-time-badge { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + font-size: 16px; + font-weight: bold; + border: 2px solid var(--c-primary); + border-radius: var(--card-border-radius); + background: var(--bg-lighter); + background: inherit; + z-index: 100; + pointer-events: none; + padding: 2px 6px; + box-shadow: var(--whiteframe-shadow-3dp); + white-space: nowrap; +} + .cdk-drag-placeholder { cursor: grabbing; opacity: 0.1;