ngx-dev-toolbar 0.0.2-2 → 0.0.2-3

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.
Files changed (30) hide show
  1. package/package.json +2 -1
  2. package/src/components/icons/icon.component.ts +42 -57
  3. package/src/components/icons/icon.models.ts +2 -1
  4. package/src/components/icons/translate-icon.component.ts +23 -0
  5. package/src/components/select/select.component.scss +25 -6
  6. package/src/components/tool-button/tool-button.component.scss +3 -3
  7. package/src/components/tool-button/tool-button.component.ts +3 -4
  8. package/src/components/toolbar-tool/toolbar-tool.component.ts +13 -5
  9. package/src/components/window/window.component.scss +17 -14
  10. package/src/components/window/window.models.ts +1 -1
  11. package/src/dev-toolbar-state.service.ts +6 -1
  12. package/src/dev-toolbar.component.scss +2 -1
  13. package/src/dev-toolbar.component.ts +18 -16
  14. package/src/index.ts +3 -0
  15. package/src/models/dev-tools.interface.ts +19 -0
  16. package/src/styles.scss +2 -0
  17. package/src/tools/feature-flags-tool/feature-flags-internal.service.ts +96 -0
  18. package/src/tools/feature-flags-tool/feature-flags-tool.component.ts +10 -3
  19. package/src/tools/feature-flags-tool/feature-flags.service.ts +13 -97
  20. package/src/tools/{settings-tool/settings-tool.component.ts → home-tool/home-tool.component.ts} +21 -25
  21. package/src/tools/language-tool/language-internal.service.ts +51 -0
  22. package/src/tools/language-tool/language-tool.component.scss +7 -0
  23. package/src/tools/language-tool/language-tool.component.ts +75 -0
  24. package/src/tools/language-tool/language.models.ts +4 -0
  25. package/src/tools/language-tool/language.service.ts +26 -0
  26. package/src/tools/index.ts +0 -5
  27. /package/src/tools/{settings-tool/settings-tool.component.scss → home-tool/home-tool.component.scss} +0 -0
  28. /package/src/tools/{settings-tool → home-tool}/settings.models.ts +0 -0
  29. /package/src/tools/{settings-tool → home-tool}/settings.service.spec.ts +0 -0
  30. /package/src/tools/{settings-tool → home-tool}/settings.service.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-dev-toolbar",
3
- "version": "0.0.2-2",
3
+ "version": "0.0.2-3",
4
4
  "peerDependencies": {
5
5
  "@angular/core": "^18.0.0 || ^19.0.0",
6
6
  "vite": "^5.0.0",
@@ -8,6 +8,7 @@
8
8
  "@nx/vite": "20.3.0",
9
9
  "@angular/forms": "^18.0.0 || ^19.0.0",
10
10
  "@angular/animations": "^18.0.0 || ^19.0.0",
11
+ "@angular/cdk": "^18.0.0 || ^19.0.0",
11
12
  "rxjs": "~7.8.0"
12
13
  },
13
14
  "sideEffects": false
@@ -24,6 +24,7 @@ import { StarIconComponent } from './star-icon.component';
24
24
  import { SunIconComponent } from './sun-icon.component';
25
25
  import { TerminalIconComponent } from './terminal-icon.component';
26
26
  import { ToggleLeftIconComponent } from './toggle-left-icon.component';
27
+ import { TranslateIconComponent } from './translate-icon.component';
27
28
  import { UsersIconComponent } from './users-icon.component';
28
29
 
