ngx-dev-toolbar 1.0.0-beta.1 → 1.0.0

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 (116) hide show
  1. package/README.md +254 -4
  2. package/eslint.config.cjs +47 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +8 -23
  5. package/project.json +37 -0
  6. package/src/components/button/button.component.scss +49 -0
  7. package/src/components/button/button.component.ts +36 -0
  8. package/src/components/card/card.component.scss +18 -0
  9. package/src/components/card/card.component.ts +30 -0
  10. package/src/components/clickable-card/clickable-card.component.scss +39 -0
  11. package/src/components/clickable-card/clickable-card.component.ts +34 -0
  12. package/src/components/icons/angular-icon.component.ts +35 -0
  13. package/src/components/icons/bug-icon.component.ts +23 -0
  14. package/src/components/icons/code-icon.component.ts +24 -0
  15. package/src/components/icons/database-icon.component.ts +27 -0
  16. package/src/components/icons/discord-icon.component.ts +23 -0
  17. package/src/components/icons/docs-icon.component.ts +23 -0
  18. package/src/components/icons/export-icon.component.ts +23 -0
  19. package/src/components/icons/gauge-icon.component.ts +27 -0
  20. package/src/components/icons/gear-icon.component.ts +27 -0
  21. package/src/components/icons/git-branch-icon.component.ts +27 -0
  22. package/src/components/icons/icon.component.ts +129 -0
  23. package/src/components/icons/icon.models.ts +27 -0
  24. package/src/components/icons/import-icon.component.ts +23 -0
  25. package/src/components/icons/layout-icon.component.ts +24 -0
  26. package/src/components/icons/lightbulb-icon.component.ts +23 -0
  27. package/src/components/icons/lighting-icon.component.ts +24 -0
  28. package/src/components/icons/moon-icon.component.ts +27 -0
  29. package/src/components/icons/network-icon.component.ts +27 -0
  30. package/src/components/icons/puzzle-icon.component.ts +27 -0
  31. package/src/components/icons/refresh-icon.component.ts +27 -0
  32. package/src/components/icons/star-icon.component.ts +27 -0
  33. package/src/components/icons/sun-icon.component.ts +27 -0
  34. package/src/components/icons/terminal-icon.component.ts +27 -0
  35. package/src/components/icons/toggle-left-icon.component.ts +27 -0
  36. package/src/components/icons/translate-icon.component.ts +23 -0
  37. package/src/components/icons/trash-icon.component.ts +23 -0
  38. package/src/components/icons/users-icon.component.ts +27 -0
  39. package/src/components/input/input.component.ts +67 -0
  40. package/src/components/link-button/link-button.component.scss +36 -0
  41. package/src/components/link-button/link-button.component.ts +29 -0
  42. package/src/components/select/select.component.scss +162 -0
  43. package/src/components/select/select.component.ts +127 -0
  44. package/src/components/tool-button/tool-button.component.scss +67 -0
  45. package/src/components/tool-button/tool-button.component.ts +126 -0
  46. package/src/components/toolbar-tool/toolbar-tool.component.scss +9 -0
  47. package/src/components/toolbar-tool/toolbar-tool.component.ts +169 -0
  48. package/src/components/toolbar-tool/toolbar-tool.models.ts +33 -0
  49. package/src/components/window/window.component.scss +95 -0
  50. package/src/components/window/window.component.ts +69 -0
  51. package/src/dev-toolbar-state.service.ts +89 -0
  52. package/src/dev-toolbar.component.scss +22 -0
  53. package/src/dev-toolbar.component.ts +105 -0
  54. package/src/index.ts +10 -0
  55. package/src/models/dev-tools.interface.ts +19 -0
  56. package/src/styles.scss +342 -0
  57. package/src/test-setup.ts +12 -0
  58. package/src/tools/feature-flags-tool/feature-flags-internal.service.ts +96 -0
  59. package/src/tools/feature-flags-tool/feature-flags-tool.component.ts +261 -0
  60. package/src/tools/feature-flags-tool/feature-flags.models.ts +10 -0
  61. package/src/tools/feature-flags-tool/feature-flags.service.ts +28 -0
  62. package/src/tools/home-tool/home-tool.component.scss +67 -0
  63. package/src/tools/home-tool/home-tool.component.ts +197 -0
  64. package/{tools/settings-tool/settings.models.d.ts → src/tools/home-tool/settings.models.ts} +1 -1
  65. package/src/tools/home-tool/settings.service.spec.ts +59 -0
  66. package/src/tools/home-tool/settings.service.ts +21 -0
  67. package/src/tools/language-tool/language-internal.service.ts +51 -0
  68. package/src/tools/language-tool/language-tool.component.scss +7 -0
  69. package/src/tools/language-tool/language-tool.component.ts +71 -0
  70. package/src/tools/language-tool/language.models.ts +4 -0
  71. package/src/tools/language-tool/language.service.ts +26 -0
  72. package/src/utils/storage.service.spec.ts +179 -0
  73. package/src/utils/storage.service.ts +80 -0
  74. package/tsconfig.json +28 -0
  75. package/tsconfig.lib.json +28 -0
  76. package/tsconfig.lib.prod.json +9 -0
  77. package/tsconfig.spec.json +29 -0
  78. package/vite.config.mts +27 -0
  79. package/components/button/button.component.d.ts +0 -12
  80. package/components/icons/angular-icon.component.d.ts +0 -5
  81. package/components/icons/bug-icon.component.d.ts +0 -6
  82. package/components/icons/code-icon.component.d.ts +0 -6
  83. package/components/icons/database-icon.component.d.ts +0 -6
  84. package/components/icons/gauge-icon.component.d.ts +0 -6
  85. package/components/icons/gear-icon.component.d.ts +0 -6
  86. package/components/icons/git-branch-icon.component.d.ts +0 -6
  87. package/components/icons/icon.component.d.ts +0 -9
  88. package/components/icons/icon.models.d.ts +0 -1
  89. package/components/icons/layout-icon.component.d.ts +0 -6
  90. package/components/icons/lighting-icon.component.d.ts +0 -6
  91. package/components/icons/moon-icon.component.d.ts +0 -6
  92. package/components/icons/network-icon.component.d.ts +0 -6
  93. package/components/icons/puzzle-icon.component.d.ts +0 -6
  94. package/components/icons/refresh-icon.component.d.ts +0 -6
  95. package/components/icons/star-icon.component.d.ts +0 -6
  96. package/components/icons/sun-icon.component.d.ts +0 -6
  97. package/components/icons/terminal-icon.component.d.ts +0 -6
  98. package/components/icons/toggle-left-icon.component.d.ts +0 -6
  99. package/components/icons/users-icon.component.d.ts +0 -6
  100. package/components/input/input.component.d.ts +0 -10
  101. package/components/select/select.component.d.ts +0 -14
  102. package/components/tool-button/tool-button.component.d.ts +0 -23
  103. package/components/toolbar-tool/toolbar-tool.component.d.ts +0 -28
  104. package/components/window/window.component.d.ts +0 -16
  105. package/components/window/window.models.d.ts +0 -20
  106. package/dev-toolbar-state.service.d.ts +0 -18
  107. package/dev-toolbar.component.d.ts +0 -17
  108. package/fesm2022/ngx-dev-toolbar.mjs +0 -2073
  109. package/fesm2022/ngx-dev-toolbar.mjs.map +0 -1
  110. package/index.d.ts +0 -3
  111. package/tools/feature-flags-tool/feature-flags-tool.component.d.ts +0 -33
  112. package/tools/feature-flags-tool/feature-flags.models.d.ts +0 -9
  113. package/tools/feature-flags-tool/feature-flags.service.d.ts +0 -35
  114. package/tools/settings-tool/settings-tool.component.d.ts +0 -15
  115. package/tools/settings-tool/settings.service.d.ts +0 -10
  116. package/utils/storage.service.d.ts +0 -9
