chore(ui): removes dead components and features [#8260 - Tier A] (#8889)

This commit is contained in:
Lane Sawyer 2026-07-10 02:40:35 -07:00 committed by GitHub
parent d02f2271da
commit 6bb0472549
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2 additions and 1035 deletions

View file

@ -128,7 +128,7 @@ export const cssSelectors = {
// TASK DETAIL PANEL SELECTORS
// ============================================================================
RIGHT_PANEL: '.right-panel',
DETAIL_PANEL: 'dialog-task-detail-panel, task-detail-panel',
DETAIL_PANEL: 'task-detail-panel',
DETAIL_PANEL_BTN: '.show-additional-info-btn',
SCHEDULE_TASK_ITEM:
'task-detail-item:has(mat-icon:text("alarm")), task-detail-item:has(mat-icon:text("today")), task-detail-item:has(mat-icon:text("schedule"))',

View file

@ -4,7 +4,7 @@ import { fillTimeInput } from './time-input-helper';
// Selectors for scheduling
const DETAIL_PANEL_BTN = '.show-additional-info-btn';
const DETAIL_PANEL_SELECTOR = 'dialog-task-detail-panel, task-detail-panel';
const DETAIL_PANEL_SELECTOR = 'task-detail-panel';
const DETAIL_PANEL_SCHEDULE_ITEM =
'task-detail-item:has(mat-icon:text("alarm")), ' +
'task-detail-item:has(mat-icon:text("today")), ' +

View file

@ -15,7 +15,6 @@ const filesToFix = [
'src/app/features/schedule/map-schedule-data/insert-blocked-blocks-view-entries-for-schedule.ts',
'src/app/features/schedule/map-schedule-data/map-to-schedule-days.ts',
'src/app/features/task-repeat-cfg/sort-repeatable-task-cfg.ts',
'src/app/features/tasks/dialog-task-detail-panel/dialog-task-detail-panel.component.ts',
'src/app/features/work-context/store/work-context-meta.helper.ts',
'src/app/root-store/index.ts',
'src/app/ui/duration/input-duration-formly/input-duration-formly.component.ts',

View file

@ -1,55 +0,0 @@
:host {
display: contents;
}
// Mobile dropdown wrapper
.mobile-dropdown-wrapper {
position: relative;
}
// Mobile dropdown
.mobile-dropdown {
position: absolute;
display: flex;
transition: var(--transition-standard);
top: 100%;
flex-direction: column;
left: 50%;
transform: translateX(-50%);
z-index: 2;
pointer-events: none;
&.isVisible {
pointer-events: all;
}
button {
transition: var(--transition-standard);
transform: translateY(-100%);
opacity: 0;
position: relative;
z-index: 2;
margin-top: var(--s);
margin-left: 0;
@for $i from 2 through 7 {
&:nth-child(#{$i}) {
transform: translateY(#{$i * -100%});
}
}
}
&.isVisible button {
transform: translateY(0);
opacity: 1;
}
}
// Active button styles
button.active {
background-color: var(--sidenav-bg);
&.isCustomized {
background-color: var(--c-accent);
}
}

View file

@ -1,229 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
signal,
} from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu';
import { TranslateModule } from '@ngx-translate/core';
import { LayoutService } from '../../layout/layout.service';
import { TaskViewCustomizerService } from '../../../features/task-view-customizer/task-view-customizer.service';
import { TaskViewCustomizerPanelComponent } from '../../../features/task-view-customizer/task-view-customizer-panel/task-view-customizer-panel.component';
import { T } from '../../../t.const';
import { KeyboardConfig } from '@sp/keyboard-config';
import { GlobalConfigService } from '../../../features/config/global-config.service';
import { Store } from '@ngrx/store';
import {
selectActivePluginId,
selectIsShowPluginPanel,
} from '../../layout/store/layout.reducer';
import { toSignal } from '@angular/core/rxjs-interop';
import { PluginBridgeService } from '../../../plugins/plugin-bridge.service';
import { PluginIconComponent } from '../../../plugins/ui/plugin-icon/plugin-icon.component';
import { togglePluginPanel } from '../../layout/store/layout.actions';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, startWith } from 'rxjs/operators';
import { BreakpointObserver } from '@angular/cdk/layout';
@Component({
selector: 'mobile-side-panel-menu',
standalone: true,
imports: [
MatIconModule,
MatButtonModule,
MatTooltipModule,
MatMenuModule,
TranslateModule,
PluginIconComponent,
TaskViewCustomizerPanelComponent,
],
template: `
<div class="mobile-dropdown-wrapper">
<button
(click)="toggleMenu()"
mat-icon-button
[matTooltip]="T.MH.SIDE_PANEL_MENU | translate"
>
<mat-icon>{{ isShowMobileMenu() ? 'close' : 'view_sidebar' }}</mat-icon>
</button>
<div
class="mobile-dropdown"
[class.isVisible]="isShowMobileMenu()"
>
<!-- Plugin buttons -->
@for (button of sidePanelButtons(); track button.pluginId) {
<button
mat-mini-fab
color=""
[matTooltip]="button.label"
(click)="onPluginButtonClick(button)"
[class.active]="activePluginId() === button.pluginId && isShowPluginPanel()"
>
<plugin-icon
[pluginId]="button.pluginId"
[fallbackIcon]="button.icon || 'extension'"
></plugin-icon>
</button>
}
<!-- Task View Customizer -->
<button
mat-mini-fab
color=""
[class.isCustomized]="taskViewCustomizerService.isCustomized()"
[disabled]="!isWorkViewPage()"
[matMenuTriggerFor]="customizerPanel.menu"
[matTooltip]="T.GCF.KEYBOARD.TOGGLE_TASK_VIEW_CUSTOMIZER_PANEL | translate"
>
<mat-icon>filter_list</mat-icon>
</button>
<task-view-customizer-panel #customizerPanel></task-view-customizer-panel>
@if (isIssuesPanelEnabled()) {
<!-- Issue Panel -->
<button
mat-mini-fab
color=""
class="e2e-toggle-issue-provider-panel"
[class.active]="isShowIssuePanel()"
[disabled]="!isRouteWithSidePanel()"
(click)="toggleIssuePanel()"
[matTooltip]="T.MH.TOGGLE_SHOW_ISSUE_PANEL | translate"
>
<mat-icon>webhook</mat-icon>
</button>
}
@if (isProjectNotesEnabled()) {
<!-- Notes -->
<button
mat-mini-fab
color=""
class="e2e-toggle-notes-btn"
[class.active]="isShowNotes()"
[disabled]="!isRouteWithSidePanel()"
(click)="toggleNotes()"
[matTooltip]="T.MH.TOGGLE_SHOW_NOTES | translate"
>
<mat-icon>comment</mat-icon>
</button>
}
</div>
</div>
`,
styleUrls: ['./mobile-side-panel-menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MobileSidePanelMenuComponent {
readonly T = T;
readonly layoutService = inject(LayoutService);
readonly taskViewCustomizerService = inject(TaskViewCustomizerService);
readonly breakpointObserver = inject(BreakpointObserver);
private _globalConfigService = inject(GlobalConfigService);
private _pluginBridge = inject(PluginBridgeService);
private _store = inject(Store);
private _router = inject(Router);
// State signals
readonly isShowMobileMenu = signal(false);
// Convert observables to signals
readonly isRouteWithSidePanel = toSignal(
this._router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => true), // Always true since right-panel is now global
startWith(true), // Always true since right-panel is now global
),
{ initialValue: true },
);
readonly isWorkViewPage = toSignal(
this._router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => !!event.urlAfterRedirects.match(/tasks$/)),
startWith(!!this._router.url.match(/tasks$/)),
),
{ initialValue: !!this._router.url.match(/tasks$/) },
);
readonly kb: KeyboardConfig = this._globalConfigService.cfg()?.keyboard || {};
// Plugin-related signals
readonly sidePanelButtons = this._pluginBridge.sidePanelButtons;
readonly activePluginId = toSignal(this._store.select(selectActivePluginId));
readonly isShowPluginPanel = toSignal(this._store.select(selectIsShowPluginPanel));
// Navigation signals
readonly currentRoute = toSignal(
this._router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this._router.url),
),
{ initialValue: this._router.url },
);
readonly isWorkView = computed(() => {
const url = this.currentRoute();
return url.includes('/active/') || url.includes('/tag/') || url.includes('/project/');
});
// Panel state signals
readonly isShowNotes = computed(() => this.layoutService.isShowNotes());
readonly isShowIssuePanel = computed(() => this.layoutService.isShowIssuePanel());
readonly isIssuesPanelEnabled = computed(
() => this._globalConfigService.appFeatures().isIssuesPanelEnabled,
);
readonly isProjectNotesEnabled = computed(
() => this._globalConfigService.appFeatures().isProjectNotesEnabled,
);
readonly isShowTaskViewCustomizerPanel = computed(() =>
this.layoutService.isShowTaskViewCustomizerPanel(),
);
// Computed signal for active panel
readonly hasActivePanel = computed(() => {
return !!(
this.isShowNotes() ||
this.isShowIssuePanel() ||
this.isShowTaskViewCustomizerPanel() ||
(this.activePluginId() && this.isShowPluginPanel())
);
});
toggleMenu(): void {
this.isShowMobileMenu.update((v) => !v);
}
onPluginButtonClick(button: {
pluginId: string;
onClick?: () => void;
label?: string;
icon?: string;
}): void {
this._store.dispatch(togglePluginPanel(button.pluginId));
if (button.onClick) {
button.onClick();
}
// Close mobile menu after action
this.isShowMobileMenu.set(false);
}
toggleIssuePanel(): void {
this.layoutService.toggleAddTaskPanel();
this.isShowMobileMenu.set(false);
}
toggleNotes(): void {
this.layoutService.toggleNotes();
this.isShowMobileMenu.set(false);
}
}