29
30
  @Component({
@@ -48,65 +49,49 @@ import { UsersIconComponent } from './users-icon.component';
48
49
  UsersIconComponent,
49
50
  SunIconComponent,
50
51
  MoonIconComponent,
52
+ TranslateIconComponent,
51
53
  ],
52
54
  changeDetection: ChangeDetectionStrategy.OnPush,
53
55
  template: `
54
- @switch (name()) {
55
- @case ('angular') {
56
- <ndt-angular-icon />
57
- }
58
- @case ('bug') {
59
- <ndt-bug-icon [fill]="fill()" />
60
- }
61
- @case ('code') {
62
- <ndt-code-icon [fill]="fill()" />
63
- }
64
- @case ('database') {
65
- <ndt-database-icon [fill]="fill()" />
66
- }
67
- @case ('gauge') {
68
- <ndt-gauge-icon [fill]="fill()" />
69
- }
70
- @case ('gear') {
71
- <ndt-gear-icon [fill]="fill()" />
72
- }
73
- @case ('git-branch') {
74
- <ndt-git-branch-icon [fill]="fill()" />
75
- }
76
- @case ('layout') {
77
- <ndt-layout-icon [fill]="fill()" />
78
- }
79
- @case ('lighting') {
80
- <ndt-lighting-icon [fill]="fill()" />
81
- }
82
- @case ('network') {
83
- <ndt-network-icon [fill]="fill()" />
84
- }
85
- @case ('puzzle') {
86
- <ndt-puzzle-icon [fill]="fill()" />
87
- }
88
- @case ('refresh') {
89
- <ndt-refresh-icon [fill]="fill()" />
90
- }
91
- @case ('star') {
92
- <ndt-star-icon [fill]="fill()" />
93
- }
94
- @case ('terminal') {
95
- <ndt-terminal-icon [fill]="fill()" />
96
- }
97
- @case ('toggle-left') {
98
- <ndt-toggle-left-icon [fill]="fill()" />
99
- }
100
- @case ('user') {
101
- <ndt-users-icon [fill]="fill()" />
102
- }
103
- @case ('sun') {
104
- <ndt-sun-icon [fill]="fill()" />
105
- }
106
- @case ('moon') {
107
- <ndt-moon-icon [fill]="fill()" />
108
- }
109
- }
56
+ @switch (name()) { @case ('angular') {
57
+ <ndt-angular-icon />
58
+ } @case ('bug') {
59
+ <ndt-bug-icon [fill]="fill()" />
60
+ } @case ('code') {
61
+ <ndt-code-icon [fill]="fill()" />
62
+ } @case ('database') {
63
+ <ndt-database-icon [fill]="fill()" />
64
+ } @case ('gauge') {
65
+ <ndt-gauge-icon [fill]="fill()" />
66
+ } @case ('gear') {
67
+ <ndt-gear-icon [fill]="fill()" />
68
+ } @case ('git-branch') {
69
+ <ndt-git-branch-icon [fill]="fill()" />
70
+ } @case ('layout') {
71
+ <ndt-layout-icon [fill]="fill()" />
72
+ } @case ('lighting') {
73
+ <ndt-lighting-icon [fill]="fill()" />
74
+ } @case ('network') {
75
+ <ndt-network-icon [fill]="fill()" />
76
+ } @case ('puzzle') {
77
+ <ndt-puzzle-icon [fill]="fill()" />
78
+ } @case ('refresh') {
79
+ <ndt-refresh-icon [fill]="fill()" />
80
+ } @case ('star') {
81
+ <ndt-star-icon [fill]="fill()" />
82
+ } @case ('terminal') {
83
+ <ndt-terminal-icon [fill]="fill()" />
84
+ } @case ('toggle-left') {
85
+ <ndt-toggle-left-icon [fill]="fill()" />
86
+ } @case ('user') {
87
+ <ndt-users-icon [fill]="fill()" />
88
+ } @case ('sun') {
89
+ <ndt-sun-icon [fill]="fill()" />
90
+ } @case ('moon') {
91
+ <ndt-moon-icon [fill]="fill()" />
92
+ } @case ('translate') {
93
+ <ndt-translate-icon [fill]="fill()" />
94
+ } }
110
95
  `,
111
96
  })
