ngx-dev-toolbar 0.0.2-1 → 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 (57) hide show
  1. package/package.json +6 -3
  2. package/project.json +1 -1
  3. package/src/components/button/button.component.scss +36 -0
  4. package/src/components/button/button.component.ts +36 -0
  5. package/src/components/icons/angular-icon.component.ts +35 -0
  6. package/src/components/icons/bug-icon.component.ts +27 -0
  7. package/src/components/icons/code-icon.component.ts +24 -0
  8. package/src/components/icons/database-icon.component.ts +27 -0
  9. package/src/components/icons/gauge-icon.component.ts +27 -0
  10. package/src/components/icons/gear-icon.component.ts +27 -0
  11. package/src/components/icons/git-branch-icon.component.ts +27 -0
  12. package/src/components/icons/icon.component.ts +105 -0
  13. package/src/components/icons/icon.models.ts +21 -0
  14. package/src/components/icons/layout-icon.component.ts +24 -0
  15. package/src/components/icons/lighting-icon.component.ts +24 -0
  16. package/src/components/icons/moon-icon.component.ts +27 -0
  17. package/src/components/icons/network-icon.component.ts +27 -0
  18. package/src/components/icons/puzzle-icon.component.ts +27 -0
  19. package/src/components/icons/refresh-icon.component.ts +27 -0
  20. package/src/components/icons/star-icon.component.ts +27 -0
  21. package/src/components/icons/sun-icon.component.ts +27 -0
  22. package/src/components/icons/terminal-icon.component.ts +27 -0
  23. package/src/components/icons/toggle-left-icon.component.ts +27 -0
  24. package/src/components/icons/translate-icon.component.ts +23 -0
  25. package/src/components/icons/users-icon.component.ts +27 -0
  26. package/src/components/input/input.component.ts +66 -0
  27. package/src/components/select/select.component.scss +102 -0
  28. package/src/components/select/select.component.ts +40 -0
  29. package/src/components/tool-button/tool-button.component.scss +67 -0
  30. package/src/components/tool-button/tool-button.component.ts +126 -0
  31. package/src/components/toolbar-tool/toolbar-tool.component.scss +9 -0
  32. package/src/components/toolbar-tool/toolbar-tool.component.ts +128 -0
  33. package/src/components/toolbar-tool/toolbar-tool.models.ts +9 -0
  34. package/src/components/window/window.component.scss +99 -0
  35. package/src/components/window/window.component.ts +79 -0
  36. package/src/components/window/window.models.ts +28 -0
  37. package/src/dev-toolbar-state.service.ts +89 -0
  38. package/src/dev-toolbar.component.scss +22 -0
  39. package/src/dev-toolbar.component.ts +104 -0
  40. package/src/index.ts +6 -1
  41. package/src/models/dev-tools.interface.ts +19 -0
  42. package/src/styles.scss +363 -0
  43. package/src/tools/feature-flags-tool/feature-flags-internal.service.ts +96 -0
  44. package/src/tools/feature-flags-tool/feature-flags-tool.component.ts +262 -0
  45. package/src/tools/feature-flags-tool/feature-flags.models.ts +10 -0
  46. package/src/tools/feature-flags-tool/feature-flags.service.ts +26 -0
  47. package/src/tools/home-tool/home-tool.component.scss +61 -0
  48. package/src/tools/home-tool/home-tool.component.ts +72 -0
  49. package/src/tools/home-tool/settings.models.ts +3 -0
  50. package/src/tools/home-tool/settings.service.spec.ts +59 -0
  51. package/src/tools/home-tool/settings.service.ts +21 -0
  52. package/src/tools/language-tool/language-internal.service.ts +51 -0
  53. package/src/tools/language-tool/language-tool.component.scss +7 -0
  54. package/src/tools/language-tool/language-tool.component.ts +75 -0
  55. package/src/tools/language-tool/language.models.ts +4 -0
  56. package/src/tools/language-tool/language.service.ts +26 -0
  57. package/src/utils/storage.service.ts +19 -0
