@memberjunction/ng-explorer-core 5.49.0 → 5.50.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 (27) hide show
  1. package/dist/lib/omnibar/omnibar-palette.component.js +98 -96
  2. package/dist/lib/omnibar/omnibar-palette.component.js.map +1 -1
  3. package/dist/lib/resource-wrappers/view-resource.component.d.ts +19 -16
  4. package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
  5. package/dist/lib/resource-wrappers/view-resource.component.js +127 -121
  6. package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
  7. package/dist/lib/shell/components/header/app-nav.component.d.ts +84 -9
  8. package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
  9. package/dist/lib/shell/components/header/app-nav.component.js +380 -41
  10. package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
  11. package/dist/lib/shell/components/header/app-switcher.component.d.ts +159 -29
  12. package/dist/lib/shell/components/header/app-switcher.component.d.ts.map +1 -1
  13. package/dist/lib/shell/components/header/app-switcher.component.js +822 -116
  14. package/dist/lib/shell/components/header/app-switcher.component.js.map +1 -1
  15. package/dist/lib/shell/services/sharing-center-dialog-host.component.d.ts +10 -1
  16. package/dist/lib/shell/services/sharing-center-dialog-host.component.d.ts.map +1 -1
  17. package/dist/lib/shell/services/sharing-center-dialog-host.component.js +30 -4
  18. package/dist/lib/shell/services/sharing-center-dialog-host.component.js.map +1 -1
  19. package/dist/lib/shell/shell.component.d.ts +3 -0
  20. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  21. package/dist/lib/shell/shell.component.js +22 -12
  22. package/dist/lib/shell/shell.component.js.map +1 -1
  23. package/dist/lib/shell/shell.module.d.ts +1 -1
  24. package/dist/lib/shell/shell.module.d.ts.map +1 -1
  25. package/dist/lib/shell/shell.module.js +3 -1
  26. package/dist/lib/shell/shell.module.js.map +1 -1
  27. package/package.json +51 -51
@@ -1,85 +1,433 @@
1
- import { Component, Input, Output, EventEmitter, HostListener, ViewChild } from '@angular/core';
2
- import { UUIDsEqual } from '@memberjunction/global';
1
+ import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
2
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
3
+ import { UserAppConfigContentComponent } from '@memberjunction/ng-explorer-settings';
4
+ import { UUIDsEqual, NormalizeUUID } from '@memberjunction/global';
5
+ import { UserInfoEngine } from '@memberjunction/core-entities';
6
+ import { Subject, takeUntil } from 'rxjs';
3
7
  import * as i0 from "@angular/core";
4
8
  import * as i1 from "@memberjunction/ng-base-application";
5
- import * as i2 from "@memberjunction/ng-ui-components";
6
- import * as i3 from "@memberjunction/ng-explorer-settings";
7
- const _c0 = ["appConfigDialog"];
8
- function AppSwitcherComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
9
- i0.ɵɵelement(0, "i", 3);
10
- } }
9
+ import * as i2 from "@angular/common";
10
+ import * as i3 from "@memberjunction/ng-ui-components";
11
+ import * as i4 from "@memberjunction/ng-explorer-settings";
12
+ const _c0 = ["filterInput"];
13
+ const _c1 = ["trigger"];
14
+ const _c2 = ["panel"];
15
+ const _c3 = (a0, a1) => ({ $implicit: a0, idx: a1 });
16
+ const _forTrack0 = ($index, $item) => $item.ID;
11
17
  function AppSwitcherComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
12
- i0.ɵɵelement(0, "i", 8);
18
+ i0.ɵɵelement(0, "i", 6);
19
+ } }
20
+ function AppSwitcherComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
21
+ i0.ɵɵelement(0, "i", 11);
13
22
  } if (rf & 2) {
14
23
  const ctx_r1 = i0.ɵɵnextContext();
15
24
  i0.ɵɵclassMap((ctx_r1.activeApp == null ? null : ctx_r1.activeApp.Icon) || "fa-solid fa-cube");
16
- i0.ɵɵstyleProp("color", ctx_r1.activeApp == null ? null : ctx_r1.activeApp.GetColor())("--active-color", ctx_r1.activeApp == null ? null : ctx_r1.activeApp.GetColor());
17
- } }
18
- function AppSwitcherComponent_Conditional_5_For_3_Conditional_1_Template(rf, ctx) { if (rf & 1) {
19
- i0.ɵɵelement(0, "i", 15);
25
+ i0.ɵɵstyleProp("color", ctx_r1.activeApp == null ? null : ctx_r1.activeApp.GetColor());
20
26
  } }
21
- function AppSwitcherComponent_Conditional_5_For_3_Conditional_2_Template(rf, ctx) { if (rf & 1) {
22
- i0.ɵɵelement(0, "i");
23
- } if (rf & 2) {
24
- const app_r5 = i0.ɵɵnextContext().$implicit;
25
- i0.ɵɵclassMap(app_r5.Icon || "fa-solid fa-cube");
27
+ function AppSwitcherComponent_Conditional_8_Conditional_2_Conditional_7_Template(rf, ctx) { if (rf & 1) {
28
+ const _r5 = i0.ɵɵgetCurrentView();
29
+ i0.ɵɵelementStart(0, "div", 19);
30
+ i0.ɵɵelement(1, "i", 21);
31
+ i0.ɵɵelementStart(2, "span", 22);
32
+ i0.ɵɵtext(3, "You have unsaved changes.");
33
+ i0.ɵɵelementEnd();
34
+ i0.ɵɵelementStart(4, "button", 23);
35
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Conditional_2_Conditional_7_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.ConfirmDiscard()); });
36
+ i0.ɵɵtext(5, "Discard");
37
+ i0.ɵɵelementEnd();
38
+ i0.ɵɵelementStart(6, "button", 24);
39
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Conditional_2_Conditional_7_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.CancelDiscard()); });
40
+ i0.ɵɵtext(7, "Keep editing");
41
+ i0.ɵɵelementEnd()();
26
42
  } }
27
- function AppSwitcherComponent_Conditional_5_For_3_Template(rf, ctx) { if (rf & 1) {
43
+ function AppSwitcherComponent_Conditional_8_Conditional_2_Template(rf, ctx) { if (rf & 1) {
28
44
  const _r4 = i0.ɵɵgetCurrentView();
29
- i0.ɵɵelementStart(0, "div", 14);
30
- i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_5_For_3_Template_div_click_0_listener() { const app_r5 = i0.ɵɵrestoreView(_r4).$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.selectApp(app_r5)); });
31
- i0.ɵɵconditionalCreate(1, AppSwitcherComponent_Conditional_5_For_3_Conditional_1_Template, 1, 0, "i", 15);
32
- i0.ɵɵconditionalCreate(2, AppSwitcherComponent_Conditional_5_For_3_Conditional_2_Template, 1, 2, "i", 16);
33
- i0.ɵɵelementStart(3, "span");
34
- i0.ɵɵtext(4);
45
+ i0.ɵɵelementStart(0, "div", 13)(1, "button", 14);
46
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Conditional_2_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.RequestExitConfigMode()); });
47
+ i0.ɵɵelement(2, "i", 15);
48
+ i0.ɵɵelementEnd();
49
+ i0.ɵɵelementStart(3, "h2", 16);
50
+ i0.ɵɵtext(4, "Configure apps");
51
+ i0.ɵɵelementEnd();
52
+ i0.ɵɵelementStart(5, "button", 17);
53
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Conditional_2_Template_button_click_5_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.CloseLauncher()); });
54
+ i0.ɵɵelement(6, "i", 18);
35
55
  i0.ɵɵelementEnd()();
56
+ i0.ɵɵconditionalCreate(7, AppSwitcherComponent_Conditional_8_Conditional_2_Conditional_7_Template, 8, 0, "div", 19);
57
+ i0.ɵɵelementStart(8, "mj-user-app-config-content", 20);
58
+ i0.ɵɵlistener("Saved", function AppSwitcherComponent_Conditional_8_Conditional_2_Template_mj_user_app_config_content_Saved_8_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.ExitConfigMode()); })("Cancelled", function AppSwitcherComponent_Conditional_8_Conditional_2_Template_mj_user_app_config_content_Cancelled_8_listener() { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.RequestExitConfigMode()); });
59
+ i0.ɵɵelementEnd();
36
60
  } if (rf & 2) {
37
- const app_r5 = ctx.$implicit;
38
61
  const ctx_r1 = i0.ɵɵnextContext(2);
39
- i0.ɵɵstyleProp("--item-color", app_r5.GetColor());
40
- i0.ɵɵclassProp("active", ctx_r1.IsActiveApp(app_r5))("loading", ctx_r1.IsLoadingApp(app_r5));
41
- i0.ɵɵproperty("mjClickable", app_r5.Name)("testId", "app-switcher-item-" + app_r5.Name);
42
- i0.ɵɵattribute("aria-current", ctx_r1.IsActiveApp(app_r5) ? "true" : null);
62
+ i0.ɵɵadvance(7);
63
+ i0.ɵɵconditional(ctx_r1.PendingDiscard ? 7 : -1);
43
64
  i0.ɵɵadvance();
44
- i0.ɵɵconditional(ctx_r1.IsLoadingApp(app_r5) ? 1 : -1);
65
+ i0.ɵɵproperty("Provider", ctx_r1.Provider);
66
+ } }
67
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_0_Template(rf, ctx) { if (rf & 1) {
68
+ const _r7 = i0.ɵɵgetCurrentView();
69
+ i0.ɵɵelementStart(0, "div", 25)(1, "div", 32);
70
+ i0.ɵɵelement(2, "i", 33);
71
+ i0.ɵɵelementStart(3, "input", 34, 3);
72
+ i0.ɵɵlistener("input", function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_0_Template_input_input_3_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.OnFilterInput($event)); })("keydown", function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_0_Template_input_keydown_3_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.OnFilterKeydown($event)); });
73
+ i0.ɵɵelementEnd()();
74
+ i0.ɵɵelementStart(5, "button", 17);
75
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_0_Template_button_click_5_listener() { i0.ɵɵrestoreView(_r7); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.CloseLauncher()); });
76
+ i0.ɵɵelement(6, "i", 18);
77
+ i0.ɵɵelementEnd()();
78
+ } if (rf & 2) {
79
+ const ctx_r1 = i0.ɵɵnextContext(3);
80
+ i0.ɵɵadvance(3);
81
+ i0.ɵɵproperty("value", ctx_r1.FilterText);
82
+ } }
83
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_For_2_ng_container_0_Template(rf, ctx) { if (rf & 1) {
84
+ i0.ɵɵelementContainer(0);
85
+ } }
86
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_For_2_Template(rf, ctx) { if (rf & 1) {
87
+ i0.ɵɵtemplate(0, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_For_2_ng_container_0_Template, 1, 0, "ng-container", 37);
88
+ } if (rf & 2) {
89
+ const app_r8 = ctx.$implicit;
90
+ const ɵ$index_68_r9 = ctx.$index;
91
+ i0.ɵɵnextContext(4);
92
+ const appCard_r10 = i0.ɵɵreference(10);
93
+ i0.ɵɵproperty("ngTemplateOutlet", appCard_r10)("ngTemplateOutletContext", i0.ɵɵpureFunction2(2, _c3, app_r8, ɵ$index_68_r9));
94
+ } }
95
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_Conditional_3_Template(rf, ctx) { if (rf & 1) {
96
+ i0.ɵɵelementStart(0, "div", 36);
97
+ i0.ɵɵtext(1);
98
+ i0.ɵɵelementEnd();
99
+ } if (rf & 2) {
100
+ const ctx_r1 = i0.ɵɵnextContext(4);
45
101
  i0.ɵɵadvance();
46
- i0.ɵɵconditional(!ctx_r1.IsLoadingApp(app_r5) ? 2 : -1);
102
+ i0.ɵɵtextInterpolate1("No apps match \"", ctx_r1.FilterText, "\"");
103
+ } }
104
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_Template(rf, ctx) { if (rf & 1) {
105
+ i0.ɵɵelementStart(0, "div", 35);
106
+ i0.ɵɵrepeaterCreate(1, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_For_2_Template, 1, 5, "ng-container", null, _forTrack0);
107
+ i0.ɵɵelementEnd();
108
+ i0.ɵɵconditionalCreate(3, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_Conditional_3_Template, 2, 1, "div", 36);
109
+ } if (rf & 2) {
110
+ const ctx_r1 = i0.ɵɵnextContext(3);
111
+ i0.ɵɵadvance();
112
+ i0.ɵɵrepeater(ctx_r1.VisibleApps);
47
113
  i0.ɵɵadvance(2);
48
- i0.ɵɵtextInterpolate(app_r5.Name);
114
+ i0.ɵɵconditional(ctx_r1.VisibleApps.length === 0 ? 3 : -1);
49
115
  } }