112
97
  export class DevToolbarIconComponent {
@@ -115,6 +100,6 @@ export class DevToolbarIconComponent {
115
100
  name = input.required<IconName>();
116
101
 
117
102
  fill = computed(() =>
118
- this.stateService.theme() === 'dark' ? '#FFFFFF' : '#000000',
103
+ this.stateService.theme() === 'dark' ? '#FFFFFF' : '#000000'
119
104
  );
120
105
  }
@@ -17,4 +17,5 @@ export type IconName =
17
17
  | 'terminal'
18
18
  | 'toggle-left'
19
19
  | 'user'
20
- | 'flag';
20
+ | 'flag'
21
+ | 'translate';
@@ -0,0 +1,23 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'ndt-translate-icon',
5
+ standalone: true,
6
+ changeDetection: ChangeDetectionStrategy.OnPush,
7
+ template: `
8
+ <svg
9
+ [attr.fill]="fill()"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ width="24"
12
+ height="24"
13
+ viewBox="0 0 256 256"
14
+ >
15
+ <path
16
+ d="M250.73,210.63l-56-112a12,12,0,0,0-21.46,0l-20.52,41A84.2,84.2,0,0,1,114,126.22,107.48,107.48,0,0,0,139.33,68H160a12,12,0,0,0,0-24H108V32a12,12,0,0,0-24,0V44H32a12,12,0,0,0,0,24h83.13A83.69,83.69,0,0,1,96,110.35,84,84,0,0,1,83.6,91a12,12,0,1,0-21.81,10A107.55,107.55,0,0,0,78,126.24,83.54,83.54,0,0,1,32,140a12,12,0,0,0,0,24,107.47,107.47,0,0,0,64-21.07,108.4,108.4,0,0,0,45.39,19.44l-24.13,48.26a12,12,0,1,0,21.46,10.73L151.41,196h65.17l12.68,25.36a12,12,0,1,0,21.47-10.73ZM163.41,172,184,130.83,204.58,172Z"
17
+ ></path>
18
+ </svg>
19
+ `,
20
+ })
21
+ export class TranslateIconComponent {
22
+ fill = input<string>('#FFFF');
23
+ }
@@ -1,4 +1,6 @@
1
- .select {
1
+ @use '../../styles' as devtools;
2
+
3
+ select.select {
2
4
  width: 100%;
3
5
  cursor: pointer;
4
6
  min-width: 100px;
@@ -45,19 +47,36 @@
45
47
  &:invalid {
46
48
  border-color: var(--devtools-border-error, #ff4444);
47
49
  }
50
+
51
+ option {
52
+ background-color: var(--devtools-bg-primary);
53
+ color: var(--devtools-text-primary);
54
+ padding: var(--devtools-spacing-sm);
55
+ font-size: var(--devtools-font-size-sm);
56
+
57
+ &:hover,
58
+ &:focus {
59
+ background-color: var(--devtools-background-hover);
60
+ }
61
+ }
48
62
  }
49
63
 
50
64
  /* Style for Firefox */
51
65
  @-moz-document url-prefix() {
52
- .select {
66
+ select.select {
53
67
  text-indent: 0.01px;
54
68
  text-overflow: '';
55
69
  padding-right: 2.5em;
70
+
71
+ option {
72
+ background-color: var(--devtools-bg-primary) !important;
73
+ color: var(--devtools-text-primary) !important;
74
+ }
56
75
  }
57
76
  }
58
77
 
59
78
  /* Style for Webkit browsers */
60
- .select::-ms-expand {
79
+ select.select::-ms-expand {
61
80
  display: none;
62
81
  }
63
82
 
@@ -67,9 +86,9 @@ select::-ms-expand {
67
86
  }
68
87
 
69
88
  /* For Webkit browsers (Chrome, Safari) */
70
- select:-webkit-autofill,
71
- select:-webkit-autofill:hover,
72
- select:-webkit-autofill:focus {
89
+ select.select:-webkit-autofill,
90
+ select.select:-webkit-autofill:hover,
91
+ select.select:-webkit-autofill:focus {
73
92
  -webkit-box-shadow: 0 0 0px 1000px var(--devtools-bg-primary) inset !important;
74
93
  -webkit-text-fill-color: var(--devtools-text-primary) !important;
75
94
  }
@@ -16,13 +16,13 @@ $dimensions: devtools.$dimensions;
16
16
  opacity: 0.5;
17
17
  position: relative;
18
18
 
19
- &:hover {
19
+ &--active {
20
20
  background: var(--devtools-hover-bg);
21
21
  opacity: 1;
22
22
  }
23
23
 
24
- &--toolbar-visible {
25
- background: transparent;
24
+ &:hover {
25
+ background: var(--devtools-hover-bg);
26
26
  opacity: 1;
27
27
  }
28
28
 
@@ -23,7 +23,6 @@ import { DevToolbarStateService } from '../../dev-toolbar-state.service';
23
23
  template: `
24
24
  <button
25
25
  class="tool-button"
26
- [class.tool-button--toolbar-visible]="isToolbarVisible()"
27
26
  [class.tool-button--active]="isActive()"
28
27
  [class.tool-button--focus]="isFocused()"
29
28
  (mouseenter)="onMouseEnter()"
@@ -74,9 +73,9 @@ export class DevToolbarToolButtonComponent {
74
73
  // Inputs
75
74
  readonly title = input.required<string>();
76
75
  readonly toolId = input.required<string>();
76
+
77
77
  // Outputs
78
- // eslint-disable-next-line @angular-eslint/no-output-native
79
- readonly click = output<void>();
78
+ readonly open = output<void>();
80
79
 
81
80
  // Signals
82
81
  readonly isActive = computed(
@@ -102,7 +101,7 @@ export class DevToolbarToolButtonComponent {
102
101
  // Public methods
103
102
  onClick(): void {
104
103
  this.isFocused.set(false);
105
- this.click.emit();
104
+ this.open.emit();
106
105
  }
107
106
 
108
107
  onMouseEnter(): void {
@@ -81,21 +81,29 @@ export class DevToolbarToolComponent {
81
81
  );
82
82
  height = computed(() => {
83
83
  switch (this.windowConfig().size) {
84
- case 'tall':
85
- return 620;
84
+ case 'small':
85
+ return 320;
86
86
  case 'medium':
87
87
  return 480;
88
+ case 'tall':
89
+ return 620;
90
+ case 'large':
91
+ return 620;
88
92
  default:
89
- return 400;
93
+ return 480;
90
94
  }
91
95
  });
92
96
 
93
97
  width = computed(() => {
94
98
  switch (this.windowConfig().size) {
95
- case 'tall':
96
- return 520;
99
+ case 'small':
100
+ return 320;
97
101
  case 'medium':
98
102
  return 480;
103
+ case 'tall':
104
+ return 480;
105
+ case 'large':
106
+ return 620;
99
107
  default:
100
108
  return 400;
101
109
  }
@@ -28,20 +28,6 @@
28
28
  justify-content: space-between;
29
29
  align-items: flex-start;
30
30
 
31
- &__content {
32
- display: flex;
33
- flex-direction: column;
34
- }
35
-
36
- &__controls {
37
- display: flex;
38
- gap: var(--devtools-spacing-sm);
39
- }
40
-
41
- &__description {
42
- font-size: var(--devtools-font-size-sm);
43
- color: var(--devtools-text-muted);
44
- }
45
31
 
46
32
  &__title {
47
33
  display: flex;
@@ -60,6 +46,23 @@
60
46
  letter-spacing: 0.5px;
61
47
  }
62
48
  }
49
+ &__description {
50
+ font-size: var(--devtools-font-size-sm);
51
+ color: var(--devtools-text-muted);
52
+ }
53
+
54
+
55
+ &__content {
56
+ display: flex;
57
+ flex-direction: column;
58
+ }
59
+
60
+ &__controls {
61
+ display: flex;
62
+ gap: var(--devtools-spacing-sm);
63
+ }
64
+
65
+
63
66
  }
64
67
 
65
68
  .content {
@@ -6,7 +6,7 @@ export type WindowPlacement =
6
6
  | 'top-center'
7
7
  | 'top-right';
8
8
 
9
- export type WindowSize = 'tall' | 'medium';
9
+ export type WindowSize = 'small' | 'medium' | 'tall' | 'large';
10
10
  export interface WindowConfig {
11
11
  id: string;
12
12
  /**
@@ -22,12 +22,17 @@ export class DevToolbarStateService {
22
22
  });
23
23
 
24
24
  // Selectors
25
- readonly isVisible = computed(() => !this.state().isHidden);
25
+ readonly isVisible = computed(
26
+ () => !this.state().isHidden || this.hasActiveTool()
27
+ );
26
28
  readonly isDarkTheme = computed(() => this.state().theme === 'dark');
27
29
  readonly activeToolId = computed(() => this.state().activeToolId);
28
30
  readonly hasActiveTool = computed(() => this.state().activeToolId !== null);
29
31
  readonly error = computed(() => this.state().error);
30
32
  readonly theme = computed(() => this.state().theme);
33
+ /**
34
+ * The delay to hide the toolbar
35
+ */
31
36
  readonly delay = computed(() => this.state().delay);
32
37
 
33
38
  // State updates
@@ -9,11 +9,12 @@
9
9
  transform: translateX(-50%);
10
10
  display: flex;
11
11
  pointer-events: auto;
12
- background: var(--devtools-bg-gradient);
12
+ background: var(--devtools-bg-primary);
13
13
  border: 1px solid var(--devtools-border-primary);
14
14
  border-radius: map.get(map.get(devtools.$dimensions, border-radius), full);
15
15
  box-shadow: var(--devtools-shadow-toolbar);
16
16
  height: map.get(devtools.$dimensions, toolbar-height);
17
+ overflow: hidden;
17
18
 
18
19
  &--active {
19
20
  opacity: 1;
@@ -5,29 +5,33 @@ import {
5
5
  transition,
6
6
  trigger,
7
7
  } from '@angular/animations';
8
- import { Component, DestroyRef, OnInit, inject } from '@angular/core';
8
+ import {
9
+ Component,
10
+ DestroyRef,
11
+ OnInit,
12
+ inject,
13
+ isDevMode,
14
+ } from '@angular/core';
9
15
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
10
16
  import { fromEvent } from 'rxjs';
11
17
  import { filter, throttleTime } from 'rxjs/operators';
12
- import { DevToolbarIconComponent } from './components/icons/icon.component';
13
- import { DevToolbarToolButtonComponent } from './components/tool-button/tool-button.component';
14
18
  import { DevToolbarStateService } from './dev-toolbar-state.service';
15
19
  import { DevToolbarFeatureFlagsToolComponent } from './tools/feature-flags-tool/feature-flags-tool.component';
16
- import { DevToolbarSettingsToolComponent } from './tools/settings-tool/settings-tool.component';
17
- import { SettingsService } from './tools/settings-tool/settings.service';
20
+ import { DevToolbarHomeToolComponent } from './tools/home-tool/home-tool.component';
21
+ import { SettingsService } from './tools/home-tool/settings.service';
22
+ import { DevToolbarLanguageToolComponent } from './tools/language-tool/language-tool.component';
18
23
 
19
24
  @Component({
20
25
  standalone: true,
21
26
  selector: 'ndt-toolbar',
22
27
  styleUrls: ['./dev-toolbar.component.scss'],
23
28
  imports: [
24
- DevToolbarToolButtonComponent,
29
+ DevToolbarHomeToolComponent,
30
+ DevToolbarLanguageToolComponent,
25
31
  DevToolbarFeatureFlagsToolComponent,
26
- DevToolbarSettingsToolComponent,
27
- DevToolbarIconComponent,
28
32
  ],
29
-
30
33
  template: `
34
+ @if (isDevMode) {
31
35
  <div
32
36
  aria-label="Developer tools"
33
37
  role="toolbar"
@@ -37,15 +41,11 @@ import { SettingsService } from './tools/settings-tool/settings.service';
37
41
  [class.dev-toolbar--active]="state.isVisible()"
38
42
  (mouseenter)="onMouseEnter()"
39
43
  >
40
- <ndt-tool-button title="Home" toolId="ndt-home">
41
- <ndt-icon name="angular" />
42
- </ndt-tool-button>
43
- <ndt-tool-button title="Performance" toolId="ndt-performance">
44
- <ndt-icon name="gauge" />
45
- </ndt-tool-button>
44
+ <ndt-home-tool />
45
+ <ndt-language-tool />
46
46
  <ndt-feature-flags-tool />
47
- <ndt-settings-tool />
48
47
  </div>
48
+ }
49
49
  `,
50
50
  animations: [
51
51
  trigger('toolbarState', [
@@ -72,6 +72,8 @@ export class DevToolbarComponent implements OnInit {
72
72
  destroyRef = inject(DestroyRef);
73
73
  settingsService = inject(SettingsService);
74
74
 
75
+ isDevMode = isDevMode();
76
+
75
77
  private keyboardShortcut = fromEvent<KeyboardEvent>(window, 'keydown')
76
78
  .pipe(
77
79
  filter((event) => event.ctrlKey && event.shiftKey && event.key === 'D'),
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export * from './dev-toolbar.component';
2
+ export * from './models/dev-tools.interface';
2
3
  export * from './tools/feature-flags-tool/feature-flags.models';
3
4
  export * from './tools/feature-flags-tool/feature-flags.service';
5
+ export * from './tools/language-tool/language.models';
6
+ export * from './tools/language-tool/language.service';
@@ -0,0 +1,19 @@
1
+ import { Observable } from 'rxjs';
2
+
3
+ /**
4
+ * Interface that should be implemented by any tool service that is used in the dev toolbar
5
+ */
6
+ export interface DevToolsService<OptionType> {
7
+ /**
8
+ * Sets the available options that will be displayed in the tool on the dev toolbar
9
+ * @param options The options to be displayed
10
+ */
11
+ setAvailableOptions(options: OptionType[]): void;
12
+
13
+ /**
14
+ * Gets the values that were forced/modified through the tool on the dev toolbar.
15
+ * If the tool only supports a single option, the returned array will have a single element.
16
+ * @returns Observable of forced values array
17
+ */
18
+ getForcedValues(): Observable<OptionType[]>;
19
+ }
package/src/styles.scss CHANGED
@@ -240,6 +240,7 @@ $z-indices: (
240
240
  h5 {
241
241
  font-weight: 600;
242
242
  color: var(--devtools-text-primary);
243
+ margin: 0;
243
244
  }
244
245
 
245
246
  h1 {
@@ -265,6 +266,7 @@ $z-indices: (
265
266
 
266
267
  p {
267
268
  line-height: 1.5em;
269
+ margin: 0;
268
270
  }
269
271
  --devtools-note-background: #{map.get(
270
272
  map.get(map.get($colors, light), annotation),
@@ -0,0 +1,96 @@
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 DevToolbarInternalFeatureFlagService {
14
+ private readonly STORAGE_KEY = 'feature-flags';
15
+ private storageService = inject(DevToolsStorageService);
16
+
17
+ private appFlags$ = new BehaviorSubject<Flag[]>([]);
18
+ private forcedFlagsSubject = new BehaviorSubject<ForcedFlagsState>({
19
+ enabled: [],
20
+ disabled: [],
21
+ });
22
+
23
+ private readonly forcedFlags$ = this.forcedFlagsSubject.asObservable();
24
+
25
+ public flags$: Observable<Flag[]> = combineLatest([
26
+ this.appFlags$,
27
+ this.forcedFlags$,
28
+ ]).pipe(
29
+ map(([appFlags, { enabled, disabled }]) => {
30
+ return appFlags.map((flag) => ({
31
+ ...flag,
32
+ isForced: enabled.includes(flag.id) || disabled.includes(flag.id),
33
+ isEnabled: enabled.includes(flag.id),
34
+ }));
35
+ })
36
+ );
37
+
38
+ public flags = toSignal(this.flags$, { initialValue: [] });
39
+
40
+ constructor() {
41
+ this.loadForcedFlags();
42
+ }
43
+
44
+ setAppFlags(flags: Flag[]): void {
45
+ this.appFlags$.next(flags);
46
+ }
47
+
48
+ getAppFlags(): Observable<Flag[]> {
49
+ return this.appFlags$.asObservable();
50
+ }
51
+
52
+ getForcedFlags(): Observable<Flag[]> {
53
+ return this.flags$.pipe(
54
+ map((flags) => flags.filter((flag) => flag.isForced))
55
+ );
56
+ }
57
+
58
+ setFlag(flagId: string, isEnabled: boolean): void {
59
+ const { enabled, disabled } = this.forcedFlagsSubject.value;
60
+
61
+ const newEnabled = enabled.filter((id) => id !== flagId);
62
+ const newDisabled = disabled.filter((id) => id !== flagId);
63
+
64
+ if (isEnabled) {
65
+ newEnabled.push(flagId);
66
+ } else {
67
+ newDisabled.push(flagId);
68
+ }
69
+
70
+ const newState = { enabled: newEnabled, disabled: newDisabled };
71
+ this.forcedFlagsSubject.next(newState);
72
+ this.storageService.set(this.STORAGE_KEY, newState);
73
+ }
74
+
75
+ removeFlagOverride(flagId: string): void {
76
+ const { enabled, disabled } = this.forcedFlagsSubject.value;
77
+
78
+ const newState = {
79
+ enabled: enabled.filter((id) => id !== flagId),
80
+ disabled: disabled.filter((id) => id !== flagId),
81
+ };
82
+
83
+ this.forcedFlagsSubject.next(newState);
84
+ this.storageService.set(this.STORAGE_KEY, newState);
85
+ }
86
+
87
+ private loadForcedFlags(): void {
88
+ const savedFlags = this.storageService.get<ForcedFlagsState>(
89
+ this.STORAGE_KEY
90
+ );
91
+
92
+ if (savedFlags) {
93
+ this.forcedFlagsSubject.next(savedFlags);
94
+ }
95
+ }
96
+ }
@@ -10,8 +10,8 @@ import { DevToolbarInputComponent } from '../../components/input/input.component
10
10
  import { DevToolbarSelectComponent } from '../../components/select/select.component';
11
11
  import { DevToolbarToolComponent } from '../../components/toolbar-tool/toolbar-tool.component';
12
12
  import { WindowSize } from '../../components/window/window.models';
13
+ import { DevToolbarInternalFeatureFlagService } from './feature-flags-internal.service';
13
14
  import { FeatureFlagFilter, Flag } from './feature-flags.models';
14
- import { DevToolbarFeatureFlagsService } from './feature-flags.service';
15
15
 
16
16
  @Component({
17
17
  selector: 'ndt-feature-flags-tool',
@@ -47,6 +47,10 @@ import { DevToolbarFeatureFlagsService } from './feature-flags.service';
47
47
  <div class="empty">
48
48
  <p>No flags found</p>
49
49
  </div>
50
+ } @else if (hasNoFilteredFlags()) {
51
+ <div class="empty">
52
+ <p>No flags found matching your filter</p>
53
+ </div>
50
54
  } @else {
51
55
  <div class="flag-list">
52
56
  @for (flag of filteredFlags(); track flag.id) {
@@ -169,7 +173,7 @@ import { DevToolbarFeatureFlagsService } from './feature-flags.service';
169
173
  })
170
174
  export class DevToolbarFeatureFlagsToolComponent {
171
175
  // Injects
172
- private readonly featureFlags = inject(DevToolbarFeatureFlagsService);
176
+ private readonly featureFlags = inject(DevToolbarInternalFeatureFlagService);
173
177
 
174
178
  // Signals
175
179
  protected readonly activeFilter = signal<FeatureFlagFilter>('all');
@@ -197,6 +201,9 @@ export class DevToolbarFeatureFlagsToolComponent {
197
201
  return matchesSearch && matchesFilter;
198
202
  });
199
203
  });
204
+ protected readonly hasNoFilteredFlags = computed(
205
+ () => this.filteredFlags().length === 0
206
+ );
200
207
 
201
208
  // Other properties
202
209
  protected readonly windowConfig = {
@@ -216,7 +223,7 @@ export class DevToolbarFeatureFlagsToolComponent {
216
223
  ];
217
224
 
218
225
  protected readonly flagValueOptions = [
219
- { value: 'not-forced', label: 'Select an override' },
226
+ { value: 'not-forced', label: 'Not Forced' },
220
227
  { value: 'off', label: 'Forced Off (false)' },
221
228
  { value: 'on', label: 'Forced On (true)' },
222
229
  ];
@@ -1,110 +1,26 @@
1
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';
2
+ import { Observable } from 'rxjs';
3
+ import { DevToolsService } from '../../models/dev-tools.interface';
4
+ import { DevToolbarInternalFeatureFlagService } from './feature-flags-internal.service';
5
5
  import { Flag } from './feature-flags.models';
6
6
 
7
- interface ForcedFlagsState {
8
- enabled: string[];
9
- disabled: string[];
10
- }
11
-
12
7
  @Injectable({ providedIn: 'root' })
13
- export class DevToolbarFeatureFlagsService {
14
- private readonly STORAGE_KEY = 'feature-flags';
15
- private storageService = inject(DevToolsStorageService);
8
+ export class DevToolbarFeatureFlagService implements DevToolsService<Flag> {
9
+ private internalService = inject(DevToolbarInternalFeatureFlagService);
16
10
 
17
11
  /**
18
- * App flags are the flags that are currently in the app
12
+ * Sets the available flags that will be displayed in the tool on the dev toolbar
13
+ * @param flags The flags to be displayed
19
14
  */
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);
15
+ setAvailableOptions(flags: Flag[]): void {
16
+ this.internalService.setAppFlags(flags);
55
17
  }
56
18
 
57
19
  /**
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
20
+ * Gets the flags that were forced/modified through the tool on the dev toolbar
21
+ * @returns Observable of forced flags array
61
22
  */
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
- }
23
+ getForcedValues(): Observable<Flag[]> {
24
+ return this.internalService.getForcedFlags();
109
25
  }
110
26
  }
@@ -14,15 +14,11 @@ import { SettingsService } from './settings.service';
14
14
  type ThemeType = 'light' | 'dark';
15
15
 
16
16
  @Component({
17
- selector: 'ndt-settings-tool',
17
+ selector: 'ndt-home-tool',
18
18
  standalone: true,
19
19
  imports: [DevToolbarToolComponent, FormsModule, DevToolbarButtonComponent],
20
20
  template: `
21
- <ndt-toolbar-tool
22
- [windowConfig]="windowConfig"
23
- title="Settings"
24
- icon="gear"
25
- >
21
+ <ndt-toolbar-tool [windowConfig]="windowConfig" title="Home" icon="angular">
26
22
  <section class="settings">
27
23
  <div class="instruction">
28
24
  <div class="instruction__label">
@@ -34,18 +30,15 @@ type ThemeType = 'light' | 'dark';
34
30
  <div class="instruction__control">
35
31
  <div class="theme">
36
32
  <ndt-button
37
- [isActive]="!state.isDarkTheme()"
38
- (click)="onThemeSelect('light')"
33
+ [isActive]="true"
34
+ (click)="onToggleTheme()"
39
35
  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"
36
+ [ariaLabel]="
37
+ state.isDarkTheme()
38
+ ? 'Switch to light theme'
39
+ : 'Switch to dark theme'
40
+ "
41
+ [icon]="state.isDarkTheme() ? 'sun' : 'moon'"
49
42
  />
50
43
  </div>
51
44
  </div>
@@ -53,24 +46,27 @@ type ThemeType = 'light' | 'dark';
53
46
  </section>
54
47
  </ndt-toolbar-tool>
55
48
  `,
56
- styleUrls: ['./settings-tool.component.scss'],
49
+ styleUrls: ['./home-tool.component.scss'],
57
50
  changeDetection: ChangeDetectionStrategy.OnPush,
58
51
  })
59
- export class DevToolbarSettingsToolComponent {
52
+ export class DevToolbarHomeToolComponent {
60
53
  state = inject(DevToolbarStateService);
61
54
  settingsService = inject(SettingsService);
62
55
 
63
56
  readonly badge = input<string | number>();
57
+ readonly title = `Angular Dev Toolbar`;
64
58
  readonly windowConfig: WindowConfig = {
65
- title: 'Settings',
59
+ title: this.title,
66
60
  isClosable: true,
67
- id: 'ndt-settings',
68
- description: 'Configure the settings for the Dev Toolbar',
61
+ id: 'ndt-home',
62
+ size: 'medium',
63
+ description: '',
69
64
  isBeta: true,
70
65
  };
71
66
 
72
- onThemeSelect(theme: ThemeType): void {
73
- this.settingsService.setSettings({ isDarkMode: theme === 'dark' });
74
- this.state.setTheme(theme);
67
+ onToggleTheme(): void {
68
+ const newTheme: ThemeType = this.state.isDarkTheme() ? 'light' : 'dark';
69
+ this.settingsService.setSettings({ isDarkMode: newTheme === 'dark' });
70
+ this.state.setTheme(newTheme);
75
71
  }
76
72
  }
@@ -0,0 +1,51 @@
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { toSignal } from '@angular/core/rxjs-interop';
3
+ import { BehaviorSubject, Observable, map } from 'rxjs';
4
+ import { DevToolsStorageService } from '../../utils/storage.service';
5
+ import { Language } from './language.models';
6
+
7
+ @Injectable({ providedIn: 'root' })
8
+ export class DevToolbarInternalLanguageService {
9
+ private readonly STORAGE_KEY = 'language';
10
+ private readonly storageService = inject(DevToolsStorageService);
11
+
12
+ private languages$ = new BehaviorSubject<Language[]>([]);
13
+ private forcedLanguage$ = new BehaviorSubject<Language | null>(null);
14
+
15
+ public languages = toSignal(this.languages$, { initialValue: [] });
16
+
17
+ constructor() {
18
+ this.loadForcedLanguage();
19
+ }
20
+
21
+ setAppLanguages(languages: Language[]): void {
22
+ this.languages$.next(languages);
23
+ }
24
+
25
+ getAppLanguages(): Observable<Language[]> {
26
+ return this.languages$.asObservable();
27
+ }
28
+
29
+ setForcedLanguage(language: Language): void {
30
+ this.forcedLanguage$.next(language);
31
+ this.storageService.set(this.STORAGE_KEY, language);
32
+ }
33
+
34
+ getForcedLanguage(): Observable<Language[]> {
35
+ return this.forcedLanguage$.pipe(
36
+ map((language) => (language ? [language] : []))
37
+ );
38
+ }
39
+
40
+ removeForcedLanguage(): void {
41
+ this.forcedLanguage$.next(null);
42
+ this.storageService.remove(this.STORAGE_KEY);
43
+ }
44
+
45
+ private loadForcedLanguage(): void {
46
+ const savedLanguage = this.storageService.get<Language>(this.STORAGE_KEY);
47
+ if (savedLanguage) {
48
+ this.forcedLanguage$.next(savedLanguage);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,7 @@
1
+ .language-select {
2
+ display: flex;
3
+ flex-direction: row;
4
+ gap: 0.5rem;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ }
@@ -0,0 +1,75 @@
1
+ import { Component, inject, signal } from '@angular/core';
2
+ import { toSignal } from '@angular/core/rxjs-interop';
3
+ import { firstValueFrom, map } from 'rxjs';
4
+ import { DevToolbarSelectComponent } from '../../components/select/select.component';
5
+ import { DevToolbarToolComponent } from '../../components/toolbar-tool/toolbar-tool.component';
6
+ import { WindowSize } from '../../components/window/window.models';
7
+ import { DevToolbarInternalLanguageService } from './language-internal.service';
8
+ import { Language } from './language.models';
9
+
10
+ @Component({
11
+ selector: 'ndt-language-tool',
12
+ standalone: true,
13
+ imports: [DevToolbarToolComponent, DevToolbarSelectComponent],
14
+ styleUrls: ['./language-tool.component.scss'],
15
+ template: `
16
+ <ndt-toolbar-tool
17
+ title="Languages"
18
+ icon="translate"
19
+ [windowConfig]="windowConfig"
20
+ >
21
+ <div class="language-select">
22
+ <label for="language-select">Language</label>
23
+ <ndt-select
24
+ id="language-select"
25
+ [value]="activeLanguage()"
26
+ [options]="languageOptions()"
27
+ [size]="'medium'"
28
+ (valueChange)="onLanguageChange($event ?? '')"
29
+ />
30
+ </div>
31
+ </ndt-toolbar-tool>
32
+ `,
33
+ })
34
+ export class DevToolbarLanguageToolComponent {
35
+ private readonly languageService = inject(DevToolbarInternalLanguageService);
36
+
37
+ protected readonly windowConfig = {
38
+ title: 'Languages',
39
+ description: 'Set the language for your current session',
40
+ isClosable: true,
41
+ size: 'small' as WindowSize,
42
+ id: 'ndt-language',
43
+ isBeta: true,
44
+ };
45
+
46
+ activeLanguage = signal<string>('not-forced');
47
+
48
+ languageOptions = toSignal(
49
+ this.languageService.getAppLanguages().pipe(
50
+ map((languages) => [
51
+ { value: 'not-forced', label: 'Not Forced' },
52
+ ...languages.map(({ id: value, name: label }) => ({
53
+ value,
54
+ label,
55
+ })),
56
+ ])
57
+ ),
58
+ { initialValue: [] }
59
+ );
60
+
61
+ async onLanguageChange(language: string): Promise<void> {
62
+ if (language === 'not-forced' || !language) {
63
+ this.languageService.removeForcedLanguage();
64
+ return;
65
+ }
66
+
67
+ const languages = await firstValueFrom(
68
+ this.languageService.getAppLanguages()
69
+ );
70
+ const selectedLanguage = languages.find(({ id }) => id === language);
71
+ if (selectedLanguage) {
72
+ this.languageService.setForcedLanguage(selectedLanguage as Language);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,4 @@
1
+ export interface Language {
2
+ id: string;
3
+ name: string;
4
+ }
@@ -0,0 +1,26 @@
1
+ import { inject, Injectable } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import { DevToolsService } from '../../models/dev-tools.interface';
4
+ import { DevToolbarInternalLanguageService } from './language-internal.service';
5
+ import { Language } from './language.models';
6
+
7
+ @Injectable({ providedIn: 'root' })
8
+ export class DevToolbarLanguageService implements DevToolsService<Language> {
9
+ private internalService = inject(DevToolbarInternalLanguageService);
10
+
11
+ /**
12
+ * Sets the available languages that will be displayed in the tool on the dev toolbar
13
+ * @param languages The languages to be displayed
14
+ */
15
+ setAvailableOptions(languages: Language[]): void {
16
+ this.internalService.setAppLanguages(languages);
17
+ }
18
+
19
+ /**
20
+ * Gets the languages that were forced/modified through the tool on the dev toolbar
21
+ * @returns Observable of forced languages array
22
+ */
23
+ getForcedValues(): Observable<Language[]> {
24
+ return this.internalService.getForcedLanguage();
25
+ }
26
+ }
@@ -1,5 +0,0 @@
1
- export * from './feature-flags-tool/feature-flags-tool.component';
2
- export * from './feature-flags-tool/feature-flags.models';
3
- export * from './feature-flags-tool/feature-flags.service';
4
-
5
- export * from './settings-tool/settings-tool.component';