View file

@ -1,196 +0,0 @@
import { WorkContextThemeCfg } from '../../features/work-context/work-context.model';
/**
* Pre-validated color palette that meets WCAG AA contrast standards (4.5:1)
* for both light and dark themes when used on standard backgrounds.
*/
export interface ValidatedColorPalette {
/** Display name for the palette */
name: string;
/** Theme configuration with validated colors */
theme: WorkContextThemeCfg;
/** Description for UI display */
description?: string;
}
/**
* Collection of pre-validated color palettes.
* All palettes are tested to meet WCAG AA contrast standards (4.5:1 ratio)
* on both light (#f8f8f7) and dark (#131314) backgrounds.
*/
export const VALIDATED_COLOR_PALETTES: readonly ValidatedColorPalette[] = [
{
name: 'Purple & Pink (Default)',
description: 'The classic Super Productivity theme',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#8b4a9d', // Darker purple for better contrast (was #a05db1)
huePrimary: '500',
accent: '#d81b60', // Darker pink for better contrast (was #ff4081)
hueAccent: '500',
warn: '#c62828', // Darker red for better contrast (was #e11826)
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Ocean Blue',
description: 'Professional blue tones',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#1976d2', // Material Blue 700
huePrimary: '500',
accent: '#0277bd', // Light Blue 800
hueAccent: '500',
warn: '#c62828', // Red 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Forest Green',
description: 'Calming natural greens',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#388e3c', // Green 700
huePrimary: '500',
accent: '#00796b', // Teal 700
hueAccent: '500',
warn: '#d32f2f', // Red 700
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Sunset Orange',
description: 'Warm and energetic',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#e64a19', // Deep Orange 700
huePrimary: '500',
accent: '#f57c00', // Orange 700
hueAccent: '500',
warn: '#c62828', // Red 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Deep Teal',
description: 'Modern and sophisticated',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#00796b', // Teal 700
huePrimary: '500',
accent: '#0097a7', // Cyan 700
hueAccent: '500',
warn: '#c62828', // Red 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Ruby Red',
description: 'Bold and striking',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#c62828', // Red 800
huePrimary: '500',
accent: '#ad1457', // Pink 800
hueAccent: '500',
warn: '#d84315', // Deep Orange 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Indigo Night',
description: 'Deep and focused',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#283593', // Indigo 800
huePrimary: '500',
accent: '#303f9f', // Indigo 700
hueAccent: '500',
warn: '#c62828', // Red 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Amber Glow',
description: 'Bright and cheerful',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#f57f17', // Yellow 800
huePrimary: '500',
accent: '#ef6c00', // Orange 800
hueAccent: '500',
warn: '#c62828', // Red 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Slate Gray',
description: 'Neutral and elegant',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#455a64', // Blue Gray 700
huePrimary: '500',
accent: '#546e7a', // Blue Gray 600
hueAccent: '500',
warn: '#c62828', // Red 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
{
name: 'Lime Fresh',
description: 'Vibrant and lively',
theme: {
isAutoContrast: true,
isDisableBackgroundTint: false,
primary: '#689f38', // Light Green 700
huePrimary: '500',
accent: '#7cb342', // Light Green 600
hueAccent: '500',
warn: '#c62828', // Red 800
hueWarn: '500',
backgroundImageDark: null,
backgroundImageLight: null,
},
},
] as const;
/**
* Get a validated palette by name
*/
export const getValidatedPalette = (name: string): ValidatedColorPalette | undefined => {
return VALIDATED_COLOR_PALETTES.find((p) => p.name === name);
};
/**
* Get the default validated palette
*/
export const getDefaultValidatedPalette = (): ValidatedColorPalette => {
return VALIDATED_COLOR_PALETTES[0];
};

View file

@ -1,34 +0,0 @@
<h1 mat-dialog-title>
{{
isEdit
? (T.F.TAG.D_EDIT.EDIT | translate: { title: title })
: (T.F.TAG.D_EDIT.ADD | translate: { title: title })
}}
</h1>
<mat-dialog-content>
<h3 class="mat-h3"></h3>
<div class="form-wrapper">
<chip-list-input
(addItem)="addTag($event)"
(addNewItem)="addNewTag($event)"
(removeItem)="removeTag($event)"
autoFocus
[label]="T.F.TAG.D_EDIT.LABEL | translate"
[model]="tagIds"
[suggestions]="tagSuggestions$ | async"
(ctrlEnterSubmit)="close()"
></chip-list-input>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
(click)="close()"
mat-button
type="submit"
>
{{ T.G.CLOSE | translate }}
</button>
</mat-dialog-actions>

View file

@ -1,24 +0,0 @@
@use '../../../../styles/_globals.scss' as *;
:host {
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
max-width: 600px;
@include mq(xs) {
width: 70vw;
}
:host-context([dir='rtl']) {
direction: rtl;
}
}
.form-wrapper {
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
}

View file

@ -1,100 +0,0 @@
import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogContent,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { T } from '../../../t.const';
import { TaskService } from '../../tasks/task.service';
import { DialogEditTagsForTaskPayload } from './dialog-edit-tags-for-task.payload';
import { truncate } from '../../../util/truncate';
import { TagService } from '../tag.service';
import { Task } from '../../tasks/task.model';
import { unique } from '../../../util/unique';
import { Observable, Subscription } from 'rxjs';
import { Tag } from '../tag.model';
import { ChipListInputComponent } from '../../../ui/chip-list-input/chip-list-input.component';
import { MatButton } from '@angular/material/button';
import { AsyncPipe } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import { addTag } from '../store/tag.actions';
@Component({
selector: 'dialog-edit-tags',
templateUrl: './dialog-edit-tags-for-task.component.html',
styleUrls: ['./dialog-edit-tags-for-task.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MatDialogTitle,
MatDialogContent,
ChipListInputComponent,
MatDialogActions,
MatButton,
AsyncPipe,
TranslatePipe,
],
})
export class DialogEditTagsForTaskComponent implements OnDestroy {
private _taskService = inject(TaskService);
private _tagService = inject(TagService);
private _store = inject(Store);
private _matDialogRef =
inject<MatDialogRef<DialogEditTagsForTaskComponent>>(MatDialogRef);
data = inject<DialogEditTagsForTaskPayload>(MAT_DIALOG_DATA);
T: typeof T = T;
title: string = truncate(this.data.task.title, 20);
task$: Observable<Task> = this._taskService.getByIdLive$(this.data.task.id);
task: Task = this.data.task;
tagIds: string[] = [...this.data.task.tagIds];
isEdit: boolean = this.data.task.tagIds && this.data.task.tagIds.length > 0;
tagSuggestions$: Observable<Tag[]> = this._tagService.tagsNoMyDayAndNoList$;
private _subs: Subscription = new Subscription();
constructor() {
this._subs.add(
this.task$.subscribe((task) => {
this.tagIds = task.tagIds;
this.task = task;
}),
);
}
ngOnDestroy(): void {
this._subs.unsubscribe();
}
close(): void {
this._matDialogRef.close();
}
addTag(id: string): void {
this._updateTags(unique([...this.tagIds, id]));
}
addNewTag(title: string): void {
const cleanTitle = (t: string): string => {
return t.replace('#', '');
};
const tag = this._tagService.createTagObject({ title: cleanTitle(title) });
this._store.dispatch(addTag({ tag }));
this._store.dispatch(
TaskSharedActions.addTagToTask({ tagId: tag.id, taskId: this.task.id }),
);
}
removeTag(id: string): void {
const updatedTagIds = this.tagIds.filter((tagId) => tagId !== id);
this._updateTags(updatedTagIds);
}
private _updateTags(newTagIds: string[]): void {
this._taskService.updateTags(this.task, unique(newTagIds));
}
}

View file

@ -1,5 +0,0 @@
import { Task } from '../../tasks/task.model';
export interface DialogEditTagsForTaskPayload {
task: Task;
}

View file

@ -1,19 +0,0 @@
<mat-dialog-content>
<!-- <h1>XXX {{(task$|async)?.id}}</h1>-->
@if (task$ | async; as task) {
<task-detail-panel
[task]="task"
[isDialogMode]="true"
></task-detail-panel>
}
</mat-dialog-content>
<!--<mat-dialog-actions align="center">-->
<!-- <button-->
<!-- mat-flat-button-->
<!-- color="primary"-->
<!-- mat-dialog-close-->
<!-- >-->
<!-- <mat-icon>close</mat-icon> {{T.G.CLOSE|translate}}-->
<!-- </button>-->
<!--</mat-dialog-actions>-->

View file

@ -1,31 +0,0 @@
@use '../../../../styles/_globals.scss' as *;
:host {
mat-dialog-content {
padding: 0;
border-radius: var(--card-border-radius);
max-width: 450px;
max-height: 75vh;
min-width: 340px;
background: var(--bg-lightest) !important;
@include mq(xxxs) {
min-width: 380px;
}
@include mq(xs) {
min-width: 450px;
max-height: 85vh;
}
}
&::ng-deep {
.task-title-wrapper {
padding-right: var(--s) !important;
}
task .hover-controls {
display: none !important;
}
}
}

View file

@ -1,58 +0,0 @@
import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
MAT_DIALOG_DATA,
MatDialogContent,
MatDialogRef,
} from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { selectSelectedTask } from '../store/task.selectors';
import { T } from 'src/app/t.const';
import { TranslateModule } from '@ngx-translate/core';
import { setSelectedTask } from '../store/task.actions';
import { TaskDetailTargetPanel } from '../task.model';
import { skipWhile } from 'rxjs/operators';
import { TaskDetailPanelComponent } from '../task-detail-panel/task-detail-panel.component';
@Component({
selector: 'dialog-task-detail-panel',
imports: [CommonModule, TranslateModule, MatDialogContent, TaskDetailPanelComponent],
templateUrl: './dialog-task-detail-panel.component.html',
styleUrl: './dialog-task-detail-panel.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DialogTaskDetailPanelComponent implements OnDestroy {
data = inject<{
taskId: string;
}>(MAT_DIALOG_DATA);
private _matDialogRef =
inject<MatDialogRef<DialogTaskDetailPanelComponent>>(MatDialogRef);
private _store = inject(Store);
T: typeof T = T;
task$ = this._store.select(selectSelectedTask).pipe(skipWhile((v) => !v));
constructor() {
const data = this.data;
this._store.dispatch(
setSelectedTask({
id: data.taskId,
taskDetailTargetPanel: TaskDetailTargetPanel.DONT_OPEN_PANEL,
isSkipToggle: true,
}),
);
// this.task$.subscribe((v) => TaskLog.log(`task$`, v));
}
// close(): void {
// this._matDialogRef.close();
// }
ngOnDestroy(): void {
this._store.dispatch(
setSelectedTask({
id: null,
}),
);
}
}

View file

@ -1,69 +0,0 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { PluginBridgeService } from '../plugin-bridge.service';
import { MatMenuItem } from '@angular/material/menu';
import { PluginIconComponent } from './plugin-icon/plugin-icon.component';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map } from 'rxjs/operators';
import { NavigationEnd, Router } from '@angular/router';
@Component({
selector: 'plugin-menu',
template: `
@for (menuEntry of menuEntries(); track menuEntry.pluginId + menuEntry.label) {
<button
mat-menu-item
class="plugin-menu-entry"
[class.isActive]="isActive(menuEntry.pluginId)"
(click)="menuEntry.onClick()"
>
<plugin-icon
[pluginId]="menuEntry.pluginId"
[fallbackIcon]="menuEntry.icon || 'extension'"
></plugin-icon>
<span>{{ menuEntry.label }}</span>
</button>
}
`,
styles: [
`
.plugin-menu-entry {
width: 100%;
justify-content: flex-start;
margin-bottom: 4px;
&.isActive {
color: var(--c-primary);
font-weight: bold;
mat-icon {
color: var(--c-primary);
}
}
mat-icon,
plugin-icon {
margin-right: 16px;
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MatMenuItem, PluginIconComponent],
})
export class PluginMenuComponent {
private readonly _pluginBridge = inject(PluginBridgeService);
private readonly _router = inject(Router);
readonly currentRoute = toSignal(
this._router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this._router.url),
),
{ initialValue: this._router.url },
);
readonly menuEntries = this._pluginBridge.menuEntries;
isActive(pluginId: string): boolean {
return this.currentRoute().includes(pluginId);
}
}

View file

@ -1,19 +0,0 @@
@if (isVisible()) {
<div
[@expandFade]
class="help-box"
>
<button
(click)="onClose()"
aria-label="Close help box"
class="close-btn"
mat-icon-button
type="button"
>
<mat-icon>close</mat-icon>
</button>
<div class="help-box-content">
<ng-content></ng-content>
</div>
</div>
}

View file

@ -1,33 +0,0 @@
:host {
display: block;
position: sticky;
top: var(--s2);
background: var(--bg);
z-index: 200;
margin: var(--s);
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: var(--card-border-radius);
background-color: var(--color-bg-info);
}
.close-btn {
position: absolute;
top: var(--s-half);
right: var(--s-half);
}
//.help-box-content {}
:host-context([dir='rtl']) {
.close-btn {
right: unset;
left: var(--s-half);
}
}
}

View file

@ -1,38 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
WritableSignal,
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 implements OnInit {
@Input({ required: true }) lsKey!: string;
isVisible: WritableSignal<boolean> = signal(true);
ngOnInit(): void {
// Check localStorage to determine if the help box should be shown
const isDismissed = lsGetBoolean(this.lsKey, false);
this.isVisible.set(!isDismissed);
}
onClose(): void {
// Set the localStorage key to true to indicate the box was dismissed
lsSetItem(this.lsKey, true);
this.isVisible.set(false);
}
}

View file

@ -1,41 +0,0 @@
import { AfterViewInit, Directive, ElementRef, OnDestroy, inject } from '@angular/core';
const DEFAULT_PINNED_CLASS = 'is-stuck';
@Directive({
selector: '[stuck]',
standalone: true,
})
export class StuckDirective implements AfterViewInit, OnDestroy {
private elRef = inject(ElementRef);
private _observer?: IntersectionObserver;
ngAfterViewInit(): void {
const el = this.elRef.nativeElement as HTMLElement;
this._observer = new IntersectionObserver(
// ([e]) => e.target.classList.toggle(DEFAULT_PINNED_CLASS, e.intersectionRatio < 1),
([e]) => {
e.target.classList.toggle(DEFAULT_PINNED_CLASS, !e.isIntersecting);
// Log.log(e.boundingClientRect.top, e);
// Log.log('top', e.boundingClientRect.top < 0, e.isIntersecting);
// if (e.boundingClientRect.top < 0) {
// e.target.classList.toggle(DEFAULT_PINNED_CLASS, e.isIntersecting);
// }
},
{
rootMargin: '0px',
threshold: 1,
},
);
this._observer.observe(el);
}
ngOnDestroy(): void {
if (!this._observer) {
throw new Error('Observer is not defined');
}
this._observer.unobserve(this.elRef.nativeElement as HTMLElement);
this._observer.disconnect();
}
}

View file

@ -1,81 +0,0 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
@Component({
selector: 'tree-drop-zones',
standalone: true,
template: `
<div
class="drop drop--before"
data-drop-zone="before"
[attr.data-node-id]="nodeId()"
[class.is-over]="overBefore()"
></div>
<div
class="drop drop--after"
data-drop-zone="after"
[attr.data-node-id]="nodeId()"
[class.is-over]="overAfter()"
></div>
`,
styles: [
`
.drop {
height: 50%;
pointer-events: none;
width: 100%;
background: transparent;
position: absolute;
z-index: 1;
:host-context(.tree.is-dragging) & {
pointer-events: all;
background: rgba(0, 123, 255, 0.1);
z-index: 10;
}
&:hover {
background: rgba(0, 123, 255, 0.15);
}
}
.drop--before,
.drop--after {
}
.drop--before {
top: 0px;
:host-context(.tree.is-dragging) & {
background: rgba(255, 165, 0, 0.1); /* Light orange when dragging */
}
&.is-over {
background: rgba(
255,
165,
0,
0.25
) !important; /* Darker orange when hovering */
}
}
.drop--after {
bottom: 0px;
:host-context(.tree.is-dragging) & {
background: rgba(34, 139, 34, 0.1); /* Light green when dragging */
}
&.is-over {
background: rgba(34, 139, 34, 0.25) !important; /* Darker green when hovering */
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TreeDropZonesComponent {
readonly nodeId = input.required<string>();
readonly overBefore = input(false);
readonly overAfter = input(false);
}