50
- function AppSwitcherComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
51
- const _r3 = i0.ɵɵgetCurrentView();
52
- i0.ɵɵelementStart(0, "div", 6)(1, "div", 9);
53
- i0.ɵɵrepeaterCreate(2, AppSwitcherComponent_Conditional_5_For_3_Template, 5, 12, "div", 10, i0.ɵɵrepeaterTrackByIdentity);
54
- i0.ɵɵelementEnd();
55
- i0.ɵɵelement(4, "div", 11);
56
- i0.ɵɵelementStart(5, "div", 12);
57
- i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_5_Template_div_click_5_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.openConfigDialog()); });
58
- i0.ɵɵelement(6, "i", 13);
59
- i0.ɵɵelementStart(7, "span");
60
- i0.ɵɵtext(8, "Configure...");
116
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_0_For_4_ng_container_0_Template(rf, ctx) { if (rf & 1) {
117
+ i0.ɵɵelementContainer(0);
118
+ } }
119
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_0_For_4_Template(rf, ctx) { if (rf & 1) {
120
+ i0.ɵɵtemplate(0, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_0_For_4_ng_container_0_Template, 1, 0, "ng-container", 37);
121
+ } if (rf & 2) {
122
+ const app_r12 = ctx.$implicit;
123
+ const ɵ$index_84_r13 = ctx.$index;
124
+ i0.ɵɵnextContext(5);
125
+ const appCard_r10 = i0.ɵɵreference(10);
126
+ i0.ɵɵproperty("ngTemplateOutlet", appCard_r10)("ngTemplateOutletContext", i0.ɵɵpureFunction2(2, _c3, app_r12, ɵ$index_84_r13));
127
+ } }
128
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_0_Template(rf, ctx) { if (rf & 1) {
129
+ i0.ɵɵelementStart(0, "div", 42);
130
+ i0.ɵɵtext(1, "Recent");
131
+ i0.ɵɵelementEnd();
132
+ i0.ɵɵelementStart(2, "div", 43);
133
+ i0.ɵɵrepeaterCreate(3, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_0_For_4_Template, 1, 5, "ng-container", null, _forTrack0);
134
+ i0.ɵɵelementEnd();
135
+ } if (rf & 2) {
136
+ const ctx_r1 = i0.ɵɵnextContext(4);
137
+ i0.ɵɵadvance(3);
138
+ i0.ɵɵrepeater(ctx_r1.RecentApps);
139
+ } }
140
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_For_8_ng_container_0_Template(rf, ctx) { if (rf & 1) {
141
+ i0.ɵɵelementContainer(0);
142
+ } }
143
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_For_8_Template(rf, ctx) { if (rf & 1) {
144
+ i0.ɵɵtemplate(0, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_For_8_ng_container_0_Template, 1, 0, "ng-container", 37);
145
+ } if (rf & 2) {
146
+ const app_r14 = ctx.$implicit;
147
+ const ɵ$index_100_r15 = ctx.$index;
148
+ const ctx_r1 = i0.ɵɵnextContext(4);
149
+ const appCard_r10 = i0.ɵɵreference(10);
150
+ i0.ɵɵproperty("ngTemplateOutlet", appCard_r10)("ngTemplateOutletContext", i0.ɵɵpureFunction2(2, _c3, app_r14, ɵ$index_100_r15 + ctx_r1.RecentApps.length));
151
+ } }
152
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_9_Template(rf, ctx) { if (rf & 1) {
153
+ i0.ɵɵelementStart(0, "div", 36);
154
+ i0.ɵɵtext(1, " No applications are enabled for you yet \u2014 use Configure\u2026 below to add some. ");
155
+ i0.ɵɵelementEnd();
156
+ } }
157
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Template(rf, ctx) { if (rf & 1) {
158
+ const _r11 = i0.ɵɵgetCurrentView();
159
+ i0.ɵɵconditionalCreate(0, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_0_Template, 5, 0);
160
+ i0.ɵɵelementStart(1, "div", 38)(2, "span");
161
+ i0.ɵɵtext(3, "All apps");
162
+ i0.ɵɵelementEnd();
163
+ i0.ɵɵelementStart(4, "button", 39);
164
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r11); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.ToggleSortMode()); });
165
+ i0.ɵɵelement(5, "i", 40);
166
+ i0.ɵɵelementEnd()();
167
+ i0.ɵɵelementStart(6, "div", 41);
168
+ i0.ɵɵrepeaterCreate(7, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_For_8_Template, 1, 5, "ng-container", null, _forTrack0);
169
+ i0.ɵɵelementEnd();
170
+ i0.ɵɵconditionalCreate(9, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Conditional_9_Template, 2, 0, "div", 36);
171
+ } if (rf & 2) {
172
+ const ctx_r1 = i0.ɵɵnextContext(3);
173
+ i0.ɵɵconditional(ctx_r1.RecentApps.length > 0 ? 0 : -1);
174
+ i0.ɵɵadvance(4);
175
+ i0.ɵɵproperty("title", ctx_r1.SortMode === "alpha" ? "Sorted A\u2013Z \u2014 click for your custom order" : "Custom order \u2014 click for A\u2013Z");
176
+ i0.ɵɵattribute("aria-pressed", ctx_r1.SortMode === "alpha")("aria-label", ctx_r1.SortMode === "alpha" ? "Sorted alphabetically \u2014 switch to your custom order" : "Sorted by your custom order \u2014 switch to alphabetical");
177
+ i0.ɵɵadvance();
178
+ i0.ɵɵclassMap(ctx_r1.SortMode === "alpha" ? "fa-solid fa-arrow-down-a-z" : "fa-solid fa-bars-staggered");
179
+ i0.ɵɵadvance(2);
180
+ i0.ɵɵrepeater(ctx_r1.OtherApps);
181
+ i0.ɵɵadvance(2);
182
+ i0.ɵɵconditional(ctx_r1.VisibleApps.length === 0 ? 9 : -1);
183
+ } }
184
+ function AppSwitcherComponent_Conditional_8_Conditional_3_Template(rf, ctx) { if (rf & 1) {
185
+ const _r6 = i0.ɵɵgetCurrentView();
186
+ i0.ɵɵconditionalCreate(0, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_0_Template, 7, 1, "div", 25);
187
+ i0.ɵɵelementStart(1, "div", 26);
188
+ i0.ɵɵconditionalCreate(2, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_2_Template, 4, 1)(3, AppSwitcherComponent_Conditional_8_Conditional_3_Conditional_3_Template, 10, 7);
189
+ i0.ɵɵelementEnd();
190
+ i0.ɵɵelementStart(4, "div", 27)(5, "span", 28)(6, "span")(7, "kbd");
191
+ i0.ɵɵtext(8, "\u2191");
192
+ i0.ɵɵelementEnd();
193
+ i0.ɵɵelementStart(9, "kbd");
194
+ i0.ɵɵtext(10, "\u2193");
195
+ i0.ɵɵelementEnd();
196
+ i0.ɵɵtext(11, " navigate");
197
+ i0.ɵɵelementEnd();
198
+ i0.ɵɵelementStart(12, "span")(13, "kbd");
199
+ i0.ɵɵtext(14, "\u21B5");
200
+ i0.ɵɵelementEnd();
201
+ i0.ɵɵtext(15, " open");
202
+ i0.ɵɵelementEnd();
203
+ i0.ɵɵelementStart(16, "span")(17, "kbd");
204
+ i0.ɵɵtext(18, "esc");
205
+ i0.ɵɵelementEnd();
206
+ i0.ɵɵtext(19, " close");
207
+ i0.ɵɵelementEnd()();
208
+ i0.ɵɵelementStart(20, "button", 29);
209
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Conditional_3_Template_button_click_20_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.EnterConfigMode()); });
210
+ i0.ɵɵelement(21, "i", 30);
211
+ i0.ɵɵelementStart(22, "span");
212
+ i0.ɵɵtext(23, "Configure\u2026");
61
213
  i0.ɵɵelementEnd()()();
214
+ i0.ɵɵelementStart(24, "span", 31);
215
+ i0.ɵɵtext(25);
216
+ i0.ɵɵelementEnd();
217
+ } if (rf & 2) {
218
+ const ctx_r1 = i0.ɵɵnextContext(2);
219
+ i0.ɵɵconditional(ctx_r1.ShowFilter ? 0 : -1);
220
+ i0.ɵɵadvance(2);
221
+ i0.ɵɵconditional(ctx_r1.IsFiltering ? 2 : 3);
222
+ i0.ɵɵadvance(23);
223
+ i0.ɵɵtextInterpolate1("", ctx_r1.AnnouncedAppCount, " applications available");
224
+ } }
225
+ function AppSwitcherComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
226
+ const _r3 = i0.ɵɵgetCurrentView();
227
+ i0.ɵɵelementStart(0, "dialog", 12, 2);
228
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Conditional_8_Template_dialog_click_0_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnDialogClick($event)); })("cancel", function AppSwitcherComponent_Conditional_8_Template_dialog_cancel_0_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnDialogCancel($event)); })("keydown", function AppSwitcherComponent_Conditional_8_Template_dialog_keydown_0_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnPanelKeydown($event)); });
229
+ i0.ɵɵconditionalCreate(2, AppSwitcherComponent_Conditional_8_Conditional_2_Template, 9, 2)(3, AppSwitcherComponent_Conditional_8_Conditional_3_Template, 26, 3);
230
+ i0.ɵɵelementEnd();
62
231
  } if (rf & 2) {
63
232
  const ctx_r1 = i0.ɵɵnextContext();
233
+ i0.ɵɵstyleProp("--anchor-left", ctx_r1.AnchorLeft + "px")("--anchor-top", ctx_r1.AnchorTop + "px");
234
+ i0.ɵɵclassProp("launcher-panel--compact", ctx_r1.CompactMode && !ctx_r1.ConfigMode);
235
+ i0.ɵɵattribute("aria-label", ctx_r1.ConfigMode ? null : "Application launcher")("aria-labelledby", ctx_r1.ConfigMode ? "mj-launcher-config-title" : null);
64
236
  i0.ɵɵadvance(2);
65
- i0.ɵɵrepeater(ctx_r1.apps);
237
+ i0.ɵɵconditional(ctx_r1.ConfigMode ? 2 : 3);
238
+ } }
239
+ function AppSwitcherComponent_ng_template_9_Conditional_2_Template(rf, ctx) { if (rf & 1) {
240
+ i0.ɵɵelement(0, "i", 46);
241
+ } }
242
+ function AppSwitcherComponent_ng_template_9_Conditional_3_Template(rf, ctx) { if (rf & 1) {
243
+ i0.ɵɵelement(0, "i", 40);
244
+ } if (rf & 2) {
245
+ const app_r17 = i0.ɵɵnextContext().$implicit;
246
+ i0.ɵɵclassMap(app_r17.Icon || "fa-solid fa-cube");
247
+ } }
248
+ function AppSwitcherComponent_ng_template_9_Conditional_8_Template(rf, ctx) { if (rf & 1) {
249
+ i0.ɵɵelementStart(0, "div", 51);
250
+ i0.ɵɵtext(1);
251
+ i0.ɵɵelementEnd();
252
+ } if (rf & 2) {
253
+ const ctx_r18 = i0.ɵɵnextContext();
254
+ const app_r17 = ctx_r18.$implicit;
255
+ const idx_r18 = ctx_r18.idx;
256
+ const ctx_r1 = i0.ɵɵnextContext();
257
+ i0.ɵɵproperty("id", ctx_r1.OptionId(idx_r18) + "-desc");
258
+ i0.ɵɵadvance();
259
+ i0.ɵɵtextInterpolate(app_r17.Description);
66
260
  } }
261
+ function AppSwitcherComponent_ng_template_9_Template(rf, ctx) { if (rf & 1) {
262
+ const _r16 = i0.ɵɵgetCurrentView();
263
+ i0.ɵɵelementStart(0, "div", 44);
264
+ i0.ɵɵlistener("click", function AppSwitcherComponent_ng_template_9_Template_div_click_0_listener() { const app_r17 = i0.ɵɵrestoreView(_r16).$implicit; const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.selectApp(app_r17)); })("keydown", function AppSwitcherComponent_ng_template_9_Template_div_keydown_0_listener($event) { const idx_r18 = i0.ɵɵrestoreView(_r16).idx; const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnCardKeydown(idx_r18, $event)); });
265
+ i0.ɵɵelementStart(1, "div", 45);
266
+ i0.ɵɵconditionalCreate(2, AppSwitcherComponent_ng_template_9_Conditional_2_Template, 1, 0, "i", 46)(3, AppSwitcherComponent_ng_template_9_Conditional_3_Template, 1, 2, "i", 47);
267
+ i0.ɵɵelementEnd();
268
+ i0.ɵɵelementStart(4, "div", 48)(5, "div", 49)(6, "span", 50);
269
+ i0.ɵɵtext(7);
270
+ i0.ɵɵelementEnd()();
271
+ i0.ɵɵconditionalCreate(8, AppSwitcherComponent_ng_template_9_Conditional_8_Template, 2, 2, "div", 51);
272
+ i0.ɵɵelementEnd()();
273
+ } if (rf & 2) {
274
+ const app_r17 = ctx.$implicit;
275
+ const idx_r18 = ctx.idx;
276
+ const ctx_r1 = i0.ɵɵnextContext();
277
+ i0.ɵɵstyleProp("--app-color", app_r17.GetColor());
278
+ i0.ɵɵclassProp("active", ctx_r1.IsActiveApp(app_r17))("loading", ctx_r1.IsLoadingApp(app_r17));
279
+ i0.ɵɵproperty("id", ctx_r1.OptionId(idx_r18))("mjClickable", app_r17.Name)("testId", "app-switcher-item-" + app_r17.Name);
280
+ i0.ɵɵattribute("aria-current", ctx_r1.IsActiveApp(app_r17) ? "true" : null)("aria-describedby", app_r17.Description ? ctx_r1.OptionId(idx_r18) + "-desc" : null);
281
+ i0.ɵɵadvance(2);
282
+ i0.ɵɵconditional(ctx_r1.IsLoadingApp(app_r17) ? 2 : 3);
283
+ i0.ɵɵadvance(5);
284
+ i0.ɵɵtextInterpolate(app_r17.Name);
285
+ i0.ɵɵadvance();
286
+ i0.ɵɵconditional(app_r17.Description ? 8 : -1);
287
+ } }
288
+ /** UserInfoEngine setting key holding the recently-switched-to apps */
289
+ const RECENT_APPS_KEY = 'mj.shell.recentApps.v1';
290
+ /** UserInfoEngine setting key for the launcher's All-apps sort preference */
291
+ const LAUNCHER_SORT_KEY = 'mj.shell.launcherSort.v1';
292
+ /** How many recent entries to persist (display shows fewer) */
293
+ const RECENT_STORE_MAX = 8;
294
+ /** How many recent cards to display */
295
+ const RECENT_DISPLAY_MAX = 3;
296
+ /** 'auto' style: below this many apps the compact anchored panel is used */
297
+ const COMPACT_AUTO_THRESHOLD = 8;
298
+ /** Compact mode: the filter input appears only at or above this many apps */
299
+ const COMPACT_FILTER_THRESHOLD = 10;
67
300
  /**
68
- * App switcher dropdown in the header.
69
- * Displays current app and allows switching between apps.
301
+ * App switcher in the header: the trigger shows the active app's identity
302
+ * glyph; activating it opens a centered launcher overlay — a filterable grid
303
+ * of app cards (identity glyph + name + Description summary), with the user's
304
+ * most recently used apps surfaced first.
305
+ *
306
+ * Rendering model: the panel is a native <dialog> opened with showModal(),
307
+ * so it lives in the browser's TOP LAYER — above every stacking context in
308
+ * the app (the shell header's z-index:500 context, chat FAB, toasts). The
309
+ * modal state also makes the rest of the document inert (focus cannot
310
+ * escape) and the browser restores focus to the trigger on close.
311
+ *
312
+ * Accessibility model: every app card is a real focusable control (via
313
+ * mjClickable), so Tab walks filter → close → cards → Configure, and
314
+ * Enter/Space activate the focused card. Arrow keys are the fast path:
315
+ * ArrowDown from the filter focuses the first card; arrows move between
316
+ * cards; ArrowUp from the first card returns to the filter. Escape layers:
317
+ * discard-bar → config view → filter → close. Exiting the config view with
318
+ * unsaved changes shows an inline discard bar (a body-appended confirm
319
+ * dialog would be inert/beneath the top layer).
70
320
  */