@@ -0,0 +1,261 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ inject,
6
+ signal,
7
+ } from '@angular/core';
8
+ import { FormsModule } from '@angular/forms';
9
+ import { DevToolbarInputComponent } from '../../components/input/input.component';
10
+ import { DevToolbarSelectComponent } from '../../components/select/select.component';
11
+ import { DevToolbarToolComponent } from '../../components/toolbar-tool/toolbar-tool.component';
12
+ import { DevToolbarWindowOptions } from '../../components/toolbar-tool/toolbar-tool.models';
13
+ import { DevToolbarInternalFeatureFlagService } from './feature-flags-internal.service';
14
+ import { DevToolbarFlag, FeatureFlagFilter } from './feature-flags.models';
15
+ @Component({
16
+ selector: 'ndt-feature-flags-tool',
17
+ standalone: true,
18
+ imports: [
19
+ FormsModule,
20
+ DevToolbarToolComponent,
21
+ DevToolbarInputComponent,
22
+ DevToolbarSelectComponent,
23
+ ],
24
+ template: `
25
+ <ndt-toolbar-tool
26
+ [options]="options"
27
+ title="Feature Flags"
28
+ icon="toggle-left"
29
+ >
30
+ <div class="container">
31
+ <div class="header">
32
+ <ndt-input
33
+ [value]="searchQuery()"
34
+ (valueChange)="onSearchChange($event)"
35
+ placeholder="Search..."
36
+ />
37
+ <ndt-select
38
+ [value]="activeFilter()"
39
+ [options]="filterOptions"
40
+ [size]="'medium'"
41
+ (valueChange)="onFilterChange($event)"
42
+ />
43
+ </div>
44
+
45
+ @if (hasNoFlags()) {
46
+ <div class="empty">
47
+ <p>No flags found</p>
48
+ </div>
49
+ } @else if (hasNoFilteredFlags()) {
50
+ <div class="empty">
51
+ <p>No flags found matching your filter</p>
52
+ </div>
53
+ } @else {
54
+ <div class="flag-list">
55
+ @for (flag of filteredFlags(); track flag.id) {
56
+ <div class="flag">
57
+ <div class="info">
58
+ <h3>{{ flag.name }}</h3>
59
+ <p>{{ flag?.description }}</p>
60
+ </div>
61
+
62
+ <ndt-select
63
+ [value]="getFlagValue(flag)"
64
+ [options]="flagValueOptions"
65
+ [ariaLabel]="'Set value for ' + flag.name"
66
+ (valueChange)="onFlagChange(flag.id, $event ?? '')"
67
+ size="small"
68
+ />
69
+ </div>
70
+ }
71
+ </div>
72
+ }
73
+ </div>
74
+ </ndt-toolbar-tool>
75
+ `,
76
+ styles: [
77
+ `
78
+ .container {
79
+ display: flex;
80
+ flex-direction: column;
81
+ height: 100%;
82
+ }
83
+
84
+ .header {
85
+ flex-shrink: 0;
86
+ display: flex;
87
+ gap: var(--ndt-spacing-sm);
88
+ margin-bottom: var(--ndt-spacing-md);
89
+
90
+ ndt-input {
91
+ flex: 0.65;
92
+ }
93
+
94
+ ndt-select {
95
+ flex: 0.35;
96
+ }
97
+ }
98
+
99
+ .empty {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: var(--ndt-spacing-md);
103
+ flex: 1;
104
+ min-height: 0;
105
+ justify-content: center;
106
+ align-items: center;
107
+ border: 1px solid var(--ndt-warning-border);
108
+ border-radius: var(--ndt-border-radius-medium);
109
+ padding: var(--ndt-spacing-md);
110
+ background: var(--ndt-warning-background);
111
+ color: var(--ndt-text-muted);
112
+ }
113
+
114
+ .flag-list {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: var(--ndt-spacing-md);
118
+ flex: 1;
119
+ min-height: 0;
120
+ overflow-y: auto;
121
+ padding-right: var(--ndt-spacing-sm);
122
+
123
+ &::-webkit-scrollbar {
124
+ width: 8px;
125
+ }
126
+
127
+ &::-webkit-scrollbar-track {
128
+ background: var(--ndt-background-secondary);
129
+ border-radius: 4px;
130
+ }
131
+
132
+ &::-webkit-scrollbar-thumb {
133
+ background: var(--ndt-border-primary);
134
+ border-radius: 4px;
135
+
136
+ &:hover {
137
+ background: var(--ndt-hover-bg);
138
+ }
139
+ }
140
+
141
+ scrollbar-width: thin;
142
+ scrollbar-color: var(--ndt-border-primary)
143
+ var(--ndt-background-secondary);
144
+ }
145
+
146
+ .flag {
147
+ display: flex;
148
+ flex-direction: row;
149
+ gap: var(--ndt-spacing-sm);
150
+ background: var(--ndt-background-secondary);
151
+ .info {
152
+ flex: 0 0 65%;
153
+ h3 {
154
+ margin: 0;
155
+ font-size: var(--ndt-font-size-md);
156
+ color: var(--ndt-text-primary);
157
+ }
158
+
159
+ p {
160
+ font-size: var(--ndt-font-size-xs);
161
+ color: var(--ndt-text-muted);
162
+ }
163
+ }
164
+
165
+ ndt-select {
166
+ flex: 0 0 35%;
167
+ }
168
+ }
169
+ `,
170
+ ],
171
+ changeDetection: ChangeDetectionStrategy.OnPush,
172
+ })
173
+ export class DevToolbarFeatureFlagsToolComponent {
174
+ // Injects
175
+ private readonly featureFlags = inject(DevToolbarInternalFeatureFlagService);
176
+
177
+ // Signals
178
+ protected readonly activeFilter = signal<FeatureFlagFilter>('all');
179
+ protected readonly searchQuery = signal<string>('');
180
+
181
+ protected readonly flags = this.featureFlags.flags;
182
+ protected readonly hasNoFlags = computed(() => this.flags().length === 0);
183
+ protected readonly filteredFlags = computed(() => {
184
+ return this.flags().filter((flag) => {
185
+ const searchTerm = this.searchQuery().toLowerCase();
186
+ const flagName = flag.name.toLowerCase();
187
+ const flagDescription = flag.description?.toLowerCase() ?? '';
188
+
189
+ const matchesSearch =
190
+ !this.searchQuery() ||
191
+ flagName.toLowerCase().includes(searchTerm.toLowerCase()) ||
192
+ flagDescription.toLowerCase().includes(searchTerm.toLowerCase());
193
+
194
+ const matchesFilter =
195
+ this.activeFilter() === 'all' ||
196
+ (this.activeFilter() === 'forced' && flag.isForced) ||
197
+ (this.activeFilter() === 'enabled' && flag.isEnabled) ||
198
+ (this.activeFilter() === 'disabled' && !flag.isEnabled);
199
+
200
+ return matchesSearch && matchesFilter;
201
+ });
202
+ });
203
+ protected readonly hasNoFilteredFlags = computed(
204
+ () => this.filteredFlags().length === 0
205
+ );
206
+
207
+ // Other properties
208
+ protected readonly options = {
209
+ title: 'Feature Flags',
210
+ description: 'Manage the feature flags for your current session',
211
+ isClosable: true,
212
+ size: 'tall',
213
+ id: 'ndt-feature-flags',
214
+ isBeta: true,
215
+ } as DevToolbarWindowOptions;
216
+
217
+ protected readonly filterOptions = [
218
+ { value: 'all', label: 'All Flags' },
219
+ { value: 'forced', label: 'Forced' },
220
+ { value: 'enabled', label: 'Enabled' },
221
+ { value: 'disabled', label: 'Disabled' },
222
+ ];
223
+
224
+ protected readonly flagValueOptions = [
225
+ { value: 'not-forced', label: 'Not Forced' },
226
+ { value: 'off', label: 'Forced Off' },
227
+ { value: 'on', label: 'Forced On' },
228
+ ];
229
+
230
+ // Public methods
231
+ onFilterChange(value: string | undefined): void {
232
+ const filter = this.filterOptions.find((f) => f.value === value);
233
+ if (filter) {
234
+ this.activeFilter.set(filter.value as FeatureFlagFilter);
235
+ }
236
+ }
237
+
238
+ onFlagChange(flagId: string, value: string): void {
239
+ switch (value) {
240
+ case 'not-forced':
241
+ this.featureFlags.removeFlagOverride(flagId);
242
+ break;
243
+ case 'on':
244
+ this.featureFlags.setFlag(flagId, true);
245
+ break;
246
+ case 'off':
247
+ this.featureFlags.setFlag(flagId, false);
248
+ break;
249
+ }
250
+ }
251
+
252
+ onSearchChange(query: string): void {
253
+ this.searchQuery.set(query);
254
+ }
255
+
256
+ // Protected methods
257
+ protected getFlagValue(flag: DevToolbarFlag): string {
258
+ if (!flag.isForced) return '';
259
+ return flag.isEnabled ? 'on' : 'off';
260
+ }
261
+ }
@@ -0,0 +1,10 @@
1
+ export interface DevToolbarFlag {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ link?: string;
6
+ isEnabled: boolean;
7
+ isForced: boolean;
8
+ }
9
+
10
+ export type FeatureFlagFilter = 'all' | 'forced' | 'enabled' | 'disabled';
@@ -0,0 +1,28 @@
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import { DevToolsService } from '../../models/dev-tools.interface';
4
+ import { DevToolbarInternalFeatureFlagService } from './feature-flags-internal.service';
5
+ import { DevToolbarFlag } from './feature-flags.models';
6
+
7
+ @Injectable({ providedIn: 'root' })
8
+ export class DevToolbarFeatureFlagService
9
+ implements DevToolsService<DevToolbarFlag>
10
+ {
11
+ private internalService = inject(DevToolbarInternalFeatureFlagService);
12
+
13
+ /**
14
+ * Sets the available flags that will be displayed in the tool on the dev toolbar
15
+ * @param flags The flags to be displayed
16
+ */
17
+ setAvailableOptions(flags: DevToolbarFlag[]): void {
18
+ this.internalService.setAppFlags(flags);
19
+ }
20
+
21
+ /**
22
+ * Gets the flags that were forced/modified through the tool on the dev toolbar
23
+ * @returns Observable of forced flags array
24
+ */
25
+ getForcedValues(): Observable<DevToolbarFlag[]> {
26
+ return this.internalService.getForcedFlags();
27
+ }
28
+ }
@@ -0,0 +1,67 @@
1
+ @use '../../styles' as *;
2
+
3
+ .settings {
4
+ display: flex;
5
+ flex-direction: column;
6
+ justify-content: space-between;
7
+ min-height: 100%;
8
+ }
9
+
10
+ .instruction {
11
+ display: flex;
12
+ justify-content: space-between;
13
+ align-items: flex-start;
14
+ gap: var(--ndt-spacing-md);
15
+
16
+ &__label {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: var(--ndt-spacing-xs);
20
+
21
+ &-text {
22
+ color: var(--ndt-text-primary);
23
+ font-size: var(--ndt-font-size-sm);
24
+ font-weight: 500;
25
+ }
26
+
27
+ &-description {
28
+ color: var(--ndt-text-muted);
29
+ font-size: var(--ndt-font-size-xs);
30
+ }
31
+ }
32
+
33
+ &__control {
34
+ flex: 1;
35
+ }
36
+ }
37
+
38
+ .instruction__control-button {
39
+ display: flex;
40
+ gap: var(--ndt-spacing-xs);
41
+ justify-content: flex-end;
42
+ }
43
+ .settings-container {
44
+ display: flex;
45
+ flex-direction: column;
46
+
47
+ .settings-actions {
48
+ display: flex;
49
+ gap: var(--ndt-spacing-md);
50
+ padding-block: var(--ndt-spacing-md);
51
+
52
+ > * {
53
+ width: 50%;
54
+ min-width: 0;
55
+ }
56
+ }
57
+ }
58
+
59
+ .footer-links {
60
+ border-top: 1px solid var(--ndt-border-subtle);
61
+ padding-top: 1em;
62
+ display: flex;
63
+ flex-direction: row;
64
+ justify-content: space-between;
65
+ gap: var(--ndt-spacing-lg);
66
+ // padding-block: var(--ndt-spacing-md);
67
+ }
@@ -0,0 +1,197 @@
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 { DevToolbarClickableCardComponent } from '../../components/clickable-card/clickable-card.component';
10
+ import { DevToolbarLinkButtonComponent } from '../../components/link-button/link-button.component';
11
+ import { DevToolbarToolComponent } from '../../components/toolbar-tool/toolbar-tool.component';
12
+ import { DevToolbarWindowOptions } from '../../components/toolbar-tool/toolbar-tool.models';
13
+ import { DevToolbarStateService } from '../../dev-toolbar-state.service';
14
+ import { DevToolsStorageService } from '../../utils/storage.service';
15
+ import { SettingsService } from './settings.service';
16
+
17
+ type ThemeType = 'light' | 'dark';
18
+
19
+ @Component({
20
+ selector: 'ndt-home-tool',
21
+ standalone: true,
22
+ imports: [
23
+ DevToolbarToolComponent,
24
+ FormsModule,
25
+ DevToolbarButtonComponent,
26
+ DevToolbarClickableCardComponent,
27
+ DevToolbarLinkButtonComponent,
28
+ ],
29
+ template: `
30
+ <ndt-toolbar-tool [options]="options" title="Home" icon="angular">
31
+ <section class="settings">
32
+ <div class="instruction">
33
+ <div class="instruction__label">
34
+ <span class="instruction__label-text">Theme</span>
35
+ <span class="instruction__label-description">
36
+ Switch between light and dark mode
37
+ </span>
38
+ </div>
39
+ <div class="instruction__control">
40
+ <div class="instruction__control-button">
41
+ <ndt-button
42
+ [isActive]="true"
43
+ (click)="onToggleTheme()"
44
+ variant="icon"
45
+ [ariaLabel]="
46
+ state.isDarkTheme()
47
+ ? 'Switch to light theme'
48
+ : 'Switch to dark theme'
49
+ "
50
+ [icon]="state.isDarkTheme() ? 'sun' : 'moon'"
51
+ />
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="settings-container">
57
+ <div class="instruction">
58
+ <div class="instruction__label">
59
+ <span class="instruction__label-text">Reset Settings</span>
60
+ <span class="instruction__label-description">
61
+ Reset all settings to their default values
62
+ </span>
63
+ </div>
64
+ <div class="instruction__control">
65
+ <div class="instruction__control-button">
66
+ <ndt-button
67
+ variant="icon"
68
+ icon="trash"
69
+ ariaLabel="Reset all settings"
70
+ (click)="onResetSettings()"
71
+ />
72
+ </div>
73
+ </div>
74
+ </div>
75
+ <div class="settings-actions">
76
+ <ndt-clickable-card
77
+ icon="export"
78
+ title="Export Settings"
79
+ subtitle="Export the current settings to share with other devs or use in your tests"
80
+ (click)="onExportSettings()"
81
+ />
82
+ <ndt-clickable-card
83
+ icon="import"
84
+ title="Import Settings"
85
+ subtitle="Import settings to reproduce a scenario"
86
+ (click)="onImportSettings()"
87
+ />
88
+ </div>
89
+ </div>
90
+
91
+ <div class="footer-links">
92
+ @for (link of links; track link.url) {
93
+ <ndt-link-button [icon]="link.icon" [url]="link.url">
94
+ {{ link.label }}
95
+ </ndt-link-button>
96
+ }
97
+ </div>
98
+ </section>
99
+ </ndt-toolbar-tool>
100
+ `,
101
+ styleUrls: ['./home-tool.component.scss'],
102
+ changeDetection: ChangeDetectionStrategy.OnPush,
103
+ })
104
+ export class DevToolbarHomeToolComponent {
105
+ protected readonly state = inject(DevToolbarStateService);
106
+ private readonly settingsService = inject(SettingsService);
107
+ private readonly storageService = inject(DevToolsStorageService);
108
+
109
+ readonly badge = input<string | number>();
110
+ readonly title = `Angular Dev Toolbar`;
111
+ readonly options: DevToolbarWindowOptions = {
112
+ title: this.title,
113
+ isClosable: true,
114
+ id: 'ndt-home',
115
+ size: 'medium',
116
+ description: '',
117
+ isBeta: true,
118
+ };
119
+
120
+ readonly links = [
121
+ {
122
+ icon: 'bug',
123
+ url: 'https://github.com/alfredoperez/ngx-dev-toolbar/issues',
124
+ label: 'Bug report',
125
+ },
126
+ {
127
+ icon: 'lightbulb',
128
+ url: 'https://github.com/alfredoperez/ngx-dev-toolbar/discussions',
129
+ label: 'Suggestions',
130
+ },
131
+ {
132
+ icon: 'docs',
133
+ url: 'https://alfredoperez.github.io/ngx-dev-toolbar/',
134
+ label: 'Docs',
135
+ },
136
+ {
137
+ icon: 'star',
138
+ url: 'https://github.com/alfredoperez/ngx-dev-toolbar',
139
+ label: 'Star on GitHub',
140
+ },
141
+ {
142
+ icon: 'discord',
143
+ url: 'https://discord.com/invite/angular',
144
+ label: 'Community',
145
+ },
146
+ ] as const;
147
+
148
+ onToggleTheme(): void {
149
+ const newTheme: ThemeType = this.state.isDarkTheme() ? 'light' : 'dark';
150
+ this.settingsService.setSettings({ isDarkMode: newTheme === 'dark' });
151
+ this.state.setTheme(newTheme);
152
+ }
153
+
154
+ onExportSettings(): void {
155
+ const settings = this.storageService.getAllSettings();
156
+ const blob = new Blob([JSON.stringify(settings, null, 2)], {
157
+ type: 'application/json',
158
+ });
159
+ const url = URL.createObjectURL(blob);
160
+ const link = document.createElement('a');
161
+ const timestamp = new Date().toISOString().split('T')[0];
162
+ link.href = url;
163
+ link.download = `ngx-dev-toolbar-settings-${timestamp}.json`;
164
+ document.body.appendChild(link);
165
+ link.click();
166
+ document.body.removeChild(link);
167
+ URL.revokeObjectURL(url);
168
+ }
169
+
170
+ onImportSettings(): void {
171
+ const input = document.createElement('input');
172
+ input.type = 'file';
173
+ input.accept = 'application/json';
174
+ input.onchange = (event) => {
175
+ const file = (event.target as HTMLInputElement).files?.[0];
176
+ if (file) {
177
+ const reader = new FileReader();
178
+ reader.onload = (e) => {
179
+ try {
180
+ const settings = JSON.parse(e.target?.result as string);
181
+ this.storageService.setAllSettings(settings);
182
+ window.location.reload();
183
+ } catch (error) {
184
+ console.error('Error importing settings:', error);
185
+ }
186
+ };
187
+ reader.readAsText(file);
188
+ }
189
+ };
190
+ input.click();
191
+ }
192
+
193
+ onResetSettings(): void {
194
+ this.storageService.clearAllSettings();
195
+ window.location.reload();
196
+ }
197
+ }
@@ -1,3 +1,3 @@
1
1
  export interface Settings {
2
- isDarkMode: boolean;
2
+ isDarkMode: boolean;
3
3
  }
@@ -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,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
+ }