@@ -0,0 +1,363 @@
1
+ @use 'sass:map';
2
+ // Color primitives
3
+ $color-black-900: rgb(17, 24, 39); // darkest
4
+ $color-black-800: rgb(19, 21, 26);
5
+ $color-gray-700: rgb(55, 65, 81);
6
+ $color-gray-600: rgb(107, 114, 128);
7
+ $color-gray-500: rgb(156, 163, 175);
8
+ $color-gray-300: rgb(229, 231, 235);
9
+ $color-gray-200: rgb(243, 244, 246);
10
+ $color-white: rgb(255, 255, 255);
11
+ $color-red-600: rgb(220, 38, 38); // dark red
12
+ $color-red-500: rgb(239, 68, 68); // light red
13
+ $color-blue-100: rgb(219, 234, 254);
14
+ $color-blue-600: rgb(37, 99, 235);
15
+ $color-yellow-100: rgb(254, 249, 195);
16
+ $color-yellow-600: rgb(202, 138, 4);
17
+ $color-red-100: rgb(254, 226, 226);
18
+ // Colors and Themes
19
+ $colors: (
20
+ dark: (
21
+ background: (
22
+ primary: $color-black-900,
23
+ gradient:
24
+ linear-gradient(
25
+ 180deg,
26
+ $color-black-800 0%,
27
+ rgba($color-black-800, 0.88) 100%
28
+ ),
29
+ ),
30
+ text: (
31
+ primary: $color-white,
32
+ secondary: $color-gray-300,
33
+ muted: $color-gray-500,
34
+ ),
35
+ border: (
36
+ primary: #343841,
37
+ subtle: rgba($color-white, 0.1),
38
+ ),
39
+ hover: (
40
+ background: rgba($color-white, 0.12),
41
+ danger: $color-red-600,
42
+ ),
43
+ annotation: (
44
+ note: (
45
+ background: rgba($color-blue-600, 0.15),
46
+ border: rgba($color-blue-600, 0.3),
47
+ ),
48
+ warning: (
49
+ background: rgba($color-yellow-600, 0.15),
50
+ border: rgba($color-yellow-600, 0.3),
51
+ ),
52
+ error: (
53
+ background: rgba($color-red-600, 0.15),
54
+ border: rgba($color-red-600, 0.3),
55
+ ),
56
+ ),
57
+ ),
58
+ light: (
59
+ background: (
60
+ primary: $color-white,
61
+ gradient:
62
+ linear-gradient(
63
+ 180deg,
64
+ $color-gray-200 0%,
65
+ rgba($color-gray-200, 0.88) 100%
66
+ ),
67
+ ),
68
+ text: (
69
+ primary: $color-black-900,
70
+ secondary: $color-gray-700,
71
+ muted: $color-gray-600,
72
+ ),
73
+ border: (
74
+ primary: #e5e7eb,
75
+ subtle: rgba($color-black-900, 0.1),
76
+ ),
77
+ hover: (
78
+ background: rgba($color-black-900, 0.05),
79
+ danger: $color-red-500,
80
+ ),
81
+ annotation: (
82
+ note: (
83
+ background: $color-blue-100,
84
+ border: rgba($color-blue-600, 0.2),
85
+ ),
86
+ warning: (
87
+ background: $color-yellow-100,
88
+ border: rgba($color-yellow-600, 0.2),
89
+ ),
90
+ error: (
91
+ background: $color-red-100,
92
+ border: rgba($color-red-600, 0.2),
93
+ ),
94
+ ),
95
+ ),
96
+ );
97
+
98
+ // Shadows
99
+ $shadow-themes: (
100
+ dark: (
101
+ toolbar: 0 2px 8px rgba(19, 21, 26, 0.3),
102
+ tooltip: (
103
+ 0 0 0 1px rgba(255, 255, 255, 0.1),
104
+ 0 4px 8px rgba(0, 0, 0, 0.4),
105
+ 0 2px 4px rgba(0, 0, 0, 0.3),
106
+ ),
107
+ window: (
108
+ 0px 0px 0px 0px rgba(19, 21, 26, 0.3),
109
+ 0px 1px 2px 0px rgba(19, 21, 26, 0.29),
110
+ 0px 4px 4px 0px rgba(19, 21, 26, 0.26),
111
+ 0px 10px 6px 0px rgba(19, 21, 26, 0.15),
112
+ 0px 17px 7px 0px rgba(19, 21, 26, 0.04),
113
+ 0px 26px 7px 0px rgba(19, 21, 26, 0.01),
114
+ ),
115
+ ),
116
+ light: (
117
+ toolbar: 0 2px 8px rgba(156, 163, 175, 0.2),
118
+ tooltip: (
119
+ 0 0 0 1px rgba(17, 24, 39, 0.05),
120
+ 0 4px 8px rgba(107, 114, 128, 0.15),
121
+ 0 2px 4px rgba(107, 114, 128, 0.1),
122
+ ),
123
+ window: (
124
+ 0px 0px 0px 0px rgba(156, 163, 175, 0.1),
125
+ 0px 1px 2px 0px rgba(156, 163, 175, 0.12),
126
+ 0px 4px 4px 0px rgba(156, 163, 175, 0.1),
127
+ 0px 10px 6px 0px rgba(156, 163, 175, 0.08),
128
+ 0px 17px 7px 0px rgba(156, 163, 175, 0.05),
129
+ 0px 26px 7px 0px rgba(156, 163, 175, 0.02),
130
+ ),
131
+ ),
132
+ );
133
+
134
+ // Typography
135
+ $font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
136
+ 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
137
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
138
+
139
+ // Add font size variables
140
+ $font-sizes: (
141
+ xs: 0.75rem,
142
+ sm: 0.875rem,
143
+ md: 1rem,
144
+ lg: 1.25rem,
145
+ xl: 2rem,
146
+ );
147
+
148
+ // Dimensions
149
+ $dimensions: (
150
+ toolbar-height: 40px,
151
+ toolbar-button-width: 44px,
152
+ window-max-width: 640px,
153
+ window-max-height: 480px,
154
+ border-radius: (
155
+ small: 4px,
156
+ medium: 8px,
157
+ large: 12px,
158
+ full: 9999px,
159
+ ),
160
+ );
161
+
162
+ // Transitions
163
+ $transitions: (
164
+ default: all 0.2s ease-out,
165
+ smooth: all 0.2s ease-in-out,
166
+ );
167
+
168
+ // Z-indices
169
+ $z-indices: (
170
+ toolbar: 999999,
171
+ window: 999999999,
172
+ );
173
+
174
+ .dev-toolbar {
175
+ // Border radius variables
176
+ --devtools-border-radius-small: #{map.get(
177
+ map.get($dimensions, border-radius),
178
+ small
179
+ )};
180
+ --devtools-border-radius-medium: #{map.get(
181
+ map.get($dimensions, border-radius),
182
+ medium
183
+ )};
184
+ --devtools-border-radius-large: #{map.get(
185
+ map.get($dimensions, border-radius),
186
+ large
187
+ )};
188
+
189
+ // Transition variables
190
+ --devtools-transition-default: #{map.get($transitions, default)};
191
+ --devtools-transition-smooth: #{map.get($transitions, smooth)};
192
+
193
+ // Default light theme
194
+ --devtools-bg-primary: #{map.get(map.get($colors, light), background, primary)};
195
+ --devtools-bg-gradient: #{map.get(
196
+ map.get($colors, light),
197
+ background,
198
+ gradient
199
+ )};
200
+ --devtools-text-primary: #{map.get(map.get($colors, light), text, primary)};
201
+ --devtools-text-secondary: #{map.get(map.get($colors, light), text, secondary)};
202
+ --devtools-text-muted: #{map.get(map.get($colors, light), text, muted)};
203
+ --devtools-border-primary: #{map.get(map.get($colors, light), border, primary)};
204
+ --devtools-border-subtle: #{map.get(map.get($colors, light), border, subtle)};
205
+ --devtools-hover-bg: #{map.get(map.get($colors, light), hover, background)};
206
+ --devtools-hover-danger: #{map.get(map.get($colors, light), hover, danger)};
207
+ --devtools-shadow-toolbar: #{map.get(map.get($shadow-themes, light), toolbar)};
208
+ --devtools-shadow-tooltip: #{map.get(map.get($shadow-themes, light), tooltip)};
209
+ --devtools-shadow-window: #{map.get(map.get($shadow-themes, light), window)};
210
+
211
+ // Add these spacing variables
212
+ --devtools-spacing-xs: 4px;
213
+ --devtools-spacing-sm: 8px;
214
+ --devtools-spacing-md: 16px;
215
+ --devtools-window-padding: 24px;
216
+
217
+ // Add font size CSS variables
218
+ --devtools-font-size-xs: #{map.get($font-sizes, xs)};
219
+ --devtools-font-size-sm: #{map.get($font-sizes, sm)};
220
+ --devtools-font-size-md: #{map.get($font-sizes, md)};
221
+ --devtools-font-size-lg: #{map.get($font-sizes, lg)};
222
+ --devtools-font-size-xl: #{map.get($font-sizes, xl)};
223
+
224
+ // Background colors
225
+ --devtools-background-secondary: var(--devtools-bg-primary);
226
+ --devtools-background-hover: var(--devtools-hover-bg);
227
+
228
+ // Primary color for active states
229
+ --devtools-primary: #df30d4; // You can adjust this color value
230
+ --devtools-text-on-primary: #{$color-white};
231
+
232
+ // Border color
233
+ --devtools-border-color: var(--devtools-border-primary);
234
+
235
+ // Typography styles
236
+ h1,
237
+ h2,
238
+ h3,
239
+ h4,
240
+ h5 {
241
+ font-weight: 600;
242
+ color: var(--devtools-text-primary);
243
+ margin: 0;
244
+ }
245
+
246
+ h1 {
247
+ font-size: var(--devtools-font-size-xl);
248
+ }
249
+ h2 {
250
+ font-size: var(--devtools-font-size-lg);
251
+ }
252
+ h3 {
253
+ font-size: var(--devtools-font-size-md);
254
+ }
255
+ h4 {
256
+ font-size: var(--devtools-font-size-sm);
257
+ }
258
+ h5 {
259
+ font-size: var(--devtools-font-size-xs);
260
+ }
261
+
262
+ hr {
263
+ border: 1px solid var(--devtools-border-subtle);
264
+ margin: 1em 0;
265
+ }
266
+
267
+ p {
268
+ line-height: 1.5em;
269
+ margin: 0;
270
+ }
271
+ --devtools-note-background: #{map.get(
272
+ map.get(map.get($colors, light), annotation),
273
+ note,
274
+ background
275
+ )};
276
+ --devtools-note-border: #{map.get(
277
+ map.get(map.get($colors, light), annotation),
278
+ note,
279
+ border
280
+ )};
281
+ --devtools-warning-background: #{map.get(
282
+ map.get(map.get($colors, light), annotation),
283
+ warning,
284
+ background
285
+ )};
286
+ --devtools-warning-border: #{map.get(
287
+ map.get(map.get($colors, light), annotation),
288
+ warning,
289
+ border
290
+ )};
291
+ --devtools-error-background: #{map.get(
292
+ map.get(map.get($colors, light), annotation),
293
+ error,
294
+ background
295
+ )};
296
+ --devtools-error-border: #{map.get(
297
+ map.get(map.get($colors, light), annotation),
298
+ error,
299
+ border
300
+ )};
301
+
302
+ &[data-theme='dark'] {
303
+ --devtools-bg-primary: #{map.get(
304
+ map.get($colors, dark),
305
+ background,
306
+ primary
307
+ )};
308
+ --devtools-bg-gradient: #{map.get(
309
+ map.get($colors, dark),
310
+ background,
311
+ gradient
312
+ )};
313
+ --devtools-text-primary: #{map.get(map.get($colors, dark), text, primary)};
314
+ --devtools-text-secondary: #{map.get(
315
+ map.get($colors, dark),
316
+ text,
317
+ secondary
318
+ )};
319
+ --devtools-text-muted: #{map.get(map.get($colors, dark), text, muted)};
320
+ --devtools-border-primary: #{map.get(
321
+ map.get($colors, dark),
322
+ border,
323
+ primary
324
+ )};
325
+ --devtools-border-subtle: #{map.get(map.get($colors, dark), border, subtle)};
326
+ --devtools-hover-bg: #{map.get(map.get($colors, dark), hover, background)};
327
+ --devtools-hover-danger: #{map.get(map.get($colors, dark), hover, danger)};
328
+ --devtools-shadow-toolbar: #{map.get(map.get($shadow-themes, dark), toolbar)};
329
+ --devtools-shadow-tooltip: #{map.get(map.get($shadow-themes, dark), tooltip)};
330
+ --devtools-shadow-window: #{map.get(map.get($shadow-themes, dark), window)};
331
+
332
+ --devtools-note-background: #{map.get(
333
+ map.get(map.get($colors, dark), annotation),
334
+ note,
335
+ background
336
+ )};
337
+ --devtools-note-border: #{map.get(
338
+ map.get(map.get($colors, dark), annotation),
339
+ note,
340
+ border
341
+ )};
342
+ --devtools-warning-background: #{map.get(
343
+ map.get(map.get($colors, dark), annotation),
344
+ warning,
345
+ background
346
+ )};
347
+ --devtools-warning-border: #{map.get(
348
+ map.get(map.get($colors, dark), annotation),
349
+ warning,
350
+ border
351
+ )};
352
+ --devtools-error-background: #{map.get(
353
+ map.get(map.get($colors, dark), annotation),
354
+ error,
355
+ background
356
+ )};
357
+ --devtools-error-border: #{map.get(
358
+ map.get(map.get($colors, dark), annotation),
359
+ error,
360
+ border
361
+ )};
362
+ }
363
+ }
@@ -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
+ }
@@ -0,0 +1,262 @@
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 { WindowSize } from '../../components/window/window.models';
13
+ import { DevToolbarInternalFeatureFlagService } from './feature-flags-internal.service';
14
+ import { FeatureFlagFilter, Flag } from './feature-flags.models';
15
+
16
+ @Component({
17
+ selector: 'ndt-feature-flags-tool',
18
+ standalone: true,
19
+ imports: [
20
+ FormsModule,
21
+ DevToolbarToolComponent,
22
+ DevToolbarInputComponent,
23
+ DevToolbarSelectComponent,
24
+ ],
25
+ template: `
26
+ <ndt-toolbar-tool
27
+ [windowConfig]="windowConfig"
28
+ title="Feature Flags"
29
+ icon="toggle-left"
30
+ >
31
+ <div class="container">
32
+ <div class="header">
33
+ <ndt-input
34
+ [value]="searchQuery()"
35
+ (valueChange)="onSearchChange($event)"
36
+ placeholder="Search..."
37
+ />
38
+ <ndt-select
39
+ [value]="activeFilter()"
40
+ [options]="filterOptions"
41
+ [size]="'medium'"
42
+ (valueChange)="onFilterChange($event)"
43
+ />
44
+ </div>
45
+
46
+ @if (hasNoFlags()) {
47
+ <div class="empty">
48
+ <p>No flags found</p>
49
+ </div>
50
+ } @else if (hasNoFilteredFlags()) {
51
+ <div class="empty">
52
+ <p>No flags found matching your filter</p>
53
+ </div>
54
+ } @else {
55
+ <div class="flag-list">
56
+ @for (flag of filteredFlags(); track flag.id) {
57
+ <div class="flag">
58
+ <div class="info">
59
+ <h3>{{ flag.name }}</h3>
60
+ <p>{{ flag?.description }}</p>
61
+ </div>
62
+
63
+ <ndt-select
64
+ [value]="getFlagValue(flag)"
65
+ [options]="flagValueOptions"
66
+ [ariaLabel]="'Set value for ' + flag.name"
67
+ (valueChange)="onFlagChange(flag.id, $event ?? '')"
68
+ size="small"
69
+ />
70
+ </div>
71
+ }
72
+ </div>
73
+ }
74
+ </div>
75
+ </ndt-toolbar-tool>
76
+ `,
77
+ styles: [
78
+ `
79
+ .container {
80
+ display: flex;
81
+ flex-direction: column;
82
+ height: 100%;
83
+ }
84
+
85
+ .header {
86
+ flex-shrink: 0;
87
+ display: flex;
88
+ gap: var(--devtools-spacing-sm);
89
+ margin-bottom: var(--devtools-spacing-md);
90
+
91
+ ndt-input {
92
+ flex: 0.65;
93
+ }
94
+
95
+ ndt-select {
96
+ flex: 0.35;
97
+ }
98
+ }
99
+
100
+ .empty {
101
+ display: flex;
102
+ flex-direction: column;
103
+ gap: var(--devtools-spacing-md);
104
+ flex: 1;
105
+ min-height: 0;
106
+ justify-content: center;
107
+ align-items: center;
108
+ border: 1px solid var(--devtools-warning-border);
109
+ border-radius: var(--devtools-border-radius-medium);
110
+ padding: var(--devtools-spacing-md);
111
+ background: var(--devtools-warning-background);
112
+ color: var(--devtools-text-muted);
113
+ }
114
+
115
+ .flag-list {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: var(--devtools-spacing-md);
119
+ flex: 1;
120
+ min-height: 0;
121
+ overflow-y: auto;
122
+ padding-right: var(--devtools-spacing-sm);
123
+
124
+ &::-webkit-scrollbar {
125
+ width: 8px;
126
+ }
127
+
128
+ &::-webkit-scrollbar-track {
129
+ background: var(--devtools-background-secondary);
130
+ border-radius: 4px;
131
+ }
132
+
133
+ &::-webkit-scrollbar-thumb {
134
+ background: var(--devtools-border-primary);
135
+ border-radius: 4px;
136
+
137
+ &:hover {
138
+ background: var(--devtools-hover-bg);
139
+ }
140
+ }
141
+
142
+ scrollbar-width: thin;
143
+ scrollbar-color: var(--devtools-border-primary)
144
+ var(--devtools-background-secondary);
145
+ }
146
+
147
+ .flag {
148
+ display: flex;
149
+ flex-direction: row;
150
+ gap: var(--devtools-spacing-sm);
151
+ background: var(--devtools-background-secondary);
152
+ .info {
153
+ flex: 0 0 65%;
154
+ h3 {
155
+ margin: 0;
156
+ font-size: var(--devtools-font-size-md);
157
+ color: var(--devtools-text-primary);
158
+ }
159
+
160
+ p {
161
+ font-size: var(--devtools-font-size-xs);
162
+ color: var(--devtools-text-muted);
163
+ }
164
+ }
165
+
166
+ ndt-select {
167
+ flex: 0 0 35%;
168
+ }
169
+ }
170
+ `,
171
+ ],
172
+ changeDetection: ChangeDetectionStrategy.OnPush,
173
+ })
174
+ export class DevToolbarFeatureFlagsToolComponent {
175
+ // Injects
176
+ private readonly featureFlags = inject(DevToolbarInternalFeatureFlagService);
177
+
178
+ // Signals
179
+ protected readonly activeFilter = signal<FeatureFlagFilter>('all');
180
+ protected readonly searchQuery = signal<string>('');
181
+
182
+ protected readonly flags = this.featureFlags.flags;
183
+ protected readonly hasNoFlags = computed(() => this.flags().length === 0);
184
+ protected readonly filteredFlags = computed(() => {
185
+ return this.flags().filter((flag) => {
186
+ const searchTerm = this.searchQuery().toLowerCase();
187
+ const flagName = flag.name.toLowerCase();
188
+ const flagDescription = flag.description?.toLowerCase() ?? '';
189
+
190
+ const matchesSearch =
191
+ !this.searchQuery() ||
192
+ flagName.toLowerCase().includes(searchTerm.toLowerCase()) ||
193
+ flagDescription.toLowerCase().includes(searchTerm.toLowerCase());
194
+
195
+ const matchesFilter =
196
+ this.activeFilter() === 'all' ||
197
+ (this.activeFilter() === 'forced' && flag.isForced) ||
198
+ (this.activeFilter() === 'enabled' && flag.isEnabled) ||
199
+ (this.activeFilter() === 'disabled' && !flag.isEnabled);
200
+
201
+ return matchesSearch && matchesFilter;
202
+ });
203
+ });
204
+ protected readonly hasNoFilteredFlags = computed(
205
+ () => this.filteredFlags().length === 0
206
+ );
207
+
208
+ // Other properties
209
+ protected readonly windowConfig = {
210
+ title: 'Feature Flags',
211
+ description: 'Manage the feature flags for your current session',
212
+ isClosable: true,
213
+ size: 'tall' as WindowSize,
214
+ id: 'ndt-feature-flags',
215
+ isBeta: true,
216
+ };
217
+
218
+ protected readonly filterOptions = [
219
+ { value: 'all', label: 'All Flags' },
220
+ { value: 'forced', label: 'Forced' },
221
+ { value: 'enabled', label: 'Enabled' },
222
+ { value: 'disabled', label: 'Disabled' },
223
+ ];
224
+
225
+ protected readonly flagValueOptions = [
226
+ { value: 'not-forced', label: 'Not Forced' },
227
+ { value: 'off', label: 'Forced Off (false)' },
228
+ { value: 'on', label: 'Forced On (true)' },
229
+ ];
230
+
231
+ // Public methods
232
+ onFilterChange(value: string | undefined): void {
233
+ const filter = this.filterOptions.find((f) => f.value === value);
234
+ if (filter) {
235
+ this.activeFilter.set(filter.value as FeatureFlagFilter);
236
+ }
237
+ }
238
+
239
+ onFlagChange(flagId: string, value: string): void {
240
+ switch (value) {
241
+ case 'not-forced':
242
+ this.featureFlags.removeFlagOverride(flagId);
243
+ break;
244
+ case 'on':
245
+ this.featureFlags.setFlag(flagId, true);
246
+ break;
247
+ case 'off':
248
+ this.featureFlags.setFlag(flagId, false);
249
+ break;
250
+ }
251
+ }
252
+
253
+ onSearchChange(query: string): void {
254
+ this.searchQuery.set(query);
255
+ }
256
+
257
+ // Protected methods
258
+ protected getFlagValue(flag: Flag): string {
259
+ if (!flag.isForced) return '';
260
+ return flag.isEnabled ? 'on' : 'off';
261
+ }
262
+ }