71
- export class AppSwitcherComponent {
321
+ export class AppSwitcherComponent extends BaseAngularComponent {
72
322
  appManager;
323
+ cdr;
324
+ destroy$ = new Subject();
73
325
  activeApp = null;
74
326
  isViewingSystemTab = false;
75
327
  /** ID of the app currently being loaded (shows loading indicator) */
76
328
  loadingAppId = null;
329
+ /** Presentation style (from Shell.AppSwitcher.Style instance config) */
330
+ SwitcherStyle = 'auto';
77
331
  appSelected = new EventEmitter();
78
- appConfigDialog;
332
+ filterInputRef;
333
+ triggerRef;
334
+ panelRef;
335
+ configContent;
79
336
  showDropdown = false;
80
- showConfigDialog = false;
81
- constructor(appManager) {
337
+ /** When true, the launcher body shows the app-configuration view instead of the card grid */
338
+ ConfigMode = false;
339
+ /** Pending action awaiting the inline unsaved-changes discard bar ('exit' = back to grid, 'close' = close launcher) */
340
+ PendingDiscard = null;
341
+ /** True when the panel renders as the compact anchored dropdown (few apps) */
342
+ CompactMode = false;
343
+ /** All-apps sort: the user's configured order ('custom') or A–Z ('alpha').
344
+ * Per-user preference, persisted via UserInfoEngine. */
345
+ SortMode = 'custom';
346
+ /** Anchor position for the compact panel (px, from the trigger's rect) */
347
+ AnchorLeft = 0;
348
+ AnchorTop = 0;
349
+ /** Live filter text — matches app names AND Description summaries */
350
+ FilterText = '';
351
+ recentEntries = [];
352
+ constructor(appManager, cdr) {
353
+ super();
82
354
  this.appManager = appManager;
355
+ this.cdr = cdr;
356
+ }
357
+ /**
358
+ * UserInfoEngine scoped to this component's provider (multi-provider rule:
359
+ * never reach for the global singleton in per-provider code paths). For the
360
+ * default single-provider shell this resolves to the already-loaded engine
361
+ * registered under the default connection. When no provider is available at
362
+ * all (test environments), fall back to the global singleton rather than
363
+ * letting GetProviderInstance construct a fresh unloaded engine.
364
+ */
365
+ get userInfoEngine() {
366
+ const provider = this.ProviderToUse;
367
+ return provider
368
+ ? UserInfoEngine.GetProviderInstance(provider, UserInfoEngine)
369
+ : UserInfoEngine.Instance;
370
+ }
371
+ ngOnInit() {
372
+ // The launcher's presentation (compact vs full, filter visibility) is
373
+ // resolved from the app COUNT — which can change while the panel is open
374
+ // (the user saves config changes that cross the auto threshold). React to
375
+ // the same stream the app list uses so the presentation can't go stale.
376
+ // Optional-chained: lightweight test stubs may not provide the observable.
377
+ this.appManager.Applications?.pipe(takeUntil(this.destroy$)).subscribe(() => {
378
+ if (this.showDropdown) {
379
+ this.RefreshPresentation();
380
+ }
381
+ });
382
+ }
383
+ ngOnDestroy() {
384
+ this.destroy$.next();
385
+ this.destroy$.complete();
386
+ }
387
+ /**
388
+ * Re-resolve the presentation for the CURRENT app count. No-op while the
389
+ * config view is open (the panel is promoted to full size there anyway);
390
+ * called when the app stream emits and when the config view exits.
391
+ */
392
+ RefreshPresentation() {
393
+ if (this.ConfigMode) {
394
+ return;
395
+ }
396
+ const wasCompact = this.CompactMode;
397
+ this.CompactMode = this.resolveCompactMode();
398
+ if (this.CompactMode && !wasCompact) {
399
+ this.computeCompactAnchor();
400
+ }
401
+ // Mode flips can add/remove the filter input — if the focused element got
402
+ // torn out of the DOM, focus would drop into the inert background. Re-home
403
+ // it after the view settles.
404
+ // Zoneless: an RxJS-driven mutation needs an explicit CD kick (same
405
+ // pattern as AppNavComponent's workspace subscription).
406
+ this.cdr.markForCheck();
407
+ try {
408
+ this.cdr.detectChanges();
409
+ }
410
+ catch {
411
+ // Re-entrant CD — harmless.
412
+ }
413
+ if (wasCompact !== this.CompactMode) {
414
+ requestAnimationFrame(() => {
415
+ const panel = this.panelRef?.nativeElement;
416
+ if (panel && !panel.contains(document.activeElement)) {
417
+ if (this.ShowFilter) {
418
+ this.filterInputRef?.nativeElement?.focus();
419
+ }
420
+ else {
421
+ panel.querySelector('.app-card')?.focus();
422
+ }
423
+ }
424
+ });
425
+ }
426
+ }
427
+ /** Template handler for the filter input's input event */
428
+ OnFilterInput(event) {
429
+ this.FilterText = event.target.value;
430
+ this.OnFilterChange();
83
431
  }
84
432
  /**
85
433
  * Check if the app switcher should show loading state
@@ -88,24 +436,331 @@ export class AppSwitcherComponent {
88
436
  return this.loadingAppId !== null;
89
437
  }
90
438
  /**
91
- * Get applications that should appear in the app switcher dropdown
439
+ * Visible trigger label: the current app's name, or "Apps" when there's no
440
+ * app context (system tabs, nothing active yet).
441
+ */
442
+ get TriggerLabel() {
443
+ return this.activeApp && !this.isViewingSystemTab ? this.activeApp.Name : 'Apps';
444
+ }
445
+ /** Accessible name for the trigger — names the control AND the current app */
446
+ get TriggerAriaLabel() {
447
+ return this.activeApp && !this.isViewingSystemTab
448
+ ? `Switch application — current: ${this.activeApp.Name}`
449
+ : 'Switch application';
450
+ }
451
+ /**
452
+ * Get applications that should appear in the app switcher
92
453
  * (NavigationStyle = 'App Switcher' or 'Both')
93
454
  */
94
455
  get apps() {
95
456
  return this.appManager.GetAppSwitcherApps();
96
457
  }
97
- /**
98
- * Toggle dropdown visibility
99
- */
100
- toggleDropdown() {
101
- this.showDropdown = !this.showDropdown;
458
+ /** Recently-used apps (persisted per user), newest first, capped for display.
459
+ * Compact mode skips the Recent section — with a handful of apps it's noise. */
460
+ get RecentApps() {
461
+ if (this.CompactMode) {
462
+ return [];
463
+ }
464
+ const byId = new Map(this.apps.map(a => [NormalizeUUID(a.ID), a]));
465
+ const result = [];
466
+ for (const entry of this.recentEntries) {
467
+ const app = byId.get(NormalizeUUID(entry.id));
468
+ if (app) {
469
+ result.push(app);
470
+ if (result.length >= RECENT_DISPLAY_MAX) {
471
+ break;
472
+ }
473
+ }
474
+ }
475
+ return this.applyFilter(result);
476
+ }
477
+ /** All switcher apps in the user's chosen sort (custom Sequence order or A–Z) */
478
+ get sortedApps() {
479
+ return this.SortMode === 'alpha'
480
+ ? [...this.apps].sort((a, b) => a.Name.localeCompare(b.Name))
481
+ : this.apps;
482
+ }
483
+ /** The complete "All apps" section, in the user's chosen sort. Apps shown in
484
+ * Recent appear here TOO — being recently used must not make an app vanish
485
+ * from its expected place in the full list. */
486
+ get OtherApps() {
487
+ return this.applyFilter(this.sortedApps);
488
+ }
489
+ /** Flat visible list in RENDERED card order — the keyboard-navigation index
490
+ * space. Filtering collapses the sections into one deduplicated list;
491
+ * otherwise it's Recent followed by the full All-apps list (a recent app
492
+ * intentionally appears twice, matching the two cards on screen). */
493
+ get VisibleApps() {
494
+ if (this.IsFiltering) {
495
+ return this.applyFilter(this.sortedApps);
496
+ }
497
+ return [...this.RecentApps, ...this.OtherApps];
498
+ }
499
+ /** Distinct app count for the screen-reader status (VisibleApps can contain
500
+ * the same app twice when it's in both Recent and All apps) */
501
+ get AnnouncedAppCount() {
502
+ return this.IsFiltering ? this.VisibleApps.length : this.applyFilter(this.apps).length;
503
+ }
504
+ /** True when a filter is active (sections collapse into one result list) */
505
+ get IsFiltering() {
506
+ return this.FilterText.trim().length > 0;
507
+ }
508
+ /** Compact mode hides the filter under a handful of apps — it's noise there */
509
+ get ShowFilter() {
510
+ return !this.CompactMode || this.apps.length >= COMPACT_FILTER_THRESHOLD;
511
+ }
512
+ applyFilter(apps) {
513
+ const q = this.FilterText.trim().toLowerCase();
514
+ if (!q) {
515
+ return apps;
516
+ }
517
+ return apps.filter(a => a.Name.toLowerCase().includes(q) || (a.Description || '').toLowerCase().includes(q));
518
+ }
519
+ /** Stable DOM id for a card (arrow-key focus target) */
520
+ OptionId(index) {
521
+ return `mj-launcher-opt-${index}`;
522
+ }
523
+ /** Trigger handler: open the launcher panel, or close it if already open */
524
+ ToggleLauncher() {
525
+ if (this.showDropdown) {
526
+ this.closeLauncher();
527
+ }
528
+ else {
529
+ this.openLauncher();
530
+ }
531
+ }
532
+ /** Flip the All-apps sort and persist it (fail-silent, like recents) */
533
+ ToggleSortMode() {
534
+ this.SortMode = this.SortMode === 'alpha' ? 'custom' : 'alpha';
535
+ try {
536
+ this.userInfoEngine.SetSettingDebounced(LAUNCHER_SORT_KEY, this.SortMode);
537
+ }
538
+ catch {
539
+ // Preference persistence must never break the launcher
540
+ }
541
+ }
542
+ loadSortMode() {
543
+ try {
544
+ this.SortMode = this.userInfoEngine.GetSetting(LAUNCHER_SORT_KEY) === 'alpha' ? 'alpha' : 'custom';
545
+ }
546
+ catch {
547
+ this.SortMode = 'custom';
548
+ }
549
+ }
550
+ openLauncher() {
551
+ this.loadRecents();
552
+ this.loadSortMode();
553
+ this.FilterText = '';
554
+ this.ConfigMode = false;
555
+ this.PendingDiscard = null;
556
+ this.CompactMode = this.resolveCompactMode();
557
+ if (this.CompactMode) {
558
+ this.computeCompactAnchor();
559
+ }
560
+ this.showDropdown = true;
561
+ // The dialog element renders under @if next frame: put it in the top
562
+ // layer via showModal() (jsdom fallback: plain open attribute), then
563
+ // focus the filter (or the first card when compact mode hides it).
564
+ requestAnimationFrame(() => {
565
+ const dialog = this.panelRef?.nativeElement;
566
+ if (dialog && !dialog.open) {
567
+ if (typeof dialog.showModal === 'function') {
568
+ dialog.showModal();
569
+ }
570
+ else {
571
+ dialog.setAttribute('open', '');
572
+ }
573
+ }
574
+ if (this.ShowFilter) {
575
+ this.filterInputRef?.nativeElement?.focus();
576
+ }
577
+ else {
578
+ dialog?.querySelector('.app-card')?.focus();
579
+ }
580
+ });
581
+ }
582
+ /** Resolve the presentation for this open (auto = compact under the threshold) */
583
+ resolveCompactMode() {
584
+ if (this.SwitcherStyle === 'compact') {
585
+ return true;
586
+ }
587
+ if (this.SwitcherStyle === 'launcher') {
588
+ return false;
589
+ }
590
+ return this.apps.length < COMPACT_AUTO_THRESHOLD;
591
+ }
592
+ /** Anchor the compact panel under the trigger, clamped to the viewport */
593
+ computeCompactAnchor() {
594
+ const rect = this.triggerRef?.nativeElement?.getBoundingClientRect();
595
+ if (!rect) {
596
+ return;
597
+ }
598
+ const panelWidth = 320; // Keep in sync with .launcher-panel--compact width
599
+ const margin = 8;
600
+ this.AnchorLeft = Math.max(margin, Math.min(rect.left, window.innerWidth - panelWidth - margin));
601
+ this.AnchorTop = rect.bottom + margin;
602
+ }
603
+ closeLauncher(restoreFocus = true) {
604
+ const dialog = this.panelRef?.nativeElement;
605
+ if (dialog?.open && typeof dialog.close === 'function') {
606
+ dialog.close(); // Browser restores focus to the trigger automatically
607
+ }
608
+ this.showDropdown = false;
609
+ this.FilterText = '';
610
+ this.ConfigMode = false;
611
+ this.PendingDiscard = null;
612
+ if (restoreFocus) {
613
+ requestAnimationFrame(() => {
614
+ this.triggerRef?.nativeElement?.focus();
615
+ });
616
+ }
617
+ }
618
+ /** Guarded close for template paths (backdrop / close button): unsaved
619
+ * config changes surface the inline discard bar instead of closing. */
620
+ CloseLauncher() {
621
+ if (this.ConfigMode && this.configContent?.HasChanges()) {
622
+ this.PendingDiscard = 'close';
623
+ return;
624
+ }
625
+ this.closeLauncher();
626
+ }
627
+ /** Backdrop clicks arrive as clicks on the <dialog> element itself */
628
+ OnDialogClick(event) {
629
+ if (event.target === this.panelRef?.nativeElement) {
630
+ this.CloseLauncher();
631
+ }
632
+ }
633
+ /** Native cancel (Esc while the browser owns the dialog) — route through
634
+ * our layered Escape handling instead of closing unconditionally. */
635
+ OnDialogCancel(event) {
636
+ event.preventDefault();
637
+ this.handleEscape();
638
+ }
639
+ /** Inline discard bar: confirm — perform the pending action, dropping changes */
640
+ ConfirmDiscard() {
641
+ const action = this.PendingDiscard;
642
+ this.PendingDiscard = null;
643
+ if (action === 'close') {
644
+ this.closeLauncher();
645
+ }
646
+ else if (action === 'exit') {
647
+ this.ExitConfigMode();
648
+ }
649
+ }
650
+ /** Inline discard bar: keep editing */
651
+ CancelDiscard() {
652
+ this.PendingDiscard = null;
653
+ }
654
+ OnFilterChange() {
655
+ // Filtering re-renders the card set; nothing else to sync — focus stays
656
+ // in the input and Tab/arrows reach the new result set naturally.
657
+ }
658
+ /** Keyboard model on the filter input */
659
+ OnFilterKeydown(event) {
660
+ const visible = this.VisibleApps;
661
+ switch (event.key) {
662
+ case 'ArrowDown':
663
+ event.preventDefault();
664
+ this.focusCard(0);
665
+ break;
666
+ case 'Enter':
667
+ event.preventDefault();
668
+ // While filtering, Enter opens the top result (omnibar semantics —
669
+ // the footer advertises "↵ open"); unfiltered it needs an unambiguous
670
+ // single app.
671
+ if (this.IsFiltering && visible.length > 0) {
672
+ this.selectApp(visible[0]);
673
+ }
674
+ else if (visible.length === 1) {
675
+ this.selectApp(visible[0]);
676
+ }
677
+ break;
678
+ case 'Escape':
679
+ event.preventDefault();
680
+ // Stop the bubble: the panel's own Escape handler would otherwise
681
+ // re-evaluate AFTER we've mutated state and double-handle the key.
682
+ event.stopPropagation();
683
+ this.handleEscape();
684
+ break;
685
+ }
686
+ }
687
+ /** Arrow-key navigation between focused cards */
688
+ OnCardKeydown(index, event) {
689
+ switch (event.key) {
690
+ case 'ArrowDown':
691
+ case 'ArrowRight':
692
+ event.preventDefault();
693
+ this.focusCard(index + 1);
694
+ break;
695
+ case 'ArrowUp':
696
+ case 'ArrowLeft':
697
+ event.preventDefault();
698
+ if (index === 0) {
699
+ this.filterInputRef?.nativeElement?.focus();
700
+ }
701
+ else {
702
+ this.focusCard(index - 1);
703
+ }
704
+ break;
705
+ case 'Home':
706
+ event.preventDefault();
707
+ this.focusCard(0);
708
+ break;
709
+ case 'End':
710
+ event.preventDefault();
711
+ this.focusCard(this.VisibleApps.length - 1);
712
+ break;
713
+ }
714
+ }
715
+ /** Move DOM focus to the card at the given VisibleApps index (clamped) */
716
+ focusCard(index) {
717
+ const count = this.VisibleApps.length;
718
+ if (count === 0) {
719
+ return;
720
+ }
721
+ const clamped = Math.min(count - 1, Math.max(0, index));
722
+ const el = document.getElementById(this.OptionId(clamped));
723
+ if (el) {
724
+ el.focus();
725
+ // jsdom (unit tests) doesn't implement scrollIntoView — guard it.
726
+ if (typeof el.scrollIntoView === 'function') {
727
+ el.scrollIntoView({ block: 'nearest' });
728
+ }
729
+ }
730
+ }
731
+ /** Keep Tab cycling within the launcher's real focusables (filter, close, Configure) */
732
+ /** Escape layering: discard bar → config view (guarded) → filter → close.
733
+ * Tab is NOT manually trapped — showModal() makes the background inert,
734
+ * which is a real trap (no leak when focus sits on a non-focusable). */
735
+ OnPanelKeydown(event) {
736
+ if (event.key === 'Escape') {
737
+ event.preventDefault();
738
+ event.stopPropagation();
739
+ this.handleEscape();
740
+ }
741
+ }
742
+ handleEscape() {
743
+ if (this.PendingDiscard) {
744
+ this.CancelDiscard();
745
+ }
746
+ else if (this.ConfigMode) {
747
+ this.RequestExitConfigMode();
748
+ }
749
+ else if (this.IsFiltering) {
750
+ this.FilterText = '';
751
+ this.filterInputRef?.nativeElement?.focus();
752
+ }
753
+ else {
754
+ this.closeLauncher();
755
+ }
102
756
  }
103
757
  /**
104
- * Select an application
105
- * When viewing a system tab, always emit to allow returning to the app
758
+ * Select an application.
759
+ * When viewing a system tab, always emit to allow returning to the app.
106
760
  */
107
761
  selectApp(app) {
108
- this.showDropdown = false;
762
+ this.recordRecent(app);
763
+ this.closeLauncher(false); // Focus moves into the app, not back to the trigger
109
764
  if (!UUIDsEqual(app.ID, this.activeApp?.ID) || this.isViewingSystemTab) {
110
765
  this.appSelected.emit(app.ID);
111
766
  }
@@ -118,92 +773,143 @@ export class AppSwitcherComponent {
118
773
  IsLoadingApp(app) {
119
774
  return UUIDsEqual(app.ID, this.loadingAppId);
120
775
  }
121
- /**
122
- * Close dropdown when clicking outside
123
- */
124
- onClickOutside(event) {
125
- const target = event.target;
126
- if (!target.closest('.app-switcher-container') && this.showDropdown) {
127
- this.showDropdown = false;
776
+ // ---- Recents persistence (MJ: User Settings via UserInfoEngine) ----
777
+ loadRecents() {
778
+ try {
779
+ const raw = this.userInfoEngine.GetSetting(RECENT_APPS_KEY);
780
+ const parsed = raw ? JSON.parse(raw) : [];
781
+ this.recentEntries = Array.isArray(parsed)
782
+ ? parsed.filter((e) => e && typeof e.id === 'string' && typeof e.ts === 'number')
783
+ : [];
784
+ }
785
+ catch {
786
+ this.recentEntries = [];
787
+ }
788
+ }
789
+ recordRecent(app) {
790
+ const id = NormalizeUUID(app.ID);
791
+ this.recentEntries = [
792
+ { id, ts: Date.now() },
793
+ ...this.recentEntries.filter(e => NormalizeUUID(e.id) !== id)
794
+ ].slice(0, RECENT_STORE_MAX);
795
+ try {
796
+ this.userInfoEngine.SetSettingDebounced(RECENT_APPS_KEY, JSON.stringify(this.recentEntries));
797
+ }
798
+ catch {
799
+ // Settings persistence must never block app switching (e.g. engine not
800
+ // configured in tests or permission-constrained sessions).
128
801
  }
129
802
  }
130
803
  /**
131
- * Open the app configuration dialog
804
+ * Swap the launcher body to the app-configuration view (in-panel, seamless —
805
+ * no separate dialog). The embedded mj-user-app-config-content loads on
806
+ * creation.
132
807
  */
133
- openConfigDialog() {
134
- this.showDropdown = false;
135
- this.showConfigDialog = true;
136
- // Use setTimeout to ensure ViewChild is available
137
- setTimeout(() => {
138
- if (this.appConfigDialog) {
139
- this.appConfigDialog.Open();
140
- }
141
- }, 0);
808
+ EnterConfigMode() {
809
+ this.ConfigMode = true;
810
+ this.FilterText = '';
811
+ this.PendingDiscard = null;
812
+ // Move focus into the config view (the back button is its first focusable)
813
+ requestAnimationFrame(() => {
814
+ this.panelRef?.nativeElement
815
+ ?.querySelector('.launcher-back')?.focus();
816
+ });
817
+ }
818
+ /** Guarded exit from the config view: unsaved changes surface the inline
819
+ * discard bar instead of silently dropping the user's edits. */
820
+ RequestExitConfigMode() {
821
+ if (this.configContent?.HasChanges()) {
822
+ this.PendingDiscard = 'exit';
823
+ return;
824
+ }
825
+ this.ExitConfigMode();
142
826
  }
143
827
  /**
144
- * Handle when config is saved close the dialog.
145
- * The app list refreshes reactively: each UserApplication save fires a BaseEntity
146
- * event, UserInfoEngine's debounced refresh (short DebounceTime on the
147
- * 'MJ: User Applications' config) emits DataChange$, and
148
- * ApplicationManager.syncFromEngine() pushes the new list to applications$,
149
- * which the `apps` getter reads. No explicit reload needed here.
828
+ * Return from the configuration view to the app grid.
829
+ * The app list refreshes reactively after a save: each UserApplication save
830
+ * fires a BaseEntity event, UserInfoEngine's debounced refresh emits
831
+ * DataChange$, and ApplicationManager.syncFromEngine() pushes the new list
832
+ * to applications$, which the `apps` getter reads. No explicit reload needed.
150
833
  */
151
- onConfigSaved() {
152
- this.showConfigDialog = false;
834
+ ExitConfigMode() {
835
+ this.ConfigMode = false;
836
+ // The config session may have changed the app count across the compact
837
+ // threshold — re-resolve the presentation for the view we're returning to.
838
+ // (The Applications stream also triggers this when the post-save engine
839
+ // refresh lands, whichever comes last wins with the freshest count.)
840
+ this.RefreshPresentation();
841
+ requestAnimationFrame(() => {
842
+ if (this.ShowFilter) {
843
+ this.filterInputRef?.nativeElement?.focus();
844
+ }
845
+ else {
846
+ this.panelRef?.nativeElement?.querySelector('.app-card')?.focus();
847
+ }
848
+ });
153
849
  }
154
- static ɵfac = function AppSwitcherComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || AppSwitcherComponent)(i0.ɵɵdirectiveInject(i1.ApplicationManager)); };
850
+ static ɵfac = function AppSwitcherComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || AppSwitcherComponent)(i0.ɵɵdirectiveInject(i1.ApplicationManager), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
155
851
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AppSwitcherComponent, selectors: [["mj-app-switcher"]], viewQuery: function AppSwitcherComponent_Query(rf, ctx) { if (rf & 1) {
156
- i0.ɵɵviewQuery(_c0, 5);
852
+ i0.ɵɵviewQuery(_c0, 5)(_c1, 5)(_c2, 5)(UserAppConfigContentComponent, 5);
157
853
  } if (rf & 2) {
158
854
  let _t;
159
- i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.appConfigDialog = _t.first);
160
- } }, hostBindings: function AppSwitcherComponent_HostBindings(rf, ctx) { if (rf & 1) {
161
- i0.ɵɵlistener("click", function AppSwitcherComponent_click_HostBindingHandler($event) { return ctx.onClickOutside($event); }, i0.ɵɵresolveDocument);
162
- } }, inputs: { activeApp: "activeApp", isViewingSystemTab: "isViewingSystemTab", loadingAppId: "loadingAppId" }, outputs: { appSelected: "appSelected" }, standalone: false, decls: 8, vars: 8, consts: [["appConfigDialog", ""], [1, "app-switcher-container"], ["mjClickable", "Switch application", "testId", "app-switcher-trigger", 1, "app-switcher-button", 3, "click"], [1, "app-icon", "loading-spinner", "fa-solid", "fa-spinner", "fa-spin"], [1, "app-icon", 3, "class", "color", "--active-color"], [1, "dropdown-arrow", "fa-solid", "fa-chevron-down"], [1, "app-switcher-dropdown"], [3, "ShowDialogChange", "ConfigSaved", "ShowDialog"], [1, "app-icon"], [1, "app-switcher-list"], [1, "app-switcher-item", 3, "mjClickable", "testId", "active", "loading", "--item-color"], [1, "app-switcher-divider"], ["mjClickable", "Configure applications", "testId", "app-switcher-configure", 1, "app-switcher-item", "configure-item", 3, "click"], [1, "fa-solid", "fa-gear"], [1, "app-switcher-item", 3, "click", "mjClickable", "testId"], [1, "fa-solid", "fa-spinner", "fa-spin"], [3, "class"]], template: function AppSwitcherComponent_Template(rf, ctx) { if (rf & 1) {
855
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.filterInputRef = _t.first);
856
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.triggerRef = _t.first);
857
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.panelRef = _t.first);
858
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.configContent = _t.first);
859
+ } }, inputs: { activeApp: "activeApp", isViewingSystemTab: "isViewingSystemTab", loadingAppId: "loadingAppId", SwitcherStyle: "SwitcherStyle" }, outputs: { appSelected: "appSelected" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 11, vars: 9, consts: [["trigger", ""], ["appCard", ""], ["panel", ""], ["filterInput", ""], [1, "app-switcher-container"], ["testId", "app-switcher-trigger", 1, "app-switcher-button", 3, "click", "mjClickable"], ["aria-hidden", "true", 1, "app-icon", "loading-spinner", "fa-solid", "fa-spinner", "fa-spin"], ["aria-hidden", "true", 1, "app-icon", 3, "class", "color"], [1, "app-label"], ["aria-hidden", "true", 1, "dropdown-arrow", "fa-solid", "fa-chevron-down"], ["role", "dialog", "aria-modal", "true", 1, "launcher-panel", 3, "launcher-panel--compact", "--anchor-left", "--anchor-top"], ["aria-hidden", "true", 1, "app-icon"], ["role", "dialog", "aria-modal", "true", 1, "launcher-panel", 3, "click", "cancel", "keydown"], [1, "launcher-head", "launcher-head--config"], ["type", "button", "aria-label", "Back to apps", 1, "launcher-back", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-arrow-left"], ["id", "mj-launcher-config-title", 1, "launcher-title"], ["type", "button", "aria-label", "Close application launcher", 1, "launcher-close", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-xmark"], ["role", "alert", 1, "launcher-discard-bar"], [1, "launcher-config-body", 3, "Saved", "Cancelled", "Provider"], ["aria-hidden", "true", 1, "fa-solid", "fa-triangle-exclamation"], [1, "discard-msg"], ["mjButton", "", "variant", "danger", "size", "sm", 3, "click"], ["mjButton", "", "size", "sm", 3, "click"], [1, "launcher-head"], [1, "launcher-scroll"], [1, "launcher-foot"], ["aria-hidden", "true", 1, "launcher-kbd-hints"], ["type", "button", "testId", "app-switcher-configure", 1, "launcher-configure", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-gear"], ["role", "status", 1, "sr-only"], [1, "launcher-filter"], ["aria-hidden", "true", 1, "fa-solid", "fa-magnifying-glass"], ["type", "text", "placeholder", "Find an app\u2026", "aria-label", "Find an app", "autocomplete", "off", "spellcheck", "false", 3, "input", "keydown", "value"], [1, "launcher-grid"], [1, "launcher-empty"], [4, "ngTemplateOutlet", "ngTemplateOutletContext"], [1, "launcher-sect", "launcher-sect--row"], ["type", "button", 1, "launcher-sort-toggle", 3, "click", "title"], ["aria-hidden", "true"], ["role", "group", "aria-label", "All applications", 1, "launcher-grid"], [1, "launcher-sect"], ["role", "group", "aria-label", "Recent applications", 1, "launcher-grid"], ["role", "link", 1, "app-card", 3, "click", "keydown", "id", "mjClickable", "testId"], [1, "app-card-glyph"], ["aria-hidden", "true", 1, "fa-solid", "fa-spinner", "fa-spin"], ["aria-hidden", "true", 3, "class"], [1, "app-card-text"], [1, "app-card-name"], [1, "app-card-name-label"], [1, "app-card-desc", 3, "id"]], template: function AppSwitcherComponent_Template(rf, ctx) { if (rf & 1) {
163
860
  const _r1 = i0.ɵɵgetCurrentView();
164
- i0.ɵɵelementStart(0, "div", 1)(1, "div", 2);
165
- i0.ɵɵlistener("click", function AppSwitcherComponent_Template_div_click_1_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.toggleDropdown()); });
166
- i0.ɵɵconditionalCreate(2, AppSwitcherComponent_Conditional_2_Template, 1, 0, "i", 3);
167
- i0.ɵɵconditionalCreate(3, AppSwitcherComponent_Conditional_3_Template, 1, 6, "i", 4);
168
- i0.ɵɵelement(4, "i", 5);
861
+ i0.ɵɵelementStart(0, "div", 4)(1, "div", 5, 0);
862
+ i0.ɵɵlistener("click", function AppSwitcherComponent_Template_div_click_1_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.ToggleLauncher()); });
863
+ i0.ɵɵconditionalCreate(3, AppSwitcherComponent_Conditional_3_Template, 1, 0, "i", 6);
864
+ i0.ɵɵconditionalCreate(4, AppSwitcherComponent_Conditional_4_Template, 1, 4, "i", 7);
865
+ i0.ɵɵelementStart(5, "span", 8);
866
+ i0.ɵɵtext(6);
169
867
  i0.ɵɵelementEnd();
170
- i0.ɵɵconditionalCreate(5, AppSwitcherComponent_Conditional_5_Template, 9, 0, "div", 6);
868
+ i0.ɵɵelement(7, "i", 9);
171
869
  i0.ɵɵelementEnd();
172
- i0.ɵɵelementStart(6, "mj-user-app-config", 7, 0);
173
- i0.ɵɵtwoWayListener("ShowDialogChange", function AppSwitcherComponent_Template_mj_user_app_config_ShowDialogChange_6_listener($event) { i0.ɵɵrestoreView(_r1); i0.ɵɵtwoWayBindingSet(ctx.showConfigDialog, $event) || (ctx.showConfigDialog = $event); return i0.ɵɵresetView($event); });
174
- i0.ɵɵlistener("ConfigSaved", function AppSwitcherComponent_Template_mj_user_app_config_ConfigSaved_6_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onConfigSaved()); });
870
+ i0.ɵɵconditionalCreate(8, AppSwitcherComponent_Conditional_8_Template, 4, 9, "dialog", 10);
175
871
  i0.ɵɵelementEnd();
