ngx-dev-toolbar 0.0.2-1 → 0.0.2-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/project.json +1 -1
- package/src/components/button/button.component.scss +36 -0
- package/src/components/button/button.component.ts +36 -0
- package/src/components/icons/angular-icon.component.ts +35 -0
- package/src/components/icons/bug-icon.component.ts +27 -0
- package/src/components/icons/code-icon.component.ts +24 -0
- package/src/components/icons/database-icon.component.ts +27 -0
- package/src/components/icons/gauge-icon.component.ts +27 -0
- package/src/components/icons/gear-icon.component.ts +27 -0
- package/src/components/icons/git-branch-icon.component.ts +27 -0
- package/src/components/icons/icon.component.ts +120 -0
- package/src/components/icons/icon.models.ts +20 -0
- package/src/components/icons/layout-icon.component.ts +24 -0
- package/src/components/icons/lighting-icon.component.ts +24 -0
- package/src/components/icons/moon-icon.component.ts +27 -0
- package/src/components/icons/network-icon.component.ts +27 -0
- package/src/components/icons/puzzle-icon.component.ts +27 -0
- package/src/components/icons/refresh-icon.component.ts +27 -0
- package/src/components/icons/star-icon.component.ts +27 -0
- package/src/components/icons/sun-icon.component.ts +27 -0
- package/src/components/icons/terminal-icon.component.ts +27 -0
- package/src/components/icons/toggle-left-icon.component.ts +27 -0
- package/src/components/icons/users-icon.component.ts +27 -0
- package/src/components/input/input.component.ts +66 -0
- package/src/components/select/select.component.scss +83 -0
- package/src/components/select/select.component.ts +40 -0
- package/src/components/tool-button/tool-button.component.scss +67 -0
- package/src/components/tool-button/tool-button.component.ts +127 -0
- package/src/components/toolbar-tool/toolbar-tool.component.scss +9 -0
- package/src/components/toolbar-tool/toolbar-tool.component.ts +120 -0
- package/src/components/toolbar-tool/toolbar-tool.models.ts +9 -0
- package/src/components/window/window.component.scss +96 -0
- package/src/components/window/window.component.ts +79 -0
- package/src/components/window/window.models.ts +28 -0
- package/src/dev-toolbar-state.service.ts +84 -0
- package/src/dev-toolbar.component.scss +21 -0
- package/src/dev-toolbar.component.ts +102 -0
- package/src/index.ts +3 -1
- package/src/styles.scss +361 -0
- package/src/tools/feature-flags-tool/feature-flags-tool.component.ts +255 -0
- package/src/tools/feature-flags-tool/feature-flags.models.ts +10 -0
- package/src/tools/feature-flags-tool/feature-flags.service.ts +110 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/settings-tool/settings-tool.component.scss +61 -0
- package/src/tools/settings-tool/settings-tool.component.ts +76 -0
- package/src/tools/settings-tool/settings.models.ts +3 -0
- package/src/tools/settings-tool/settings.service.spec.ts +59 -0
- package/src/tools/settings-tool/settings.service.ts +21 -0
- package/src/utils/storage.service.ts +19 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Injectable, inject } from '@angular/core';
|
|
2
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
3
|
+
import { BehaviorSubject, Observable, combineLatest, map } from 'rxjs';
|
|
4
|
+
import { DevToolsStorageService } from '../../utils/storage.service';
|
|
5
|
+
import { Flag } from './feature-flags.models';
|
|
6
|
+
|
|
7
|
+
interface ForcedFlagsState {
|
|
8
|
+
enabled: string[];
|
|
9
|
+
disabled: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Injectable({ providedIn: 'root' })
|
|
13
|
+
export class DevToolbarFeatureFlagsService {
|
|
14
|
+
private readonly STORAGE_KEY = 'feature-flags';
|
|
15
|
+
private storageService = inject(DevToolsStorageService);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* App flags are the flags that are currently in the app
|
|
19
|
+
*/
|
|
20
|
+
private appFlags$ = new BehaviorSubject<Flag[]>([]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Forced flags are the flags that are currently forced in the app
|
|
24
|
+
*/
|
|
25
|
+
private forcedFlagsSubject = new BehaviorSubject<ForcedFlagsState>({
|
|
26
|
+
enabled: [],
|
|
27
|
+
disabled: [],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
private readonly forcedFlags$ = this.forcedFlagsSubject.asObservable();
|
|
31
|
+
/**
|
|
32
|
+
* Flags with the forced flag status
|
|
33
|
+
*/
|
|
34
|
+
public flags$: Observable<Flag[]> = combineLatest([
|
|
35
|
+
this.appFlags$,
|
|
36
|
+
this.forcedFlags$,
|
|
37
|
+
]).pipe(
|
|
38
|
+
map(([appFlags, { enabled, disabled }]) => {
|
|
39
|
+
return appFlags.map((flag) => ({
|
|
40
|
+
...flag,
|
|
41
|
+
isForced: enabled.includes(flag.id) || disabled.includes(flag.id),
|
|
42
|
+
isEnabled: enabled.includes(flag.id),
|
|
43
|
+
}));
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
public flags = toSignal(this.flags$, { initialValue: [] });
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
this.loadForcedFlags();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public set(flags: Flag[]): void {
|
|
54
|
+
this.appFlags$.next(flags);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This is used by the app that uses the dev toolbar and
|
|
59
|
+
* helps to set the flags that can be overridden by the dev toolbar
|
|
60
|
+
* @param flags - The flags to set
|
|
61
|
+
*/
|
|
62
|
+
public setAppFlags(flags: Flag[]): void {
|
|
63
|
+
this.appFlags$.next(flags);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getAppFlags(): Observable<Flag[]> {
|
|
67
|
+
return this.appFlags$.asObservable();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public setFlag(flagId: string, isEnabled: boolean): void {
|
|
71
|
+
const { enabled, disabled } = this.forcedFlagsSubject.value;
|
|
72
|
+
|
|
73
|
+
// Remove from both arrays first
|
|
74
|
+
const newEnabled = enabled.filter((id) => id !== flagId);
|
|
75
|
+
const newDisabled = disabled.filter((id) => id !== flagId);
|
|
76
|
+
|
|
77
|
+
// Add to appropriate array
|
|
78
|
+
if (isEnabled) {
|
|
79
|
+
newEnabled.push(flagId);
|
|
80
|
+
} else {
|
|
81
|
+
newDisabled.push(flagId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const newState = { enabled: newEnabled, disabled: newDisabled };
|
|
85
|
+
this.forcedFlagsSubject.next(newState);
|
|
86
|
+
this.storageService.set(this.STORAGE_KEY, newState);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public removeFlagOverride(flagId: string): void {
|
|
90
|
+
const { enabled, disabled } = this.forcedFlagsSubject.value;
|
|
91
|
+
|
|
92
|
+
const newState = {
|
|
93
|
+
enabled: enabled.filter((id) => id !== flagId),
|
|
94
|
+
disabled: disabled.filter((id) => id !== flagId),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this.forcedFlagsSubject.next(newState);
|
|
98
|
+
this.storageService.set(this.STORAGE_KEY, newState);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private loadForcedFlags(): void {
|
|
102
|
+
const savedFlags = this.storageService.get<ForcedFlagsState>(
|
|
103
|
+
this.STORAGE_KEY
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (savedFlags) {
|
|
107
|
+
this.forcedFlagsSubject.next(savedFlags);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
@use '../../styles' as *;
|
|
2
|
+
|
|
3
|
+
.settings {
|
|
4
|
+
padding: var(--devtools-spacing-md);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.instruction {
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: var(--devtools-spacing-md);
|
|
12
|
+
|
|
13
|
+
&__label {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: var(--devtools-spacing-xs);
|
|
17
|
+
|
|
18
|
+
&-text {
|
|
19
|
+
color: var(--devtools-text-primary);
|
|
20
|
+
font-size: var(--devtools-font-size-sm);
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&-description {
|
|
25
|
+
color: var(--devtools-text-muted);
|
|
26
|
+
font-size: var(--devtools-font-size-xs);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.theme {
|
|
32
|
+
display: flex;
|
|
33
|
+
gap: var(--devtools-spacing-xs);
|
|
34
|
+
|
|
35
|
+
&__button {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
padding: var(--devtools-spacing-xs);
|
|
40
|
+
border-radius: var(--devtools-border-radius-small);
|
|
41
|
+
border: 1px solid var(--devtools-border-subtle);
|
|
42
|
+
background: var(--devtools-bg-primary);
|
|
43
|
+
color: var(--devtools-text-primary);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
transition: var(--devtools-transition-default);
|
|
46
|
+
|
|
47
|
+
&:hover {
|
|
48
|
+
background: var(--devtools-hover-bg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&--active {
|
|
52
|
+
background: var(--devtools-primary);
|
|
53
|
+
color: var(--devtools-text-on-primary);
|
|
54
|
+
border-color: var(--devtools-primary);
|
|
55
|
+
|
|
56
|
+
&:hover {
|
|
57
|
+
background: var(--devtools-primary);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
inject,
|
|
5
|
+
input,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import { FormsModule } from '@angular/forms';
|
|
8
|
+
import { DevToolbarButtonComponent } from '../../components/button/button.component';
|
|
9
|
+
import { DevToolbarToolComponent } from '../../components/toolbar-tool/toolbar-tool.component';
|
|
10
|
+
import { WindowConfig } from '../../components/window/window.models';
|
|
11
|
+
import { DevToolbarStateService } from '../../dev-toolbar-state.service';
|
|
12
|
+
import { SettingsService } from './settings.service';
|
|
13
|
+
|
|
14
|
+
type ThemeType = 'light' | 'dark';
|
|
15
|
+
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'ndt-settings-tool',
|
|
18
|
+
standalone: true,
|
|
19
|
+
imports: [DevToolbarToolComponent, FormsModule, DevToolbarButtonComponent],
|
|
20
|
+
template: `
|
|
21
|
+
<ndt-toolbar-tool
|
|
22
|
+
[windowConfig]="windowConfig"
|
|
23
|
+
title="Settings"
|
|
24
|
+
icon="gear"
|
|
25
|
+
>
|
|
26
|
+
<section class="settings">
|
|
27
|
+
<div class="instruction">
|
|
28
|
+
<div class="instruction__label">
|
|
29
|
+
<span class="instruction__label-text">Theme</span>
|
|
30
|
+
<span class="instruction__label-description">
|
|
31
|
+
Switch between light and dark mode
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="instruction__control">
|
|
35
|
+
<div class="theme">
|
|
36
|
+
<ndt-button
|
|
37
|
+
[isActive]="!state.isDarkTheme()"
|
|
38
|
+
(click)="onThemeSelect('light')"
|
|
39
|
+
variant="icon"
|
|
40
|
+
ariaLabel="Switch to light theme"
|
|
41
|
+
icon="sun"
|
|
42
|
+
/>
|
|
43
|
+
<ndt-button
|
|
44
|
+
[isActive]="state.isDarkTheme()"
|
|
45
|
+
(click)="onThemeSelect('dark')"
|
|
46
|
+
variant="icon"
|
|
47
|
+
ariaLabel="Switch to dark theme"
|
|
48
|
+
icon="moon"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
</ndt-toolbar-tool>
|
|
55
|
+
`,
|
|
56
|
+
styleUrls: ['./settings-tool.component.scss'],
|
|
57
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
58
|
+
})
|
|
59
|
+
export class DevToolbarSettingsToolComponent {
|
|
60
|
+
state = inject(DevToolbarStateService);
|
|
61
|
+
settingsService = inject(SettingsService);
|
|
62
|
+
|
|
63
|
+
readonly badge = input<string | number>();
|
|
64
|
+
readonly windowConfig: WindowConfig = {
|
|
65
|
+
title: 'Settings',
|
|
66
|
+
isClosable: true,
|
|
67
|
+
id: 'ndt-settings',
|
|
68
|
+
description: 'Configure the settings for the Dev Toolbar',
|
|
69
|
+
isBeta: true,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
onThemeSelect(theme: ThemeType): void {
|
|
73
|
+
this.settingsService.setSettings({ isDarkMode: theme === 'dark' });
|
|
74
|
+
this.state.setTheme(theme);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { DevToolsStorageService } from '../../utils/storage.service';
|
|
3
|
+
import { SettingsService } from './settings.service';
|
|
4
|
+
|
|
5
|
+
describe('SettingsService', () => {
|
|
6
|
+
let service: SettingsService;
|
|
7
|
+
let storageServiceMock: DevToolsStorageService;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
storageServiceMock = {
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
set: vi.fn(),
|
|
13
|
+
} as unknown as DevToolsStorageService;
|
|
14
|
+
|
|
15
|
+
TestBed.configureTestingModule({
|
|
16
|
+
providers: [
|
|
17
|
+
SettingsService,
|
|
18
|
+
{ provide: DevToolsStorageService, useValue: storageServiceMock },
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
service = TestBed.inject(SettingsService);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should be created', () => {
|
|
26
|
+
expect(service).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('getSettings', () => {
|
|
30
|
+
it('should return settings from storage if they exist', () => {
|
|
31
|
+
const mockSettings = { isDarkMode: true };
|
|
32
|
+
vi.spyOn(storageServiceMock, 'get').mockReturnValue(mockSettings);
|
|
33
|
+
|
|
34
|
+
const result = service.getSettings();
|
|
35
|
+
|
|
36
|
+
expect(storageServiceMock.get).toHaveBeenCalledWith('settings');
|
|
37
|
+
expect(result).toEqual(mockSettings);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return default settings if nothing in storage', () => {
|
|
41
|
+
vi.spyOn(storageServiceMock, 'get').mockReturnValue(null);
|
|
42
|
+
|
|
43
|
+
const result = service.getSettings();
|
|
44
|
+
|
|
45
|
+
expect(storageServiceMock.get).toHaveBeenCalledWith('settings');
|
|
46
|
+
expect(result).toEqual({ isDarkMode: false });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('setSettings', () => {
|
|
51
|
+
it('should save settings to storage', () => {
|
|
52
|
+
const settings = { isDarkMode: true };
|
|
53
|
+
|
|
54
|
+
service.setSettings(settings);
|
|
55
|
+
|
|
56
|
+
expect(storageServiceMock.set).toHaveBeenCalledWith('settings', settings);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Injectable, inject } from '@angular/core';
|
|
2
|
+
import { DevToolsStorageService } from '../../utils/storage.service';
|
|
3
|
+
import { Settings } from './settings.models';
|
|
4
|
+
|
|
5
|
+
@Injectable({ providedIn: 'root' })
|
|
6
|
+
export class SettingsService {
|
|
7
|
+
private readonly STORAGE_KEY = 'settings';
|
|
8
|
+
private readonly storageService = inject(DevToolsStorageService);
|
|
9
|
+
|
|
10
|
+
public getSettings(): Settings {
|
|
11
|
+
return (
|
|
12
|
+
this.storageService.get<Settings>(this.STORAGE_KEY) || {
|
|
13
|
+
isDarkMode: false,
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public setSettings(settings: Settings): void {
|
|
19
|
+
this.storageService.set(this.STORAGE_KEY, settings);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable({ providedIn: 'root' })
|
|
4
|
+
export class DevToolsStorageService {
|
|
5
|
+
private readonly prefix = 'ndt-';
|
|
6
|
+
|
|
7
|
+
public set<T>(key: string, value: T): void {
|
|
8
|
+
localStorage.setItem(this.prefix + key, JSON.stringify(value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public get<T>(key: string): T | null {
|
|
12
|
+
const item = localStorage.getItem(this.prefix + key);
|
|
13
|
+
return item ? JSON.parse(item) : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public remove(key: string): void {
|
|
17
|
+
localStorage.removeItem(this.prefix + key);
|
|
18
|
+
}
|
|
19
|
+
}
|