872
+ i0.ɵɵtemplate(9, AppSwitcherComponent_ng_template_9_Template, 9, 14, "ng-template", null, 1, i0.ɵɵtemplateRefExtractor);
176
873
  } if (rf & 2) {
177
874
  i0.ɵɵclassProp("loading", ctx.isLoading);
178
875
  i0.ɵɵadvance();
179
- i0.ɵɵattribute("aria-haspopup", true)("aria-expanded", ctx.showDropdown);
180
- i0.ɵɵadvance();
181
- i0.ɵɵconditional(ctx.isLoading ? 2 : -1);
182
- i0.ɵɵadvance();
183
- i0.ɵɵconditional(!ctx.isLoading ? 3 : -1);
876
+ i0.ɵɵproperty("mjClickable", ctx.TriggerAriaLabel);
877
+ i0.ɵɵattribute("aria-haspopup", "dialog")("aria-expanded", ctx.showDropdown);
184
878
  i0.ɵɵadvance(2);
185
- i0.ɵɵconditional(ctx.showDropdown ? 5 : -1);
879
+ i0.ɵɵconditional(ctx.isLoading ? 3 : -1);
186
880
  i0.ɵɵadvance();
187
- i0.ɵɵtwoWayProperty("ShowDialog", ctx.showConfigDialog);
188
- } }, dependencies: [i2.MJClickableDirective, i3.UserAppConfigComponent], styles: [".app-switcher-container[_ngcontent-%COMP%] {\n position: relative;\n margin-left: -2px; \n\n}\n\n.app-switcher-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s;\n user-select: none;\n position: relative;\n}\n.app-switcher-button[_ngcontent-%COMP%] .app-icon[_ngcontent-%COMP%] {\n font-size: 20px;\n width: 24px;\n text-align: center;\n position: relative;\n}\n\n\n.app-switcher-button[_ngcontent-%COMP%] .app-icon[_ngcontent-%COMP%]::after {\n content: \"\";\n position: absolute;\n bottom: -6px;\n left: 2px;\n right: 2px;\n height: 3px;\n border-radius: 2px;\n background-color: var(--active-color, var(--mj-text-muted));\n}\n.app-switcher-button[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n.app-switcher-button[_ngcontent-%COMP%] .dropdown-arrow[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-disabled);\n}\n\n.app-switcher-dropdown[_ngcontent-%COMP%] {\n position: absolute;\n top: calc(100% + 8px);\n left: 0;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-lg);\n min-width: 220px;\n width: max-content;\n overflow: hidden;\n z-index: 10000;\n max-height: calc(100vh - 80px - var(--mj-connectivity-banner-height, 0px));\n display: flex;\n flex-direction: column;\n}\n.app-switcher-list[_ngcontent-%COMP%] {\n overflow-y: auto;\n flex: 1 1 auto;\n min-height: 0;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 16px;\n cursor: pointer;\n transition: all 0.15s;\n font-weight: 500;\n color: var(--mj-text-secondary);\n position: relative;\n white-space: nowrap;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item[_ngcontent-%COMP%]::before {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 8px;\n bottom: 8px;\n width: 3px;\n border-radius: 2px;\n background-color: var(--item-color, var(--mj-text-muted));\n opacity: 0.8;\n transition: all 0.15s;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 18px;\n color: var(--item-color, var(--mj-text-muted));\n width: 20px;\n text-align: center;\n opacity: 1;\n transition: all 0.15s;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item[_ngcontent-%COMP%]:hover::before {\n opacity: 1;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.active[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--item-color, var(--mj-brand-primary)) 10%, var(--mj-bg-surface-elevated));\n color: var(--item-color, var(--mj-brand-primary));\n font-weight: 600;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.active[_ngcontent-%COMP%]::before {\n width: 4px;\n left: 3px;\n opacity: 1;\n}\n\n\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.configure-item[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.configure-item[_ngcontent-%COMP%]::before {\n display: none;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.configure-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.configure-item[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.configure-item[_ngcontent-%COMP%]:hover i[_ngcontent-%COMP%] {\n color: var(--mj-text-primary);\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-divider[_ngcontent-%COMP%] {\n height: 1px;\n background: var(--mj-border-default);\n margin: 4px 12px;\n}\n\n\n\n.app-switcher-button[_ngcontent-%COMP%] .loading-spinner[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n}\n.app-switcher-button[_ngcontent-%COMP%] .loading-spinner[_ngcontent-%COMP%]::after {\n \n\n display: none;\n}\n\n\n\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.loading[_ngcontent-%COMP%] {\n pointer-events: none;\n opacity: 0.7;\n}\n.app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item.loading[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--item-color, var(--mj-text-muted));\n}\n\n\n\n@media (max-width: 600px) {\n .app-switcher-dropdown[_ngcontent-%COMP%] {\n position: fixed;\n top: calc(60px + var(--mj-connectivity-banner-height, 0px));\n left: 0;\n right: 0;\n width: 100%;\n min-width: unset;\n height: calc(100vh - 60px - var(--mj-connectivity-banner-height, 0px));\n overflow-y: auto;\n border-radius: 0;\n border-top: none;\n padding: 0.25rem 0;\n }\n\n .app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item[_ngcontent-%COMP%] {\n padding: 14px 20px;\n gap: 14px;\n min-height: 48px;\n font-size: 0.9375rem;\n }\n\n .app-switcher-dropdown[_ngcontent-%COMP%] .app-switcher-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 20px;\n width: 24px;\n }\n\n .app-switcher-list[_ngcontent-%COMP%] {\n overflow-y: visible;\n flex: none;\n }\n}"] });
881
+ i0.ɵɵconditional(!ctx.isLoading ? 4 : -1);
882
+ i0.ɵɵadvance(2);
883
+ i0.ɵɵtextInterpolate(ctx.TriggerLabel);
884
+ i0.ɵɵadvance(2);
885
+ i0.ɵɵconditional(ctx.showDropdown ? 8 : -1);
886
+ } }, dependencies: [i2.NgTemplateOutlet, i3.MJClickableDirective, i3.MJButtonDirective, i4.UserAppConfigContentComponent], styles: [".app-switcher-container[_ngcontent-%COMP%] {\n position: relative;\n margin-left: calc(-1 * var(--mj-space-0-5)); \n\n}\n\n\n\n.app-switcher-button[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-1-5);\n padding: var(--mj-space-2) var(--mj-space-2-5);\n border-radius: var(--mj-radius-md);\n cursor: pointer;\n transition: background var(--mj-transition-fast);\n user-select: none;\n position: relative;\n}\n.app-switcher-button[_ngcontent-%COMP%] .app-icon[_ngcontent-%COMP%] {\n font-size: var(--mj-text-xl);\n width: 24px;\n text-align: center;\n position: relative;\n}\n.app-switcher-button[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n.app-switcher-button[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n\n\n.app-switcher-button[_ngcontent-%COMP%] .app-label[_ngcontent-%COMP%] {\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n max-width: 160px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.app-switcher-button[_ngcontent-%COMP%] .dropdown-arrow[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-muted);\n transition: color var(--mj-transition-fast);\n}\n\n\n\n.app-switcher-button[_ngcontent-%COMP%]:hover .dropdown-arrow[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n}\n.app-switcher-button[_ngcontent-%COMP%] .loading-spinner[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n}\n\n\n\n\n\n\n\n.launcher-panel[_ngcontent-%COMP%] {\n \n\n padding: 0;\n margin: 0;\n inset: unset;\n max-width: none;\n position: fixed;\n top: calc(60px + var(--mj-connectivity-banner-height, 0px) + var(--mj-space-6));\n left: 50%;\n transform: translateX(-50%);\n width: min(880px, calc(100vw - var(--mj-space-12)));\n max-height: calc(100vh - 60px - var(--mj-connectivity-banner-height, 0px) - var(--mj-space-12));\n background: var(--mj-bg-surface-elevated);\n color: inherit;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n box-shadow: var(--mj-shadow-xl);\n flex-direction: column;\n overflow: hidden;\n animation: _ngcontent-%COMP%_launcher-pop .13s ease-out;\n}\n.launcher-panel[open][_ngcontent-%COMP%] {\n display: flex;\n}\n.launcher-panel[_ngcontent-%COMP%]::backdrop {\n background: var(--mj-bg-overlay);\n animation: _ngcontent-%COMP%_launcher-fade-in var(--mj-transition-fast);\n}\n\n\n\n\n\n\n\n@media (min-width: 769px) {\n .launcher-panel--compact[_ngcontent-%COMP%] {\n left: var(--anchor-left);\n top: var(--anchor-top);\n transform: none;\n width: 320px;\n max-height: calc(100vh - var(--anchor-top) - var(--mj-space-6));\n box-shadow: var(--mj-shadow-lg);\n \n\n animation: _ngcontent-%COMP%_launcher-fade-in var(--mj-transition-fast);\n }\n .launcher-panel--compact[_ngcontent-%COMP%]::backdrop {\n background: transparent;\n animation: none;\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .launcher-grid[_ngcontent-%COMP%] {\n grid-template-columns: 1fr;\n gap: var(--mj-space-1);\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .launcher-scroll[_ngcontent-%COMP%] {\n padding: var(--mj-space-2);\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .launcher-sect[_ngcontent-%COMP%] {\n display: none; \n\n }\n \n\n\n .launcher-panel--compact[_ngcontent-%COMP%] .app-card[_ngcontent-%COMP%] {\n padding: var(--mj-space-2) var(--mj-space-2-5);\n align-items: center;\n background: transparent;\n border-color: transparent;\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .app-card[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n border-color: transparent;\n transform: none;\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .app-card.active[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n border-color: transparent;\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .app-card[_ngcontent-%COMP%]:hover {\n transform: none;\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .app-card-glyph[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n font-size: var(--mj-text-base);\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .app-card-desc[_ngcontent-%COMP%] {\n display: none;\n }\n .launcher-panel--compact[_ngcontent-%COMP%] .launcher-kbd-hints[_ngcontent-%COMP%] {\n display: none;\n }\n}\n\n@keyframes _ngcontent-%COMP%_launcher-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n\n\n\n\n@keyframes _ngcontent-%COMP%_launcher-pop {\n from { opacity: 0; transform: translateX(-50%) scale(.985) translateY(-4px); }\n to { opacity: 1; transform: translateX(-50%) scale(1); }\n}\n\n\n\n.launcher-head[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: var(--mj-space-4) var(--mj-space-4) var(--mj-space-2);\n flex-shrink: 0;\n}\n.launcher-filter[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2-5);\n flex: 1;\n height: 46px;\n padding: 0 var(--mj-space-3-5);\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n transition: border-color var(--mj-transition-fast);\n}\n.launcher-filter[_ngcontent-%COMP%]:focus-within {\n border-color: var(--mj-border-focus);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n.launcher-filter[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n flex-shrink: 0;\n}\n.launcher-filter[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n border: none;\n outline: none;\n background: transparent;\n width: 100%;\n font-family: inherit;\n font-size: var(--mj-text-base);\n color: var(--mj-text-primary);\n}\n.launcher-filter[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-muted);\n}\n.launcher-close[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n flex-shrink: 0;\n border: none;\n border-radius: var(--mj-radius-md);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background var(--mj-transition-fast);\n}\n.launcher-close[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-close[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n\n\n.launcher-head--config[_ngcontent-%COMP%] {\n align-items: center;\n gap: var(--mj-space-2-5);\n padding-bottom: var(--mj-space-2);\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.launcher-back[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n flex-shrink: 0;\n border: none;\n border-radius: var(--mj-radius-md);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background var(--mj-transition-fast);\n}\n.launcher-back[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-back[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n.launcher-title[_ngcontent-%COMP%] {\n flex: 1;\n margin: 0;\n font-size: var(--mj-text-base);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n}\n.launcher-config-body[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n min-height: 0;\n overflow: hidden;\n}\n\n\n\n.launcher-discard-bar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2-5);\n margin: var(--mj-space-2) var(--mj-space-4) 0;\n padding: var(--mj-space-2) var(--mj-space-3);\n background: var(--mj-status-warning-bg);\n border: 1px solid var(--mj-status-warning-border);\n border-radius: var(--mj-radius-md);\n flex-shrink: 0;\n}\n.launcher-discard-bar[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-status-warning);\n}\n.launcher-discard-bar[_ngcontent-%COMP%] .discard-msg[_ngcontent-%COMP%] {\n flex: 1;\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-medium);\n color: var(--mj-status-warning-text);\n}\n\n\n\n.launcher-scroll[_ngcontent-%COMP%] {\n overflow-y: auto;\n padding: 0 var(--mj-space-4) var(--mj-space-4);\n flex: 1 1 auto;\n min-height: 0;\n}\n\n\n.launcher-sect[_ngcontent-%COMP%] {\n font-size: 10.5px;\n font-weight: var(--mj-font-bold);\n letter-spacing: .6px;\n text-transform: uppercase;\n color: var(--mj-text-muted);\n padding: var(--mj-space-3) var(--mj-space-1-5) var(--mj-space-1-5);\n}\n.launcher-sect--row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-right: var(--mj-space-1);\n}\n\n\n.launcher-sort-toggle[_ngcontent-%COMP%] {\n width: 28px;\n height: 28px;\n border: none;\n border-radius: var(--mj-radius-sm);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-xs);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background var(--mj-transition-fast), color var(--mj-transition-fast);\n}\n.launcher-sort-toggle[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-sort-toggle[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n.launcher-sort-toggle[aria-pressed=\"true\"][_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n.launcher-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: var(--mj-space-2-5);\n}\n.launcher-empty[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n text-align: center;\n padding: var(--mj-space-6) var(--mj-space-2);\n}\n\n\n\n.app-card[_ngcontent-%COMP%] {\n display: flex;\n align-items: flex-start;\n gap: var(--mj-space-3);\n \n\n\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n padding: var(--mj-space-3) var(--mj-space-3-5);\n cursor: pointer;\n min-width: 0;\n transition: border-color var(--mj-transition-fast), background var(--mj-transition-fast), transform var(--mj-transition-fast);\n}\n.app-card[_ngcontent-%COMP%]:hover {\n border-color: color-mix(in srgb, var(--mj-brand-primary) 45%, var(--mj-border-default));\n background: var(--mj-bg-surface-hover);\n transform: translateY(-1px);\n}\n.app-card[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: -1px;\n}\n.app-card.active[_ngcontent-%COMP%] {\n \n\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n border-color: color-mix(in srgb, var(--mj-brand-primary) 45%, transparent);\n}\n.app-card.loading[_ngcontent-%COMP%] {\n pointer-events: none;\n opacity: 0.7;\n}\n.app-card-glyph[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: var(--mj-text-lg);\n color: var(--app-color, var(--mj-text-muted));\n background: color-mix(in srgb, var(--app-color, var(--mj-text-muted)) 14%, var(--mj-bg-surface-card));\n}\n.app-card-text[_ngcontent-%COMP%] {\n min-width: 0;\n flex: 1;\n}\n.app-card-name[_ngcontent-%COMP%] {\n display: flex;\n align-items: baseline;\n gap: var(--mj-space-2);\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-bold);\n color: var(--mj-text-primary);\n}\n.app-card-name-label[_ngcontent-%COMP%] {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.app-card.active[_ngcontent-%COMP%] .app-card-name[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n.app-card-desc[_ngcontent-%COMP%] {\n margin-top: var(--mj-space-0-5);\n font-size: var(--mj-text-xs);\n line-height: var(--mj-leading-snug);\n color: var(--mj-text-muted);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n\n\n\n\n.launcher-foot[_ngcontent-%COMP%] {\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface-card);\n padding: var(--mj-space-2-5) var(--mj-space-4);\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n\n\n.launcher-configure[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin-left: auto;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-family: inherit;\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-semibold);\n padding: var(--mj-space-1-5) var(--mj-space-2);\n border-radius: var(--mj-radius-md);\n cursor: pointer;\n transition: background var(--mj-transition-fast), color var(--mj-transition-fast);\n}\n.launcher-configure[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-configure[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n.launcher-kbd-hints[_ngcontent-%COMP%] {\n display: flex;\n gap: var(--mj-space-4);\n font-size: 11.5px;\n color: var(--mj-text-muted);\n}\n\n\n\n.launcher-kbd-hints[_ngcontent-%COMP%] kbd[_ngcontent-%COMP%] {\n font-family: inherit;\n font-size: 10.5px;\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n border-bottom-width: 2px;\n border-radius: 5px;\n padding: 1.5px var(--mj-space-1-5);\n}\n\n\n\n.sr-only[_ngcontent-%COMP%] {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n\n\n@media (max-width: 900px) {\n .launcher-grid[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n\n\n\n@media (max-width: 768px) {\n \n\n\n .launcher-panel[_ngcontent-%COMP%] {\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n transform: none;\n width: 100%;\n max-height: none;\n height: 100dvh;\n border-radius: 0;\n border: none;\n animation: none;\n }\n .launcher-grid[_ngcontent-%COMP%] {\n grid-template-columns: 1fr;\n }\n .app-card[_ngcontent-%COMP%] {\n min-height: 48px;\n align-items: center;\n }\n .launcher-close[_ngcontent-%COMP%] {\n width: 46px;\n height: 46px;\n }\n .launcher-kbd-hints[_ngcontent-%COMP%] {\n display: none; \n\n }\n .app-switcher-button[_ngcontent-%COMP%] .app-label[_ngcontent-%COMP%] {\n display: none; \n\n }\n}\n\n\n\n@media (prefers-reduced-motion: reduce) {\n .launcher-panel[_ngcontent-%COMP%], \n .launcher-panel[_ngcontent-%COMP%]::backdrop {\n animation: none;\n }\n .app-card[_ngcontent-%COMP%], \n .app-card[_ngcontent-%COMP%]:hover {\n transition: none;\n transform: none;\n }\n}"] });
189
887
  }
190
888
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AppSwitcherComponent, [{
191
889
  type: Component,
192
- args: [{ standalone: false, selector: 'mj-app-switcher', template: "<div class=\"app-switcher-container\" [class.loading]=\"isLoading\">\n <div class=\"app-switcher-button\"\n mjClickable=\"Switch application\"\n testId=\"app-switcher-trigger\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-expanded]=\"showDropdown\"\n (click)=\"toggleDropdown()\">\n <!-- Show spinner when loading, otherwise show app icon -->\n @if (isLoading) {\n <i class=\"app-icon loading-spinner fa-solid fa-spinner fa-spin\"></i>\n }\n @if (!isLoading) {\n <i class=\"app-icon\" [class]=\"activeApp?.Icon || 'fa-solid fa-cube'\"\n [style.color]=\"activeApp?.GetColor()\"\n [style.--active-color]=\"activeApp?.GetColor()\"></i>\n }\n <i class=\"dropdown-arrow fa-solid fa-chevron-down\"></i>\n </div>\n\n @if (showDropdown) {\n <div class=\"app-switcher-dropdown\">\n <div class=\"app-switcher-list\">\n @for (app of apps; track app) {\n <div\n class=\"app-switcher-item\"\n [mjClickable]=\"app.Name\"\n [testId]=\"'app-switcher-item-' + app.Name\"\n [attr.aria-current]=\"IsActiveApp(app) ? 'true' : null\"\n [class.active]=\"IsActiveApp(app)\"\n [class.loading]=\"IsLoadingApp(app)\"\n [style.--item-color]=\"app.GetColor()\"\n (click)=\"selectApp(app)\">\n @if (IsLoadingApp(app)) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n @if (!IsLoadingApp(app)) {\n <i [class]=\"app.Icon || 'fa-solid fa-cube'\"></i>\n }\n <span>{{ app.Name }}</span>\n </div>\n }\n </div>\n <!-- Divider + Configure pinned at bottom -->\n <div class=\"app-switcher-divider\"></div>\n <div class=\"app-switcher-item configure-item\" mjClickable=\"Configure applications\" testId=\"app-switcher-configure\" (click)=\"openConfigDialog()\">\n <i class=\"fa-solid fa-gear\"></i>\n <span>Configure...</span>\n </div>\n </div>\n }\n</div>\n\n<!-- App Configuration Dialog -->\n<mj-user-app-config\n #appConfigDialog\n [(ShowDialog)]=\"showConfigDialog\"\n (ConfigSaved)=\"onConfigSaved()\">\n</mj-user-app-config>\n", styles: [".app-switcher-container {\n position: relative;\n margin-left: -2px; /* Fine-tune alignment when no nav-bar icons to the left */\n}\n\n.app-switcher-button {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s;\n user-select: none;\n position: relative;\n}\n.app-switcher-button .app-icon {\n font-size: 20px;\n width: 24px;\n text-align: center;\n position: relative;\n}\n/* Color underline under the icon only, not the caret */\n.app-switcher-button .app-icon::after {\n content: \"\";\n position: absolute;\n bottom: -6px;\n left: 2px;\n right: 2px;\n height: 3px;\n border-radius: 2px;\n background-color: var(--active-color, var(--mj-text-muted));\n}\n.app-switcher-button:hover {\n background: var(--mj-bg-surface-hover);\n}\n.app-switcher-button .dropdown-arrow {\n font-size: 10px;\n color: var(--mj-text-disabled);\n}\n\n.app-switcher-dropdown {\n position: absolute;\n top: calc(100% + 8px);\n left: 0;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-lg);\n min-width: 220px;\n width: max-content;\n overflow: hidden;\n z-index: 10000;\n max-height: calc(100vh - 80px - var(--mj-connectivity-banner-height, 0px));\n display: flex;\n flex-direction: column;\n}\n.app-switcher-list {\n overflow-y: auto;\n flex: 1 1 auto;\n min-height: 0;\n}\n.app-switcher-dropdown .app-switcher-item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 16px;\n cursor: pointer;\n transition: all 0.15s;\n font-weight: 500;\n color: var(--mj-text-secondary);\n position: relative;\n white-space: nowrap;\n}\n.app-switcher-dropdown .app-switcher-item::before {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 8px;\n bottom: 8px;\n width: 3px;\n border-radius: 2px;\n background-color: var(--item-color, var(--mj-text-muted));\n opacity: 0.8;\n transition: all 0.15s;\n}\n.app-switcher-dropdown .app-switcher-item i {\n font-size: 18px;\n color: var(--item-color, var(--mj-text-muted));\n width: 20px;\n text-align: center;\n opacity: 1;\n transition: all 0.15s;\n}\n.app-switcher-dropdown .app-switcher-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n.app-switcher-dropdown .app-switcher-item:hover::before {\n opacity: 1;\n}\n.app-switcher-dropdown .app-switcher-item.active {\n background: color-mix(in srgb, var(--item-color, var(--mj-brand-primary)) 10%, var(--mj-bg-surface-elevated));\n color: var(--item-color, var(--mj-brand-primary));\n font-weight: 600;\n}\n.app-switcher-dropdown .app-switcher-item.active::before {\n width: 4px;\n left: 3px;\n opacity: 1;\n}\n/* Configure option - no color line, neutral gray styling */\n.app-switcher-dropdown .app-switcher-item.configure-item {\n color: var(--mj-text-secondary);\n}\n.app-switcher-dropdown .app-switcher-item.configure-item::before {\n display: none;\n}\n.app-switcher-dropdown .app-switcher-item.configure-item i {\n color: var(--mj-text-muted);\n}\n.app-switcher-dropdown .app-switcher-item.configure-item:hover {\n color: var(--mj-text-primary);\n}\n.app-switcher-dropdown .app-switcher-item.configure-item:hover i {\n color: var(--mj-text-primary);\n}\n.app-switcher-dropdown .app-switcher-divider {\n height: 1px;\n background: var(--mj-border-default);\n margin: 4px 12px;\n}\n\n/* Loading state for app switcher button */\n.app-switcher-button .loading-spinner {\n color: var(--mj-text-muted);\n}\n.app-switcher-button .loading-spinner::after {\n /* Hide the underline when showing spinner */\n display: none;\n}\n\n/* Loading state for dropdown items */\n.app-switcher-dropdown .app-switcher-item.loading {\n pointer-events: none;\n opacity: 0.7;\n}\n.app-switcher-dropdown .app-switcher-item.loading i {\n color: var(--item-color, var(--mj-text-muted));\n}\n\n/* Mobile: full-width dropdown with scroll */\n@media (max-width: 600px) {\n .app-switcher-dropdown {\n position: fixed;\n top: calc(60px + var(--mj-connectivity-banner-height, 0px));\n left: 0;\n right: 0;\n width: 100%;\n min-width: unset;\n height: calc(100vh - 60px - var(--mj-connectivity-banner-height, 0px));\n overflow-y: auto;\n border-radius: 0;\n border-top: none;\n padding: 0.25rem 0;\n }\n\n .app-switcher-dropdown .app-switcher-item {\n padding: 14px 20px;\n gap: 14px;\n min-height: 48px;\n font-size: 0.9375rem;\n }\n\n .app-switcher-dropdown .app-switcher-item i {\n font-size: 20px;\n width: 24px;\n }\n\n .app-switcher-list {\n overflow-y: visible;\n flex: none;\n }\n}\n"] }]
193
- }], () => [{ type: i1.ApplicationManager }], { activeApp: [{
890
+ args: [{ standalone: false, selector: 'mj-app-switcher', template: "<div class=\"app-switcher-container\" [class.loading]=\"isLoading\">\n <div class=\"app-switcher-button\"\n #trigger\n [mjClickable]=\"TriggerAriaLabel\"\n testId=\"app-switcher-trigger\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"showDropdown\"\n (click)=\"ToggleLauncher()\">\n <!-- Show spinner when loading, otherwise show app icon -->\n @if (isLoading) {\n <i class=\"app-icon loading-spinner fa-solid fa-spinner fa-spin\" aria-hidden=\"true\"></i>\n }\n @if (!isLoading) {\n <i class=\"app-icon\" [class]=\"activeApp?.Icon || 'fa-solid fa-cube'\"\n [style.color]=\"activeApp?.GetColor()\" aria-hidden=\"true\"></i>\n }\n <!-- Current app name; hidden at the mobile breakpoint (icon-only trigger) -->\n <span class=\"app-label\">{{ TriggerLabel }}</span>\n <i class=\"dropdown-arrow fa-solid fa-chevron-down\" aria-hidden=\"true\"></i>\n </div>\n\n @if (showDropdown) {\n <!-- Native <dialog> + showModal(): lives in the browser TOP LAYER (above\n every app stacking context \u2014 chat FAB, toasts), background is inert\n (real focus trap), and focus restores to the trigger on close. The\n backdrop is ::backdrop; clicks on it arrive as clicks on the dialog\n element itself (OnDialogClick). -->\n <dialog class=\"launcher-panel\"\n #panel\n role=\"dialog\"\n aria-modal=\"true\"\n [class.launcher-panel--compact]=\"CompactMode && !ConfigMode\"\n [style.--anchor-left]=\"AnchorLeft + 'px'\"\n [style.--anchor-top]=\"AnchorTop + 'px'\"\n [attr.aria-label]=\"ConfigMode ? null : 'Application launcher'\"\n [attr.aria-labelledby]=\"ConfigMode ? 'mj-launcher-config-title' : null\"\n (click)=\"OnDialogClick($event)\"\n (cancel)=\"OnDialogCancel($event)\"\n (keydown)=\"OnPanelKeydown($event)\">\n\n @if (ConfigMode) {\n <!-- Configuration view: same panel, body swapped -->\n <div class=\"launcher-head launcher-head--config\">\n <button class=\"launcher-back\" type=\"button\" aria-label=\"Back to apps\" (click)=\"RequestExitConfigMode()\">\n <i class=\"fa-solid fa-arrow-left\" aria-hidden=\"true\"></i>\n </button>\n <h2 class=\"launcher-title\" id=\"mj-launcher-config-title\">Configure apps</h2>\n <button class=\"launcher-close\" type=\"button\" aria-label=\"Close application launcher\" (click)=\"CloseLauncher()\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n </button>\n </div>\n @if (PendingDiscard) {\n <div class=\"launcher-discard-bar\" role=\"alert\">\n <i class=\"fa-solid fa-triangle-exclamation\" aria-hidden=\"true\"></i>\n <span class=\"discard-msg\">You have unsaved changes.</span>\n <button mjButton variant=\"danger\" size=\"sm\" (click)=\"ConfirmDiscard()\">Discard</button>\n <button mjButton size=\"sm\" (click)=\"CancelDiscard()\">Keep editing</button>\n </div>\n }\n <mj-user-app-config-content\n class=\"launcher-config-body\"\n [Provider]=\"Provider\"\n (Saved)=\"ExitConfigMode()\"\n (Cancelled)=\"RequestExitConfigMode()\">\n </mj-user-app-config-content>\n } @else {\n @if (ShowFilter) {\n <div class=\"launcher-head\">\n <div class=\"launcher-filter\">\n <i class=\"fa-solid fa-magnifying-glass\" aria-hidden=\"true\"></i>\n <input\n #filterInput\n type=\"text\"\n placeholder=\"Find an app\u2026\"\n aria-label=\"Find an app\"\n [value]=\"FilterText\"\n (input)=\"OnFilterInput($event)\"\n (keydown)=\"OnFilterKeydown($event)\"\n autocomplete=\"off\"\n spellcheck=\"false\" />\n </div>\n <button class=\"launcher-close\" type=\"button\" aria-label=\"Close application launcher\" (click)=\"CloseLauncher()\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n\n <div class=\"launcher-scroll\">\n @if (IsFiltering) {\n <div class=\"launcher-grid\">\n @for (app of VisibleApps; track app.ID; let i = $index) {\n <ng-container *ngTemplateOutlet=\"appCard; context: { $implicit: app, idx: i }\"></ng-container>\n }\n </div>\n @if (VisibleApps.length === 0) {\n <div class=\"launcher-empty\">No apps match \"{{ FilterText }}\"</div>\n }\n } @else {\n @if (RecentApps.length > 0) {\n <div class=\"launcher-sect\">Recent</div>\n <div class=\"launcher-grid\" role=\"group\" aria-label=\"Recent applications\">\n @for (app of RecentApps; track app.ID; let i = $index) {\n <ng-container *ngTemplateOutlet=\"appCard; context: { $implicit: app, idx: i }\"></ng-container>\n }\n </div>\n }\n <div class=\"launcher-sect launcher-sect--row\">\n <span>All apps</span>\n <button class=\"launcher-sort-toggle\" type=\"button\"\n [attr.aria-pressed]=\"SortMode === 'alpha'\"\n [attr.aria-label]=\"SortMode === 'alpha' ? 'Sorted alphabetically \u2014 switch to your custom order' : 'Sorted by your custom order \u2014 switch to alphabetical'\"\n [title]=\"SortMode === 'alpha' ? 'Sorted A\u2013Z \u2014 click for your custom order' : 'Custom order \u2014 click for A\u2013Z'\"\n (click)=\"ToggleSortMode()\">\n <i [class]=\"SortMode === 'alpha' ? 'fa-solid fa-arrow-down-a-z' : 'fa-solid fa-bars-staggered'\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"launcher-grid\" role=\"group\" aria-label=\"All applications\">\n @for (app of OtherApps; track app.ID; let i = $index) {\n <ng-container *ngTemplateOutlet=\"appCard; context: { $implicit: app, idx: i + RecentApps.length }\"></ng-container>\n }\n </div>\n @if (VisibleApps.length === 0) {\n <div class=\"launcher-empty\">\n No applications are enabled for you yet \u2014 use Configure\u2026 below to add some.\n </div>\n }\n }\n </div>\n\n <div class=\"launcher-foot\">\n <span class=\"launcher-kbd-hints\" aria-hidden=\"true\">\n <span><kbd>\u2191</kbd><kbd>\u2193</kbd> navigate</span>\n <span><kbd>\u21B5</kbd> open</span>\n <span><kbd>esc</kbd> close</span>\n </span>\n <button class=\"launcher-configure\" type=\"button\" testId=\"app-switcher-configure\" (click)=\"EnterConfigMode()\">\n <i class=\"fa-solid fa-gear\" aria-hidden=\"true\"></i>\n <span>Configure\u2026</span>\n </button>\n </div>\n\n <span class=\"sr-only\" role=\"status\">{{ AnnouncedAppCount }} applications available</span>\n }\n </dialog>\n }\n</div>\n\n<!-- Shared app-card template: each card is a real focusable control (mjClickable\n adds role, tabindex 0, aria-label, and Enter/Space activation), so Tab walks\n the cards and arrow keys are the fast path. -->\n<ng-template #appCard let-app let-idx=\"idx\">\n <div class=\"app-card\"\n [id]=\"OptionId(idx)\"\n [mjClickable]=\"app.Name\"\n role=\"link\"\n [testId]=\"'app-switcher-item-' + app.Name\"\n [attr.aria-current]=\"IsActiveApp(app) ? 'true' : null\"\n [attr.aria-describedby]=\"app.Description ? OptionId(idx) + '-desc' : null\"\n [class.active]=\"IsActiveApp(app)\"\n [class.loading]=\"IsLoadingApp(app)\"\n [style.--app-color]=\"app.GetColor()\"\n (click)=\"selectApp(app)\"\n (keydown)=\"OnCardKeydown(idx, $event)\">\n <div class=\"app-card-glyph\">\n @if (IsLoadingApp(app)) {\n <i class=\"fa-solid fa-spinner fa-spin\" aria-hidden=\"true\"></i>\n } @else {\n <i [class]=\"app.Icon || 'fa-solid fa-cube'\" aria-hidden=\"true\"></i>\n }\n </div>\n <div class=\"app-card-text\">\n <div class=\"app-card-name\">\n <span class=\"app-card-name-label\">{{ app.Name }}</span>\n </div>\n @if (app.Description) {\n <div class=\"app-card-desc\" [id]=\"OptionId(idx) + '-desc'\">{{ app.Description }}</div>\n }\n </div>\n </div>\n</ng-template>\n\n", styles: [".app-switcher-container {\n position: relative;\n margin-left: calc(-1 * var(--mj-space-0-5)); /* Fine-tune alignment when no nav-bar icons to the left */\n}\n\n/* ---- Trigger button (header) ---- */\n.app-switcher-button {\n display: flex;\n align-items: center;\n gap: var(--mj-space-1-5);\n padding: var(--mj-space-2) var(--mj-space-2-5);\n border-radius: var(--mj-radius-md);\n cursor: pointer;\n transition: background var(--mj-transition-fast);\n user-select: none;\n position: relative;\n}\n.app-switcher-button .app-icon {\n font-size: var(--mj-text-xl);\n width: 24px;\n text-align: center;\n position: relative;\n}\n.app-switcher-button:hover {\n background: var(--mj-bg-surface-hover);\n}\n.app-switcher-button:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n/* Current-app name in the trigger. Capped at 160px: no current catalog name\n truncates at this width \u2014 the ellipsis only guards future/client apps. */\n.app-switcher-button .app-label {\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n max-width: 160px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.app-switcher-button .dropdown-arrow {\n font-size: 10px;\n color: var(--mj-text-muted);\n transition: color var(--mj-transition-fast);\n}\n/* text-disabled sat nearly on top of --mj-bg-surface-hover in dark mode \u2014\n the chevron vanished on hover. Muted at rest, secondary on hover. */\n.app-switcher-button:hover .dropdown-arrow {\n color: var(--mj-text-secondary);\n}\n.app-switcher-button .loading-spinner {\n color: var(--mj-text-muted);\n}\n\n/* ---- Launcher overlay ----\n The panel is a native <dialog> opened with showModal(): it renders in the\n browser TOP LAYER (above every app stacking context, incl. the header's\n z-index:500 context, the chat FAB, and toasts), the page behind it is\n inert, and the whole-screen scrim is the dialog's ::backdrop. */\n.launcher-panel {\n /* Reset dialog UA defaults, then apply the command-surface spec */\n padding: 0;\n margin: 0;\n inset: unset;\n max-width: none;\n position: fixed;\n top: calc(60px + var(--mj-connectivity-banner-height, 0px) + var(--mj-space-6));\n left: 50%;\n transform: translateX(-50%);\n width: min(880px, calc(100vw - var(--mj-space-12)));\n max-height: calc(100vh - 60px - var(--mj-connectivity-banner-height, 0px) - var(--mj-space-12));\n background: var(--mj-bg-surface-elevated);\n color: inherit;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n box-shadow: var(--mj-shadow-xl);\n flex-direction: column;\n overflow: hidden;\n animation: launcher-pop .13s ease-out;\n}\n.launcher-panel[open] {\n display: flex;\n}\n.launcher-panel::backdrop {\n background: var(--mj-bg-overlay);\n animation: launcher-fade-in var(--mj-transition-fast);\n}\n\n/* ---- Compact mode (few apps / Shell.AppSwitcher.Style) ----\n Anchored under the trigger like a dropdown: no dim, single-column rows,\n no recents/filter. Entering the Configure view drops this class, promoting\n the panel back to the centered command surface (the config layout needs\n the width). Desktop-only \u2014 the mobile sheet below overrides regardless. */\n@media (min-width: 769px) {\n .launcher-panel--compact {\n left: var(--anchor-left);\n top: var(--anchor-top);\n transform: none;\n width: 320px;\n max-height: calc(100vh - var(--anchor-top) - var(--mj-space-6));\n box-shadow: var(--mj-shadow-lg);\n /* The pop animation animates translateX(-50%), which compact doesn't use */\n animation: launcher-fade-in var(--mj-transition-fast);\n }\n .launcher-panel--compact::backdrop {\n background: transparent;\n animation: none;\n }\n .launcher-panel--compact .launcher-grid {\n grid-template-columns: 1fr;\n gap: var(--mj-space-1);\n }\n .launcher-panel--compact .launcher-scroll {\n padding: var(--mj-space-2);\n }\n .launcher-panel--compact .launcher-sect {\n display: none; /* Single flat list \u2014 \"All apps\" header is noise here */\n }\n /* Compact rows are flat menu rows (not card blocks) with an explicit\n hover fill; active keeps the shared brand tint. */\n .launcher-panel--compact .app-card {\n padding: var(--mj-space-2) var(--mj-space-2-5);\n align-items: center;\n background: transparent;\n border-color: transparent;\n }\n .launcher-panel--compact .app-card:hover {\n background: var(--mj-bg-surface-hover);\n border-color: transparent;\n transform: none;\n }\n .launcher-panel--compact .app-card.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n border-color: transparent;\n }\n .launcher-panel--compact .app-card:hover {\n transform: none;\n }\n .launcher-panel--compact .app-card-glyph {\n width: 32px;\n height: 32px;\n font-size: var(--mj-text-base);\n }\n .launcher-panel--compact .app-card-desc {\n display: none;\n }\n .launcher-panel--compact .launcher-kbd-hints {\n display: none;\n }\n}\n\n@keyframes launcher-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n/* Desktop pop matches the omnibar's ob-pop exactly. The from-state keeps the\n layout translateX(-50%); on the mobile sheet (base transform: none) this\n animation is disabled rather than redefined \u2014 redefining keyframes inside\n the media query is what caused the jump-in bug. */\n@keyframes launcher-pop {\n from { opacity: 0; transform: translateX(-50%) scale(.985) translateY(-4px); }\n to { opacity: 1; transform: translateX(-50%) scale(1); }\n}\n\n/* ---- Head: filter + close ---- */\n.launcher-head {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: var(--mj-space-4) var(--mj-space-4) var(--mj-space-2);\n flex-shrink: 0;\n}\n.launcher-filter {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2-5);\n flex: 1;\n height: 46px;\n padding: 0 var(--mj-space-3-5);\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n transition: border-color var(--mj-transition-fast);\n}\n.launcher-filter:focus-within {\n border-color: var(--mj-border-focus);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n.launcher-filter i {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n flex-shrink: 0;\n}\n.launcher-filter input {\n border: none;\n outline: none;\n background: transparent;\n width: 100%;\n font-family: inherit;\n font-size: var(--mj-text-base);\n color: var(--mj-text-primary);\n}\n.launcher-filter input::placeholder {\n color: var(--mj-text-muted);\n}\n.launcher-close {\n width: 40px;\n height: 40px;\n flex-shrink: 0;\n border: none;\n border-radius: var(--mj-radius-md);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background var(--mj-transition-fast);\n}\n.launcher-close:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-close:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n/* ---- In-panel configuration view (view swap) ---- */\n.launcher-head--config {\n align-items: center;\n gap: var(--mj-space-2-5);\n padding-bottom: var(--mj-space-2);\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.launcher-back {\n width: 40px;\n height: 40px;\n flex-shrink: 0;\n border: none;\n border-radius: var(--mj-radius-md);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background var(--mj-transition-fast);\n}\n.launcher-back:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-back:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n.launcher-title {\n flex: 1;\n margin: 0;\n font-size: var(--mj-text-base);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n}\n.launcher-config-body {\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n min-height: 0;\n overflow: hidden;\n}\n/* Inline unsaved-changes bar: lives inside the panel because a body-appended\n confirm dialog would sit beneath (and inert to) the top layer. */\n.launcher-discard-bar {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2-5);\n margin: var(--mj-space-2) var(--mj-space-4) 0;\n padding: var(--mj-space-2) var(--mj-space-3);\n background: var(--mj-status-warning-bg);\n border: 1px solid var(--mj-status-warning-border);\n border-radius: var(--mj-radius-md);\n flex-shrink: 0;\n}\n.launcher-discard-bar i {\n color: var(--mj-status-warning);\n}\n.launcher-discard-bar .discard-msg {\n flex: 1;\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-medium);\n color: var(--mj-status-warning-text);\n}\n\n/* ---- Scrollable card area ---- */\n.launcher-scroll {\n overflow-y: auto;\n padding: 0 var(--mj-space-4) var(--mj-space-4);\n flex: 1 1 auto;\n min-height: 0;\n}\n/* Typography matches the omnibar's .ob-group-label */\n.launcher-sect {\n font-size: 10.5px;\n font-weight: var(--mj-font-bold);\n letter-spacing: .6px;\n text-transform: uppercase;\n color: var(--mj-text-muted);\n padding: var(--mj-space-3) var(--mj-space-1-5) var(--mj-space-1-5);\n}\n.launcher-sect--row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-right: var(--mj-space-1);\n}\n/* All-apps sort toggle: quiet icon button, brand-tinted when non-default */\n.launcher-sort-toggle {\n width: 28px;\n height: 28px;\n border: none;\n border-radius: var(--mj-radius-sm);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-xs);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background var(--mj-transition-fast), color var(--mj-transition-fast);\n}\n.launcher-sort-toggle:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-sort-toggle:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n.launcher-sort-toggle[aria-pressed=\"true\"] {\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n.launcher-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: var(--mj-space-2-5);\n}\n.launcher-empty {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n text-align: center;\n padding: var(--mj-space-6) var(--mj-space-2);\n}\n\n/* ---- App cards ---- */\n.app-card {\n display: flex;\n align-items: flex-start;\n gap: var(--mj-space-3);\n /* surface-card, NOT surface: on the elevated panel, plain surface is the\n same white in light mode and the cards visually vanish */\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n padding: var(--mj-space-3) var(--mj-space-3-5);\n cursor: pointer;\n min-width: 0;\n transition: border-color var(--mj-transition-fast), background var(--mj-transition-fast), transform var(--mj-transition-fast);\n}\n.app-card:hover {\n border-color: color-mix(in srgb, var(--mj-brand-primary) 45%, var(--mj-border-default));\n background: var(--mj-bg-surface-hover);\n transform: translateY(-1px);\n}\n.app-card:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: -1px;\n}\n.app-card.active {\n /* Shared active-tint recipe */\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n border-color: color-mix(in srgb, var(--mj-brand-primary) 45%, transparent);\n}\n.app-card.loading {\n pointer-events: none;\n opacity: 0.7;\n}\n.app-card-glyph {\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: var(--mj-text-lg);\n color: var(--app-color, var(--mj-text-muted));\n background: color-mix(in srgb, var(--app-color, var(--mj-text-muted)) 14%, var(--mj-bg-surface-card));\n}\n.app-card-text {\n min-width: 0;\n flex: 1;\n}\n.app-card-name {\n display: flex;\n align-items: baseline;\n gap: var(--mj-space-2);\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-bold);\n color: var(--mj-text-primary);\n}\n.app-card-name-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.app-card.active .app-card-name {\n color: var(--mj-brand-primary);\n}\n.app-card-desc {\n margin-top: var(--mj-space-0-5);\n font-size: var(--mj-text-xs);\n line-height: var(--mj-leading-snug);\n color: var(--mj-text-muted);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* ---- Footer ---- */\n/* Footer tone matches the omnibar's .ob-foot */\n.launcher-foot {\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface-card);\n padding: var(--mj-space-2-5) var(--mj-space-4);\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n/* Footer options button \u2014 shared spec with the omnibar's Settings button:\n quiet labeled gear, right-aligned. */\n.launcher-configure {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin-left: auto;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-family: inherit;\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-semibold);\n padding: var(--mj-space-1-5) var(--mj-space-2);\n border-radius: var(--mj-radius-md);\n cursor: pointer;\n transition: background var(--mj-transition-fast), color var(--mj-transition-fast);\n}\n.launcher-configure:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.launcher-configure:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n.launcher-kbd-hints {\n display: flex;\n gap: var(--mj-space-4);\n font-size: 11.5px;\n color: var(--mj-text-muted);\n}\n/* kbd chips: the canonical ob-kbd chrome (10.5px / 5px radius / bottom-heavy\n border) \u2014 same spec the omnibar and the header search button use. */\n.launcher-kbd-hints kbd {\n font-family: inherit;\n font-size: 10.5px;\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n border-bottom-width: 2px;\n border-radius: 5px;\n padding: 1.5px var(--mj-space-1-5);\n}\n\n/* ---- Screen-reader-only live region ---- */\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* ---- Responsive ---- */\n@media (max-width: 900px) {\n .launcher-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n/* Mobile: full sheet below the header, single-column cards, 48px+ targets.\n Breakpoint matches the shell's 768px mobile breakpoint. */\n@media (max-width: 768px) {\n /* Full-screen sheet, matching the omnibar's mobile treatment. The pop\n animation is disabled (its from-state translateX no longer applies). */\n .launcher-panel {\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n transform: none;\n width: 100%;\n max-height: none;\n height: 100dvh;\n border-radius: 0;\n border: none;\n animation: none;\n }\n .launcher-grid {\n grid-template-columns: 1fr;\n }\n .app-card {\n min-height: 48px;\n align-items: center;\n }\n .launcher-close {\n width: 46px;\n height: 46px;\n }\n .launcher-kbd-hints {\n display: none; /* No hardware-keyboard hints on touch */\n }\n .app-switcher-button .app-label {\n display: none; /* Icon-only trigger \u2014 the 390px header has no room for a name */\n }\n}\n\n/* ---- Reduced motion ---- */\n@media (prefers-reduced-motion: reduce) {\n .launcher-panel,\n .launcher-panel::backdrop {\n animation: none;\n }\n .app-card,\n .app-card:hover {\n transition: none;\n transform: none;\n }\n}\n"] }]
891
+ }], () => [{ type: i1.ApplicationManager }, { type: i0.ChangeDetectorRef }], { activeApp: [{
194
892
  type: Input
195
893
  }], isViewingSystemTab: [{
196
894
  type: Input
197
895
  }], loadingAppId: [{
198
896
  type: Input
897
+ }], SwitcherStyle: [{
898
+ type: Input
199
899
  }], appSelected: [{
200
900
  type: Output
201
- }], appConfigDialog: [{
901
+ }], filterInputRef: [{
902
+ type: ViewChild,
903
+ args: ['filterInput']
904
+ }], triggerRef: [{
905
+ type: ViewChild,
906
+ args: ['trigger']
907
+ }], panelRef: [{
908
+ type: ViewChild,
909
+ args: ['panel']
910
+ }], configContent: [{
202
911
  type: ViewChild,
203
- args: ['appConfigDialog']
204
- }], onClickOutside: [{
205
- type: HostListener,
206
- args: ['document:click', ['$event']]
912
+ args: [UserAppConfigContentComponent]
207
913
  }] }); })();
208
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AppSwitcherComponent, { className: "AppSwitcherComponent", filePath: "src/lib/shell/components/header/app-switcher.component.ts", lineNumber: 16 }); })();
914
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AppSwitcherComponent, { className: "AppSwitcherComponent", filePath: "src/lib/shell/components/header/app-switcher.component.ts", lineNumber: 63 }); })();
209
915
  //# sourceMappingURL=app-switcher.component.js.map