@meshmakers/shared-ui 3.4.1480 → 3.5.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 (56) hide show
  1. package/README.md +48 -0
  2. package/assets/octobot/lg/octo_blink.webp +0 -0
  3. package/assets/octobot/lg/octo_blink_still.png +0 -0
  4. package/assets/octobot/lg/octo_idle.webp +0 -0
  5. package/assets/octobot/lg/octo_idle_still.png +0 -0
  6. package/assets/octobot/lg/octo_look.webp +0 -0
  7. package/assets/octobot/lg/octo_look_still.png +0 -0
  8. package/assets/octobot/lg/octo_swim.webp +0 -0
  9. package/assets/octobot/lg/octo_swim_still.png +0 -0
  10. package/assets/octobot/lg/octo_thinking.webp +0 -0
  11. package/assets/octobot/lg/octo_thinking_still.png +0 -0
  12. package/assets/octobot/lg/octo_wave.webp +0 -0
  13. package/assets/octobot/lg/octo_wave_still.png +0 -0
  14. package/assets/octobot/md/octo_blink.webp +0 -0
  15. package/assets/octobot/md/octo_blink_still.png +0 -0
  16. package/assets/octobot/md/octo_idle.webp +0 -0
  17. package/assets/octobot/md/octo_idle_still.png +0 -0
  18. package/assets/octobot/md/octo_look.webp +0 -0
  19. package/assets/octobot/md/octo_look_still.png +0 -0
  20. package/assets/octobot/md/octo_swim.webp +0 -0
  21. package/assets/octobot/md/octo_swim_still.png +0 -0
  22. package/assets/octobot/md/octo_thinking.webp +0 -0
  23. package/assets/octobot/md/octo_thinking_still.png +0 -0
  24. package/assets/octobot/md/octo_wave.webp +0 -0
  25. package/assets/octobot/md/octo_wave_still.png +0 -0
  26. package/assets/octobot/sm/octo_blink.webp +0 -0
  27. package/assets/octobot/sm/octo_blink_still.png +0 -0
  28. package/assets/octobot/sm/octo_idle.webp +0 -0
  29. package/assets/octobot/sm/octo_idle_still.png +0 -0
  30. package/assets/octobot/sm/octo_look.webp +0 -0
  31. package/assets/octobot/sm/octo_look_still.png +0 -0
  32. package/assets/octobot/sm/octo_swim.webp +0 -0
  33. package/assets/octobot/sm/octo_swim_still.png +0 -0
  34. package/assets/octobot/sm/octo_thinking.webp +0 -0
  35. package/assets/octobot/sm/octo_thinking_still.png +0 -0
  36. package/assets/octobot/sm/octo_wave.webp +0 -0
  37. package/assets/octobot/sm/octo_wave_still.png +0 -0
  38. package/fesm2022/meshmakers-shared-ui-assistant-core.mjs +940 -0
  39. package/fesm2022/meshmakers-shared-ui-assistant-core.mjs.map +1 -0
  40. package/fesm2022/meshmakers-shared-ui-assistant.mjs +819 -0
  41. package/fesm2022/meshmakers-shared-ui-assistant.mjs.map +1 -0
  42. package/fesm2022/meshmakers-shared-ui-command-palette.mjs +817 -0
  43. package/fesm2022/meshmakers-shared-ui-command-palette.mjs.map +1 -0
  44. package/fesm2022/meshmakers-shared-ui-shell.mjs +1738 -0
  45. package/fesm2022/meshmakers-shared-ui-shell.mjs.map +1 -0
  46. package/fesm2022/meshmakers-shared-ui-testing.mjs +92 -0
  47. package/fesm2022/meshmakers-shared-ui-testing.mjs.map +1 -0
  48. package/fesm2022/meshmakers-shared-ui.mjs +1550 -203
  49. package/fesm2022/meshmakers-shared-ui.mjs.map +1 -1
  50. package/package.json +28 -2
  51. package/types/meshmakers-shared-ui-assistant-core.d.ts +732 -0
  52. package/types/meshmakers-shared-ui-assistant.d.ts +258 -0
  53. package/types/meshmakers-shared-ui-command-palette.d.ts +398 -0
  54. package/types/meshmakers-shared-ui-shell.d.ts +939 -0
  55. package/types/meshmakers-shared-ui-testing.d.ts +41 -0
  56. package/types/meshmakers-shared-ui.d.ts +1004 -154
@@ -0,0 +1,1738 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, computed, signal, Injectable, ElementRef, ChangeDetectorRef, input, output, DestroyRef, effect, untracked, ChangeDetectionStrategy, Component, afterNextRender, afterRenderEffect, HostListener } from '@angular/core';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
4
+ import { Router, NavigationEnd } from '@angular/router';
5
+ import { CommandService, CommandSettingsService } from '@meshmakers/shared-services';
6
+ import { filter } from 'rxjs/operators';
7
+ import { Subject } from 'rxjs';
8
+ import { NgTemplateOutlet } from '@angular/common';
9
+ import { SVGIconComponent } from '@progress/kendo-angular-icons';
10
+
11
+ /** English defaults of {@link ShellMessages}. */
12
+ const DEFAULT_SHELL_MESSAGES = {
13
+ opensInNewTab: '(opens in a new tab)',
14
+ railLabel: 'Areas',
15
+ navBadge: '{count} open',
16
+ navItemWithBadge: '{label}, {badge}',
17
+ backTo: 'Back to {target}',
18
+ statusChips: 'Status',
19
+ areaPages: '{area} pages',
20
+ areaCategories: '{area} categories',
21
+ allOfArea: 'All {area}',
22
+ modeSwitch: 'Mode',
23
+ searchAriaLabel: 'Search or run a command',
24
+ search: 'Search',
25
+ assistant: 'Assistant',
26
+ signIn: 'Sign in',
27
+ signOut: 'Sign out',
28
+ accountAndPreferences: 'Account and preferences',
29
+ accountAndPreferencesOf: 'Account and preferences of {name}',
30
+ theme: 'Theme',
31
+ themeSystem: 'System',
32
+ themeLight: 'Light',
33
+ themeDark: 'Dark',
34
+ density: 'Density',
35
+ language: 'Language',
36
+ manageProfile: 'Manage profile',
37
+ version: 'Version {version}',
38
+ environmentProduction: 'Production',
39
+ environmentProductionShort: 'Prod',
40
+ environmentStaging: 'Staging',
41
+ environmentStagingShort: 'Stage',
42
+ environmentDevelopment: 'Development',
43
+ environmentDevelopmentShort: 'Dev',
44
+ environmentTesting: 'Testing',
45
+ environmentTestingShort: 'Test',
46
+ environmentUnknown: 'Environment unknown',
47
+ environmentUnknownShort: 'Env?',
48
+ greetingMorning: 'Good morning',
49
+ greetingAfternoon: 'Good afternoon',
50
+ greetingEvening: 'Good evening',
51
+ greetingWithName: '{greeting}, {name}',
52
+ tenant: 'Tenant',
53
+ };
54
+ /**
55
+ * App-wide translations of the shell. Partial: members left out keep their English default.
56
+ */
57
+ const SHELL_MESSAGES = new InjectionToken('SHELL_MESSAGES');
58
+ /**
59
+ * Merges message overrides over {@link DEFAULT_SHELL_MESSAGES}, later sources winning.
60
+ * Members that are missing, `undefined` or `null` keep the earlier value, so a host compiled
61
+ * against an older `ShellMessages` shape never renders an empty label.
62
+ */
63
+ function resolveShellMessages(...overrides) {
64
+ const resolved = { ...DEFAULT_SHELL_MESSAGES };
65
+ for (const messages of overrides) {
66
+ if (!messages) {
67
+ continue;
68
+ }
69
+ for (const key of Object.keys(messages)) {
70
+ const value = messages[key];
71
+ if (value !== undefined && value !== null) {
72
+ resolved[key] = value;
73
+ }
74
+ }
75
+ }
76
+ return resolved;
77
+ }
78
+ /** Replaces `{name}` placeholders; unknown placeholders stay as they are. */
79
+ function formatShellMessage(message, values) {
80
+ return message.replace(/\{(\w+)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match);
81
+ }
82
+ /**
83
+ * The resolved messages of a shell component: {@link SHELL_MESSAGES} merged with the
84
+ * component's `messages` input (the input wins). Call it in an injection context.
85
+ * @internal
86
+ */
87
+ function shellMessages(input) {
88
+ const injected = inject(SHELL_MESSAGES, { optional: true });
89
+ return computed(() => resolveShellMessages(injected, input()));
90
+ }
91
+
92
+ /**
93
+ * Ids and keys shared by the host's command tree (`CommandSettingsService`), the rail,
94
+ * the space shell and route data. One definition feeds all of them:
95
+ *
96
+ * - A rail area is a top-level command item with the id `area-<space>`.
97
+ * - A tab is a direct child of an area with the id `<space>-<tab>`.
98
+ * - Routes may carry `data: { space: '<space>', tab: '<tab>' }`; the shell uses
99
+ * it where the URL alone does not match a navigation entry.
100
+ * - A top-level separator with the id {@link RAIL_BOTTOM_SEPARATOR_ID} moves the
101
+ * areas after it to the bottom of the rail (e.g. Settings).
102
+ *
103
+ * The space names themselves belong to the host (e.g. `'data' | 'integration' | …`).
104
+ */
105
+ const AREA_ID_PREFIX = 'area-';
106
+ /** Id of the separator in front of the rail's bottom block (Settings). */
107
+ const RAIL_BOTTOM_SEPARATOR_ID = 'rail-bottom';
108
+ function areaId(space) {
109
+ return `${AREA_ID_PREFIX}${space}`;
110
+ }
111
+ /** The space of an area id (`area-data` → `data`), or null for any other id. */
112
+ function spaceOfAreaId(id) {
113
+ return id.startsWith(AREA_ID_PREFIX) ? id.substring(AREA_ID_PREFIX.length) : null;
114
+ }
115
+ function tabId(space, tab) {
116
+ return `${space}-${tab}`;
117
+ }
118
+ /**
119
+ * The space whose pages use the settings side navigation (`mm-settings-side-nav`) instead
120
+ * of the tab strip. Default `'settings'` (area id `area-settings`); provide `null` for a
121
+ * host without a settings space.
122
+ */
123
+ const SHELL_SETTINGS_SPACE = new InjectionToken('SHELL_SETTINGS_SPACE', {
124
+ providedIn: 'root',
125
+ factory: () => 'settings',
126
+ });
127
+
128
+ /**
129
+ * The shell's view of the navigation: turns the flat, visibility-filtered
130
+ * item list of the CommandService into a tree (rail areas → tabs → nested
131
+ * entries) and works out which area and tab are active.
132
+ *
133
+ * The active area comes from the route's `data.space` when it names an area
134
+ * of the current mode, otherwise from the area that holds the selected
135
+ * entry. The active tab likewise comes from `data.tab` (matched as
136
+ * `<space>-<tab>`) or from the selection. URLs never change for the shell.
137
+ */
138
+ class ShellNavigationService {
139
+ commandService = inject(CommandService);
140
+ commandSettings = inject(CommandSettingsService);
141
+ router = inject(Router);
142
+ settingsSpace = inject(SHELL_SETTINGS_SPACE);
143
+ items = toSignal(this.commandService.drawerItems, { initialValue: [] });
144
+ _routeData = signal({}, /* @ts-ignore */
145
+ ...(ngDevMode ? [{ debugName: "_routeData" }] : /* istanbul ignore next */ []));
146
+ routeData = this._routeData.asReadonly();
147
+ tree = computed(() => ShellNavigationService.buildTree(this.items(), this.collectKinds()), /* @ts-ignore */
148
+ ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
149
+ /** Rail areas above the bottom separator. */
150
+ topAreas = computed(() => this.tree().top, /* @ts-ignore */
151
+ ...(ngDevMode ? [{ debugName: "topAreas" }] : /* istanbul ignore next */ []));
152
+ /** Rail areas pinned to the bottom (Settings). */
153
+ bottomAreas = computed(() => this.tree().bottom, /* @ts-ignore */
154
+ ...(ngDevMode ? [{ debugName: "bottomAreas" }] : /* istanbul ignore next */ []));
155
+ activeArea = computed(() => {
156
+ const areas = [...this.topAreas(), ...this.bottomAreas()];
157
+ const space = this._routeData().space;
158
+ if (space) {
159
+ const fromRoute = areas.find(area => area.id === areaId(space));
160
+ if (fromRoute) {
161
+ return fromRoute;
162
+ }
163
+ }
164
+ return areas.find(area => area.active) ?? null;
165
+ }, /* @ts-ignore */
166
+ ...(ngDevMode ? [{ debugName: "activeArea" }] : /* istanbul ignore next */ []));
167
+ activeTab = computed(() => {
168
+ const area = this.activeArea();
169
+ if (!area) {
170
+ return null;
171
+ }
172
+ const tab = this._routeData().tab;
173
+ const space = spaceOfAreaId(area.id);
174
+ if (tab && space) {
175
+ const fromRoute = area.children.find(child => child.id === tabId(space, tab));
176
+ if (fromRoute) {
177
+ return fromRoute;
178
+ }
179
+ }
180
+ return area.children.find(child => child.active) ?? null;
181
+ }, /* @ts-ignore */
182
+ ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
183
+ /**
184
+ * Settings has one navigation (concept §5.6, AB#5523; the space comes from {@link SHELL_SETTINGS_SPACE}): the category list on the left of every
185
+ * Settings page, never the tab strip — so opening a category cannot switch between two layouts.
186
+ * Off on object detail pages, whose object header owns the navigation.
187
+ */
188
+ settingsSideNav = computed(() => this.settingsSpace !== null && this.activeArea()?.id === areaId(this.settingsSpace) && !this.objectDetail(), /* @ts-ignore */
189
+ ...(ngDevMode ? [{ debugName: "settingsSideNav" }] : /* istanbul ignore next */ []));
190
+ /** The Settings home is open: Settings side navigation without an active category. */
191
+ settingsHomeActive = computed(() => this.settingsSideNav() && this.activeTab() === null, /* @ts-ignore */
192
+ ...(ngDevMode ? [{ debugName: "settingsHomeActive" }] : /* istanbul ignore next */ []));
193
+ /**
194
+ * Whether the space header shows the area's tab strip: not in Settings (side navigation), not
195
+ * when the route says `spaceTabs: false`; default true.
196
+ */
197
+ showTabs = computed(() => this._routeData().spaceTabs !== false && !this.settingsSideNav(), /* @ts-ignore */
198
+ ...(ngDevMode ? [{ debugName: "showTabs" }] : /* istanbul ignore next */ []));
199
+ /** The current page is an object detail page (route data `objectDetail`). */
200
+ objectDetail = computed(() => this._routeData().objectDetail === true, /* @ts-ignore */
201
+ ...(ngDevMode ? [{ debugName: "objectDetail" }] : /* istanbul ignore next */ []));
202
+ /**
203
+ * Where "back" leads from an object detail page: the selected entry of the active area
204
+ * (the list the object belongs to, e.g. Integration › Adapters), else the active tab.
205
+ */
206
+ backTarget = computed(() => {
207
+ const area = this.activeArea();
208
+ return (area ? ShellNavigationService.deepestSelected(area) : null) ?? this.activeTab();
209
+ }, /* @ts-ignore */
210
+ ...(ngDevMode ? [{ debugName: "backTarget" }] : /* istanbul ignore next */ []));
211
+ constructor() {
212
+ this.router.events
213
+ .pipe(filter(event => event instanceof NavigationEnd))
214
+ .subscribe(() => this._routeData.set(ShellNavigationService.readRouteData(this.router.routerState?.snapshot?.root)));
215
+ }
216
+ /**
217
+ * Opens an entry: navigable entries navigate (or open their href), grouping
218
+ * entries open their first navigable descendant.
219
+ */
220
+ async open(node) {
221
+ const target = node.navigable ? node : ShellNavigationService.firstPage(node, true);
222
+ if (target) {
223
+ await this.commandService.setSelectedDrawerItem({ id: target.id });
224
+ }
225
+ }
226
+ /**
227
+ * Opens an area from the rail. An area with its own in-app link (e.g. Home without tabs, or a
228
+ * Settings overview) opens that page (AB#5621); any other area opens its first page. Nothing
229
+ * happens when the current page already lives in the area (re-clicking the active area must
230
+ * not throw the person out of the page they are on). External entries are skipped; an area
231
+ * click never pops up a browser tab.
232
+ */
233
+ async openArea(area) {
234
+ if (area.id === this.activeArea()?.id) {
235
+ return;
236
+ }
237
+ const target = ShellNavigationService.areaTarget(area);
238
+ if (target) {
239
+ await this.commandService.setSelectedDrawerItem({ id: target.id });
240
+ }
241
+ }
242
+ /** Opens the first page of the first area (used after a mode switch). */
243
+ async openFirstPage() {
244
+ const first = this.topAreas()[0];
245
+ const target = first ? ShellNavigationService.areaTarget(first) : null;
246
+ if (target) {
247
+ await this.commandService.setSelectedDrawerItem({ id: target.id });
248
+ }
249
+ }
250
+ /**
251
+ * Where a rail click on an area leads: the area itself when it has its own in-app link,
252
+ * else its first navigable, in-app entry; `null` when there is none.
253
+ */
254
+ static areaTarget(area) {
255
+ if (area.navigable && !area.external) {
256
+ return area;
257
+ }
258
+ return ShellNavigationService.firstPage(area, false);
259
+ }
260
+ /** The selected entry at or below a node, deepest first; `null` when none is selected. */
261
+ static deepestSelected(node) {
262
+ for (const child of node.children) {
263
+ const nested = ShellNavigationService.deepestSelected(child);
264
+ if (nested) {
265
+ return nested;
266
+ }
267
+ }
268
+ return node.selected && node.navigable ? node : null;
269
+ }
270
+ /** The first navigable, in-app entry below a node (depth first). */
271
+ static firstPage(node, allowExternal) {
272
+ for (const child of node.children) {
273
+ if (child.separator) {
274
+ continue;
275
+ }
276
+ if (child.navigable && (allowExternal || !child.external)) {
277
+ return child;
278
+ }
279
+ const nested = ShellNavigationService.firstPage(child, allowExternal);
280
+ if (nested) {
281
+ return nested;
282
+ }
283
+ }
284
+ return null;
285
+ }
286
+ /**
287
+ * Link / href / target per item id, read from the command item tree. The
288
+ * CommandService's items carry neither, but the rail must know whether an
289
+ * entry navigates and whether it leaves the app.
290
+ */
291
+ collectKinds() {
292
+ // Re-read whenever the items change: the tree depends on mode and tenant data.
293
+ this.items();
294
+ const kinds = new Map();
295
+ const walk = (items) => {
296
+ for (const item of items) {
297
+ const navigable = item.link !== undefined || item.href !== undefined || item.onClick !== undefined;
298
+ kinds.set(item.id, {
299
+ navigable,
300
+ external: item.href !== undefined,
301
+ tooltip: item.tooltip
302
+ });
303
+ if (item.children) {
304
+ walk(item.children);
305
+ }
306
+ }
307
+ };
308
+ walk(this.commandSettings.commandItems);
309
+ return kinds;
310
+ }
311
+ static buildTree(items, kinds) {
312
+ const byParent = new Map();
313
+ const roots = [];
314
+ for (const item of items) {
315
+ const parentId = item.parentId;
316
+ if (parentId) {
317
+ const siblings = byParent.get(String(parentId)) ?? [];
318
+ siblings.push(item);
319
+ byParent.set(String(parentId), siblings);
320
+ }
321
+ else {
322
+ roots.push(item);
323
+ }
324
+ }
325
+ const toNode = (item) => {
326
+ const id = String(item.id ?? '');
327
+ const children = ShellNavigationService.tidySeparators((byParent.get(id) ?? []).map(toNode));
328
+ const kind = kinds.get(id);
329
+ const selected = item.selected === true;
330
+ return {
331
+ id,
332
+ text: item.text ?? '',
333
+ svgIcon: item.svgIcon,
334
+ tooltip: kind?.tooltip,
335
+ separator: item.separator === true,
336
+ selected,
337
+ active: selected || children.some(child => child.active),
338
+ navigable: kind?.navigable ?? false,
339
+ external: kind?.external ?? false,
340
+ children
341
+ };
342
+ };
343
+ const top = [];
344
+ const bottom = [];
345
+ let target = top;
346
+ for (const root of roots) {
347
+ if (root.separator === true) {
348
+ if (root.id === RAIL_BOTTOM_SEPARATOR_ID) {
349
+ target = bottom;
350
+ }
351
+ continue;
352
+ }
353
+ target.push(toNode(root));
354
+ }
355
+ return { top, bottom };
356
+ }
357
+ /** Drops leading, trailing and repeated separators (left over when entries around them are hidden). */
358
+ static tidySeparators(nodes) {
359
+ const result = [];
360
+ for (const node of nodes) {
361
+ if (node.separator && (result.length === 0 || result[result.length - 1].separator)) {
362
+ continue;
363
+ }
364
+ result.push(node);
365
+ }
366
+ while (result.length > 0 && result[result.length - 1].separator) {
367
+ result.pop();
368
+ }
369
+ return result;
370
+ }
371
+ static readRouteData(root) {
372
+ const data = {};
373
+ let route = root ?? null;
374
+ while (route) {
375
+ const space = route.data?.['space'];
376
+ const tab = route.data?.['tab'];
377
+ if (route.data?.['objectDetail'] === true) {
378
+ data.objectDetail = true;
379
+ }
380
+ if (typeof route.data?.['spaceTabs'] === 'boolean') {
381
+ data.spaceTabs = route.data['spaceTabs'];
382
+ }
383
+ if (typeof space === 'string') {
384
+ data.space = space;
385
+ // A new space resets the tab unless this route names one as well.
386
+ data.tab = typeof tab === 'string' ? tab : undefined;
387
+ }
388
+ else if (typeof tab === 'string') {
389
+ data.tab = tab;
390
+ }
391
+ route = route.firstChild;
392
+ }
393
+ return data;
394
+ }
395
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellNavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
396
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellNavigationService, providedIn: 'root' });
397
+ }
398
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellNavigationService, decorators: [{
399
+ type: Injectable,
400
+ args: [{ providedIn: 'root' }]
401
+ }], ctorParameters: () => [] });
402
+
403
+ /** Highest count shown as a number; larger counts read `99+`. */
404
+ const SHELL_BADGE_MAX = 99;
405
+ /**
406
+ * The badge of one entry: the `badges` input wins over the node's own `badgeCount`;
407
+ * `null` when there is nothing to show (missing, 0, negative or not a number).
408
+ */
409
+ function resolveShellBadge(badges, id, ownCount) {
410
+ const fromInput = badges?.[id];
411
+ const badge = typeof fromInput === 'number' ? { count: fromInput }
412
+ : fromInput ? fromInput
413
+ : typeof ownCount === 'number' ? { count: ownCount }
414
+ : null;
415
+ return badge && Number.isFinite(badge.count) && badge.count > 0 ? badge : null;
416
+ }
417
+ /** Visible text of a badge count: the number, or `99+` above {@link SHELL_BADGE_MAX}. */
418
+ function shellBadgeText(count) {
419
+ return count > SHELL_BADGE_MAX ? `${SHELL_BADGE_MAX}+` : String(Math.floor(count));
420
+ }
421
+
422
+ /**
423
+ * Lets the page shown inside a space contribute to the space header: status
424
+ * chips next to the title and a template for the primary actions.
425
+ *
426
+ * A page sets them in its constructor or `ngOnInit` and never cleans up: the
427
+ * shell calls {@link mark} when a navigation starts and
428
+ * {@link clearOlderThan} when it ends with a different page component, which
429
+ * drops only what the previous page contributed. A cancelled navigation or a
430
+ * param-only change (same component) keeps the chips.
431
+ */
432
+ class SpaceHeaderService {
433
+ _chips = signal([], /* @ts-ignore */
434
+ ...(ngDevMode ? [{ debugName: "_chips" }] : /* istanbul ignore next */ []));
435
+ _actions = signal(null, /* @ts-ignore */
436
+ ...(ngDevMode ? [{ debugName: "_actions" }] : /* istanbul ignore next */ []));
437
+ stamp = 0;
438
+ chipsStamp = 0;
439
+ actionsStamp = 0;
440
+ chips = this._chips.asReadonly();
441
+ actions = this._actions.asReadonly();
442
+ setChips(chips) {
443
+ this._chips.set(chips);
444
+ this.chipsStamp = ++this.stamp;
445
+ }
446
+ setActions(actions) {
447
+ this._actions.set(actions);
448
+ this.actionsStamp = ++this.stamp;
449
+ }
450
+ /** A marker for "now"; contributions made after it count as newer. */
451
+ mark() {
452
+ return this.stamp;
453
+ }
454
+ /** Drops every contribution made at or before the marker. */
455
+ clearOlderThan(marker) {
456
+ if (this.chipsStamp <= marker) {
457
+ this._chips.set([]);
458
+ }
459
+ if (this.actionsStamp <= marker) {
460
+ this._actions.set(null);
461
+ }
462
+ }
463
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SpaceHeaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
464
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SpaceHeaderService, providedIn: 'root' });
465
+ }
466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SpaceHeaderService, decorators: [{
467
+ type: Injectable,
468
+ args: [{ providedIn: 'root' }]
469
+ }] });
470
+
471
+ /**
472
+ * Hook between the top bar's search button and the command palette (AB#5520).
473
+ *
474
+ * The shell only announces that the person asked for search; it does not
475
+ * know the palette. The host (or the palette) subscribes once and opens it:
476
+ *
477
+ * ```ts
478
+ * inject(ShellSearchTriggerService).requested$
479
+ * .pipe(takeUntilDestroyed())
480
+ * .subscribe(() => commandPalette.open());
481
+ * ```
482
+ *
483
+ * Until something subscribes, a click is a no-op. The keyboard shortcut
484
+ * (Cmd/Ctrl+K) is not bound here — it belongs to the palette, which can call
485
+ * {@link request} or open itself directly.
486
+ */
487
+ class ShellSearchTriggerService {
488
+ requests = new Subject();
489
+ /** Emits every time search was requested from the shell. */
490
+ requested$ = this.requests.asObservable();
491
+ request() {
492
+ this.requests.next();
493
+ }
494
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellSearchTriggerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
495
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellSearchTriggerService, providedIn: 'root' });
496
+ }
497
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellSearchTriggerService, decorators: [{
498
+ type: Injectable,
499
+ args: [{ providedIn: 'root' }]
500
+ }] });
501
+
502
+ const NO_LEGACY_CRUMBS = new Set();
503
+ function isAreaCrumb(item) {
504
+ return !!item && typeof item.areaId === 'string';
505
+ }
506
+ /**
507
+ * Breadcrumbs as the shell shows them (AB#5516): the first crumb names the rail area of the
508
+ * page (route `data.space`), never a section of an old drawer navigation.
509
+ *
510
+ * - a legacy section crumb ({@link ShellBreadcrumbOptions.legacySectionCrumbs}) is replaced by the area crumb;
511
+ * - a first crumb that already is the area ("Settings") stays as it is;
512
+ * - otherwise the area crumb is put in front ("Data Explorer" → "Data › Data Explorer");
513
+ * - the area crumb is dropped again when the next crumb carries the same label
514
+ * ("General › Settings" → "Settings", not "Settings › Settings").
515
+ *
516
+ * - crumbs without text are dropped (no empty segments).
517
+ *
518
+ * Pure: returns a new array and never mutates the items (the BreadCrumbService re-emits the
519
+ * same objects after resolving `{{placeholders}}`).
520
+ */
521
+ function shellBreadcrumbs(allItems, area, options = {}) {
522
+ const legacyCrumbs = options.legacySectionCrumbs ?? NO_LEGACY_CRUMBS;
523
+ const legacyAreaNames = options.legacySectionAreaNames ?? {};
524
+ // A crumb without text (e.g. an unresolved or empty `{{placeholder}}`) would render as an empty
525
+ // segment ("Integration › › Mesh Adapter", visual check 2026-10-06) — drop it.
526
+ const items = allItems.filter((item) => (item.text ?? '').trim().length > 0);
527
+ if (items.length === 0) {
528
+ return [];
529
+ }
530
+ if (!area) {
531
+ const known = legacyAreaNames[(items[0].text ?? '').trim().toLowerCase()];
532
+ return known ? [{ ...items[0], text: known, title: known, labelTemplate: known }, ...items.slice(1)] : [...items];
533
+ }
534
+ const areaLabel = area.text.trim().toLowerCase();
535
+ const [first, ...rest] = items;
536
+ const firstLabel = (first.text ?? '').trim().toLowerCase();
537
+ if (firstLabel === areaLabel) {
538
+ return [...items];
539
+ }
540
+ const tail = legacyCrumbs.has(firstLabel) ? rest : [...items];
541
+ if ((tail[0]?.text ?? '').trim().toLowerCase() === areaLabel) {
542
+ return tail;
543
+ }
544
+ const areaCrumb = {
545
+ text: area.text,
546
+ title: area.text,
547
+ svgIcon: area.svgIcon,
548
+ labelTemplate: area.text,
549
+ url: '',
550
+ areaId: area.id
551
+ };
552
+ return [areaCrumb, ...tail];
553
+ }
554
+ /** "Integration › Adapters" — the description stored with a recent page. */
555
+ function crumbPath(items) {
556
+ const texts = items.map(item => item.text?.trim()).filter((text) => !!text);
557
+ return texts.length > 0 ? texts.join(' › ') : undefined;
558
+ }
559
+
560
+ /** Morning 05–11, afternoon 12–17, evening otherwise (local time of the browser). */
561
+ function greetingPhrase(now, messages) {
562
+ const m = resolveShellMessages(messages);
563
+ const hour = now.getHours();
564
+ if (hour >= 5 && hour < 12) {
565
+ return m.greetingMorning;
566
+ }
567
+ if (hour >= 12 && hour < 18) {
568
+ return m.greetingAfternoon;
569
+ }
570
+ return m.greetingEvening;
571
+ }
572
+ /** The first name: the `given_name` claim, else the first word of the display name, else null. */
573
+ function firstNameOf(user, displayName) {
574
+ const given = user?.given_name?.trim();
575
+ if (given) {
576
+ return given;
577
+ }
578
+ const first = displayName?.trim().split(/\s+/)[0];
579
+ return first ? first : null;
580
+ }
581
+ /** "Good morning, Gerald", or just "Good morning" when the name is unknown. */
582
+ function homeGreeting(user, displayName, now, messages) {
583
+ const phrase = greetingPhrase(now, messages);
584
+ const name = firstNameOf(user, displayName);
585
+ return name ? formatShellMessage(resolveShellMessages(messages).greetingWithName, { greeting: phrase, name }) : phrase;
586
+ }
587
+ /** The meta line of the Home header: the tenant (nothing without one). */
588
+ function homeMeta(tenantId, messages) {
589
+ return tenantId ? [{ label: resolveShellMessages(messages).tenant, value: tenantId }] : [];
590
+ }
591
+
592
+ /**
593
+ * The environment chip of a tenant mode, in sentence case with its status colour:
594
+ * Production (error, marks the bar), Staging (warning), Development (success),
595
+ * Testing (info), anything else "Environment unknown" (neutral).
596
+ */
597
+ function shellEnvironmentChip(mode, messages) {
598
+ const m = resolveShellMessages(messages);
599
+ switch (mode) {
600
+ case 'PRODUCTION':
601
+ return { label: m.environmentProduction, short: m.environmentProductionShort, status: 'error', production: true };
602
+ case 'STAGING':
603
+ return { label: m.environmentStaging, short: m.environmentStagingShort, status: 'warning' };
604
+ case 'DEVELOPMENT':
605
+ return { label: m.environmentDevelopment, short: m.environmentDevelopmentShort, status: 'success' };
606
+ case 'TESTING':
607
+ return { label: m.environmentTesting, short: m.environmentTestingShort, status: 'info' };
608
+ default:
609
+ return { label: m.environmentUnknown, short: m.environmentUnknownShort, status: 'neutral' };
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Toast stacking for a shell host (AB#5516 visual check: six identical "not found" errors
615
+ * covered the top bar): identical messages once; errors leave after 10 s and warnings after
616
+ * 8 s (paused while hovered or focused), except those with details, which wait for the user;
617
+ * at most three toasts, a success/info is dropped before any error. Errors that must stay
618
+ * pass `hideAfter: 0`.
619
+ *
620
+ * Use: `{ provide: NOTIFICATION_DISPLAY_OPTIONS, useValue: SHELL_NOTIFICATION_OPTIONS }`.
621
+ * Placing `.k-notification-group` below the top bar is the host's global style.
622
+ */
623
+ const SHELL_NOTIFICATION_OPTIONS = {
624
+ dedupe: true,
625
+ errorHideAfter: 10_000,
626
+ warningHideAfter: 8_000,
627
+ stickyWithDetails: true,
628
+ maxVisible: 3
629
+ };
630
+
631
+ /**
632
+ * The rail: one icon per area of the navigation tree, Settings pinned to the
633
+ * bottom. Enter/Space or a click on an icon navigates to the area. The
634
+ * flyout with the area's entries is a disclosure: it opens on hover or with
635
+ * Arrow Right (`aria-expanded` / `aria-controls` on the icon); it never opens
636
+ * on focus alone. On phones (≤ 640 px) the rail is a bottom bar without
637
+ * flyouts — the space shell's tab strip takes over.
638
+ *
639
+ * Pointer: hovering opens the flyout after a short intent delay (at once when
640
+ * another flyout is already showing). It stays open while the pointer is over
641
+ * the icon or the flyout — the flyout host spans the gap between them, so
642
+ * crossing it never counts as leaving — and closes only after a grace delay
643
+ * once the pointer has left both. A mouse click on an icon with a flyout
644
+ * still navigates, and additionally pins the flyout open (as does Arrow
645
+ * Right): a pinned flyout ignores the pointer leaving and closes on an
646
+ * outside click, Escape, choosing an entry, focus leaving the entry, a
647
+ * second click on the same icon, or a navigation (AB#5621).
648
+ *
649
+ * Navigation (AB#5621): every open flyout — pinned or hovered — closes when a
650
+ * router navigation ends or the active area changes, so a click on an icon
651
+ * that navigates does not leave the flyout over the new page. A click that
652
+ * does not navigate (the area's page is already open) keeps the pin. Closing
653
+ * on navigation moves focus only when it was inside the flyout (back to its
654
+ * area icon); focus the new page took is left alone.
655
+ *
656
+ * Keyboard: the rail is one tab stop (roving tabindex). Arrow Up/Down (Left/
657
+ * Right on the bottom bar) move between areas, Home/End jump, Arrow Right
658
+ * opens the flyout and focuses its first entry; inside the flyout Arrow
659
+ * Up/Down move, Escape or Arrow Left return to the icon. After choosing a
660
+ * flyout entry focus returns to the area icon instead of falling to <body>.
661
+ *
662
+ * Badges (AB#5621): `badges` maps node ids (areas and flyout entries) to a count or a
663
+ * {@link ShellNavBadge}; a node's own `badgeCount` is the fallback. 0 or no count shows nothing.
664
+ * The count joins the icon's accessible name ("Inbox, 3 open") and is read after a flyout entry.
665
+ *
666
+ * Every activation of an area icon emits `areaSelected` — also for areas without entries
667
+ * (Home, a Settings overview), which have no flyout; `ShellNavigationService.openArea` opens
668
+ * the area's own link then.
669
+ *
670
+ * Pure presentation: the host feeds the areas (`ShellNavigationService.topAreas()` /
671
+ * `bottomAreas()`) and handles the outputs (`openArea` / `open`).
672
+ */
673
+ /** Hover intent before a flyout opens when none is showing (ms). */
674
+ const RAIL_FLYOUT_OPEN_DELAY = 120;
675
+ /** Grace period after the pointer left both icon and flyout (ms). */
676
+ const RAIL_FLYOUT_CLOSE_DELAY = 300;
677
+ class ShellRailComponent {
678
+ host = inject(ElementRef);
679
+ changeDetector = inject(ChangeDetectorRef);
680
+ areas = input([], /* @ts-ignore */
681
+ ...(ngDevMode ? [{ debugName: "areas" }] : /* istanbul ignore next */ []));
682
+ bottomAreas = input([], /* @ts-ignore */
683
+ ...(ngDevMode ? [{ debugName: "bottomAreas" }] : /* istanbul ignore next */ []));
684
+ activeAreaId = input(null, /* @ts-ignore */
685
+ ...(ngDevMode ? [{ debugName: "activeAreaId" }] : /* istanbul ignore next */ []));
686
+ /** Counts per node id (area or flyout entry); wins over `ShellNavNode.badgeCount`. */
687
+ badges = input({}, /* @ts-ignore */
688
+ ...(ngDevMode ? [{ debugName: "badges" }] : /* istanbul ignore next */ []));
689
+ /** Translations; members left out fall back to {@link SHELL_MESSAGES}, then English. */
690
+ messages = input(null, /* @ts-ignore */
691
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
692
+ m = shellMessages(this.messages);
693
+ /** An area icon was activated. */
694
+ areaSelected = output();
695
+ /** An entry in a flyout was activated. */
696
+ itemSelected = output();
697
+ /** The area whose flyout is shown. */
698
+ openId = signal(null, /* @ts-ignore */
699
+ ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
700
+ /** Opened by click or keyboard: the pointer leaving does not close it. */
701
+ pinned = signal(false, /* @ts-ignore */
702
+ ...(ngDevMode ? [{ debugName: "pinned" }] : /* istanbul ignore next */ []));
703
+ focusedId = signal(null, /* @ts-ignore */
704
+ ...(ngDevMode ? [{ debugName: "focusedId" }] : /* istanbul ignore next */ []));
705
+ openTimer = null;
706
+ closeTimer = null;
707
+ constructor() {
708
+ const destroyRef = inject(DestroyRef);
709
+ destroyRef.onDestroy(() => this.clearTimers());
710
+ // Optional: the rail also works without a router (the active area change covers that case).
711
+ const router = inject(Router, { optional: true });
712
+ const subscription = router?.events.subscribe(event => {
713
+ if (event instanceof NavigationEnd) {
714
+ this.closeOnNavigation();
715
+ }
716
+ });
717
+ destroyRef.onDestroy(() => subscription?.unsubscribe());
718
+ let previousActive;
719
+ effect(() => {
720
+ const active = this.activeAreaId();
721
+ if (previousActive !== undefined && previousActive !== active) {
722
+ untracked(() => this.closeOnNavigation());
723
+ }
724
+ previousActive = active;
725
+ });
726
+ }
727
+ allAreas = computed(() => [...this.areas(), ...this.bottomAreas()], /* @ts-ignore */
728
+ ...(ngDevMode ? [{ debugName: "allAreas" }] : /* istanbul ignore next */ []));
729
+ /** The one area button in the tab order: the focused one, else the active one, else the first. */
730
+ tabStopId = computed(() => {
731
+ const ids = this.allAreas().map(area => area.id);
732
+ const focused = this.focusedId();
733
+ if (focused && ids.includes(focused)) {
734
+ return focused;
735
+ }
736
+ const active = this.activeAreaId();
737
+ return active && ids.includes(active) ? active : ids[0] ?? null;
738
+ }, /* @ts-ignore */
739
+ ...(ngDevMode ? [{ debugName: "tabStopId" }] : /* istanbul ignore next */ []));
740
+ hasFlyout(area) {
741
+ return area.children.some(child => !child.separator);
742
+ }
743
+ /** The badge of a node, or null when it shows none. */
744
+ badgeOf(node) {
745
+ return resolveShellBadge(this.badges(), node.id, node.badgeCount);
746
+ }
747
+ badgeText(badge) {
748
+ return shellBadgeText(badge.count);
749
+ }
750
+ /** Accessible text of a badge: its own label, else "{count} open". */
751
+ badgeLabel(badge) {
752
+ return badge.label ?? formatShellMessage(this.m().navBadge, { count: badge.count });
753
+ }
754
+ /** Accessible name of an area icon, with its badge when it has one. */
755
+ areaLabel(area) {
756
+ const badge = this.badgeOf(area);
757
+ return badge
758
+ ? formatShellMessage(this.m().navItemWithBadge, { label: area.text, badge: this.badgeLabel(badge) })
759
+ : area.text;
760
+ }
761
+ flyoutId(area) {
762
+ return `rail-flyout-${area.id}`;
763
+ }
764
+ onAreaClick(area, event) {
765
+ // A click with the pointer (detail ≥ 1) pins the flyout; Enter/Space keep the plain behaviour.
766
+ const byPointer = !!event && event.detail > 0;
767
+ if (byPointer && this.hasFlyout(area) && !this.isBottomBar()) {
768
+ if (this.pinned() && this.openId() === area.id) {
769
+ this.close();
770
+ return;
771
+ }
772
+ this.openNow(area.id, true);
773
+ }
774
+ else {
775
+ this.close();
776
+ }
777
+ this.areaSelected.emit(area);
778
+ }
779
+ onItemClick(area, item) {
780
+ // The flyout hides once closed; move focus first so it does not drop to <body>.
781
+ this.focusArea(area.id);
782
+ this.close();
783
+ this.itemSelected.emit(item);
784
+ }
785
+ onAreaFocus(area) {
786
+ this.focusedId.set(area.id);
787
+ }
788
+ onPointerEnter(area) {
789
+ this.cancelClose();
790
+ if (this.pinned() || !this.hasFlyout(area)) {
791
+ return;
792
+ }
793
+ if (this.openId() !== null) {
794
+ // Moving between areas while a flyout shows: switch without delay.
795
+ this.openNow(area.id, false);
796
+ return;
797
+ }
798
+ this.cancelOpen();
799
+ this.openTimer = setTimeout(() => {
800
+ this.openTimer = null;
801
+ this.openNow(area.id, false);
802
+ }, RAIL_FLYOUT_OPEN_DELAY);
803
+ }
804
+ onPointerLeave(area) {
805
+ this.cancelOpen();
806
+ if (this.pinned() || this.openId() !== area.id) {
807
+ return;
808
+ }
809
+ this.cancelClose();
810
+ this.closeTimer = setTimeout(() => {
811
+ this.closeTimer = null;
812
+ if (this.openId() === area.id && !this.pinned() && !this.entryOf(area)?.contains(document.activeElement)) {
813
+ this.openId.set(null);
814
+ }
815
+ }, RAIL_FLYOUT_CLOSE_DELAY);
816
+ }
817
+ onEntryFocusOut(area, event) {
818
+ const next = event.relatedTarget;
819
+ if (next instanceof Node && this.entryOf(area)?.contains(next)) {
820
+ return;
821
+ }
822
+ // Focus dropping to nothing (e.g. a click on the flyout title) is not "leaving":
823
+ // a pinned flyout waits for the outside click instead.
824
+ if (next === null && this.pinned()) {
825
+ return;
826
+ }
827
+ if (this.openId() === area.id) {
828
+ this.close();
829
+ }
830
+ }
831
+ onDocumentPointerDown(event) {
832
+ const id = this.openId();
833
+ if (!this.pinned() || id === null) {
834
+ return;
835
+ }
836
+ const target = event.target;
837
+ if (target instanceof Node && this.findByAttribute('data-entry-id', id)?.contains(target)) {
838
+ return;
839
+ }
840
+ this.close();
841
+ }
842
+ onDocumentEscape() {
843
+ if (this.openId() !== null) {
844
+ this.close();
845
+ }
846
+ }
847
+ onAreaKeydown(area, event) {
848
+ const ids = this.allAreas().map(a => a.id);
849
+ const index = ids.indexOf(area.id);
850
+ let target;
851
+ switch (event.key) {
852
+ case 'ArrowDown':
853
+ target = ids[(index + 1) % ids.length];
854
+ break;
855
+ case 'ArrowUp':
856
+ target = ids[(index - 1 + ids.length) % ids.length];
857
+ break;
858
+ case 'Home':
859
+ target = ids[0];
860
+ break;
861
+ case 'End':
862
+ target = ids[ids.length - 1];
863
+ break;
864
+ case 'ArrowRight':
865
+ if (this.isBottomBar()) {
866
+ target = ids[(index + 1) % ids.length];
867
+ break;
868
+ }
869
+ if (this.hasFlyout(area)) {
870
+ event.preventDefault();
871
+ this.openNow(area.id, true);
872
+ // The flyout is hidden by CSS (`display: none`) until the entry carries `.open`; render that
873
+ // class first, otherwise browsers (and jsdom with styles) refuse to focus the hidden entry.
874
+ this.changeDetector.detectChanges();
875
+ this.flyoutItems(area)[0]?.focus();
876
+ }
877
+ return;
878
+ case 'ArrowLeft':
879
+ if (this.isBottomBar()) {
880
+ target = ids[(index - 1 + ids.length) % ids.length];
881
+ break;
882
+ }
883
+ return;
884
+ case 'Escape':
885
+ this.close();
886
+ return;
887
+ default:
888
+ return;
889
+ }
890
+ if (target) {
891
+ event.preventDefault();
892
+ this.focusArea(target);
893
+ }
894
+ }
895
+ onFlyoutKeydown(area, event) {
896
+ const items = this.flyoutItems(area);
897
+ const index = items.indexOf(event.target);
898
+ switch (event.key) {
899
+ case 'ArrowDown':
900
+ event.preventDefault();
901
+ items[(index + 1) % items.length]?.focus();
902
+ break;
903
+ case 'ArrowUp':
904
+ event.preventDefault();
905
+ items[(index - 1 + items.length) % items.length]?.focus();
906
+ break;
907
+ case 'Home':
908
+ event.preventDefault();
909
+ items[0]?.focus();
910
+ break;
911
+ case 'End':
912
+ event.preventDefault();
913
+ items[items.length - 1]?.focus();
914
+ break;
915
+ case 'Escape':
916
+ case 'ArrowLeft':
917
+ event.preventDefault();
918
+ this.focusArea(area.id);
919
+ this.close();
920
+ break;
921
+ }
922
+ }
923
+ /** A navigation happened: close the flyout without taking focus from the new page. */
924
+ closeOnNavigation() {
925
+ const id = this.openId();
926
+ if (id === null) {
927
+ return;
928
+ }
929
+ const flyout = this.findByAttribute('id', `rail-flyout-${id}`);
930
+ if (flyout && document.activeElement instanceof Node && flyout.contains(document.activeElement)) {
931
+ // Focus would drop to <body> once the flyout hides.
932
+ this.focusArea(id);
933
+ }
934
+ this.close();
935
+ this.changeDetector.markForCheck();
936
+ }
937
+ openNow(id, pinned) {
938
+ this.clearTimers();
939
+ this.openId.set(id);
940
+ this.pinned.set(pinned);
941
+ }
942
+ close() {
943
+ this.clearTimers();
944
+ this.openId.set(null);
945
+ this.pinned.set(false);
946
+ }
947
+ cancelOpen() {
948
+ if (this.openTimer !== null) {
949
+ clearTimeout(this.openTimer);
950
+ this.openTimer = null;
951
+ }
952
+ }
953
+ cancelClose() {
954
+ if (this.closeTimer !== null) {
955
+ clearTimeout(this.closeTimer);
956
+ this.closeTimer = null;
957
+ }
958
+ }
959
+ clearTimers() {
960
+ this.cancelOpen();
961
+ this.cancelClose();
962
+ }
963
+ focusArea(id) {
964
+ this.focusedId.set(id);
965
+ this.findByAttribute('data-area-id', id)?.focus();
966
+ }
967
+ entryOf(area) {
968
+ return this.findByAttribute('data-entry-id', area.id);
969
+ }
970
+ flyoutItems(area) {
971
+ const flyout = this.findByAttribute('id', this.flyoutId(area));
972
+ return flyout ? Array.from(flyout.querySelectorAll('.flyout-item')) : [];
973
+ }
974
+ /** Attribute lookup without building a selector from ids (no escaping needed). */
975
+ findByAttribute(name, value) {
976
+ const candidates = this.host.nativeElement.querySelectorAll(`[${name}]`);
977
+ return Array.from(candidates).find(element => element.getAttribute(name) === value) ?? null;
978
+ }
979
+ /** Below 640 px the rail is laid out horizontally at the bottom. */
980
+ isBottomBar() {
981
+ return typeof window.matchMedia === 'function' && window.matchMedia('(max-width: 640px)').matches;
982
+ }
983
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellRailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
984
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: ShellRailComponent, isStandalone: true, selector: "mm-shell-rail", inputs: { areas: { classPropertyName: "areas", publicName: "areas", isSignal: true, isRequired: false, transformFunction: null }, bottomAreas: { classPropertyName: "bottomAreas", publicName: "bottomAreas", isSignal: true, isRequired: false, transformFunction: null }, activeAreaId: { classPropertyName: "activeAreaId", publicName: "activeAreaId", isSignal: true, isRequired: false, transformFunction: null }, badges: { classPropertyName: "badges", publicName: "badges", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { areaSelected: "areaSelected", itemSelected: "itemSelected" }, host: { listeners: { "document:pointerdown": "onDocumentPointerDown($event)", "document:keydown.escape": "onDocumentEscape()" } }, ngImport: i0, template: "<nav class=\"rail\" [attr.aria-label]=\"m().railLabel\">\n <ul class=\"rail-list\" role=\"list\">\n @for (area of areas(); track area.id) {\n <ng-container *ngTemplateOutlet=\"entry; context: { $implicit: area }\"></ng-container>\n }\n </ul>\n @if (bottomAreas().length > 0) {\n <ul class=\"rail-list rail-bottom\" role=\"list\">\n @for (area of bottomAreas(); track area.id) {\n <ng-container *ngTemplateOutlet=\"entry; context: { $implicit: area }\"></ng-container>\n }\n </ul>\n }\n</nav>\n\n<ng-template #entry let-area>\n <li class=\"rail-entry\"\n [attr.data-entry-id]=\"area.id\"\n [class.open]=\"openId() === area.id\"\n [class.pinned]=\"pinned() && openId() === area.id\"\n (mouseenter)=\"onPointerEnter(area)\"\n (mouseleave)=\"onPointerLeave(area)\"\n (focusout)=\"onEntryFocusOut(area, $event)\">\n <button type=\"button\"\n class=\"rail-item\"\n [attr.data-area-id]=\"area.id\"\n [attr.aria-label]=\"areaLabel(area)\"\n [attr.aria-current]=\"area.id === activeAreaId() ? 'true' : null\"\n [attr.aria-expanded]=\"hasFlyout(area) ? openId() === area.id : null\"\n [attr.aria-controls]=\"hasFlyout(area) ? flyoutId(area) : null\"\n [tabindex]=\"tabStopId() === area.id ? 0 : -1\"\n (focus)=\"onAreaFocus(area)\"\n (keydown)=\"onAreaKeydown(area, $event)\"\n (click)=\"onAreaClick(area, $event)\">\n @if (area.svgIcon) {\n <kendo-svgicon class=\"rail-icon\" [icon]=\"area.svgIcon\"></kendo-svgicon>\n }\n @if (badgeOf(area); as badge) {\n <span class=\"rail-badge\" [class.attention]=\"badge.attention\" [attr.data-badge]=\"area.id\" aria-hidden=\"true\">{{ badgeText(badge) }}</span>\n }\n </button>\n @if (hasFlyout(area)) {\n <!-- The host starts right at the icon's edge; its transparent padding bridges the gap to the panel. -->\n <div class=\"rail-flyout-host\">\n <div class=\"rail-flyout\" [id]=\"flyoutId(area)\">\n <div class=\"flyout-title\" aria-hidden=\"true\">{{ area.text }}</div>\n <ul class=\"flyout-list\" [attr.aria-label]=\"area.text\">\n @for (child of area.children; track child.id) {\n @if (child.separator) {\n <li class=\"flyout-separator\" aria-hidden=\"true\"></li>\n } @else {\n <li>\n <button type=\"button\"\n class=\"flyout-item\"\n [class.active]=\"child.active\"\n [attr.aria-current]=\"child.selected ? 'page' : null\"\n [title]=\"child.tooltip ?? ''\"\n (keydown)=\"onFlyoutKeydown(area, $event)\"\n (click)=\"onItemClick(area, child)\">\n <span class=\"flyout-text\">{{ child.text }}</span>\n @if (badgeOf(child); as badge) {\n <span class=\"flyout-badge\" [class.attention]=\"badge.attention\" [attr.data-badge]=\"child.id\" aria-hidden=\"true\">{{ badgeText(badge) }}</span><span class=\"visually-hidden\">, {{ badgeLabel(badge) }}</span>\n }\n @if (child.external) {\n <span class=\"flyout-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n </button>\n </li>\n @for (nested of child.children; track nested.id) {\n @if (!nested.separator) {\n <li>\n <button type=\"button\"\n class=\"flyout-item nested\"\n [class.active]=\"nested.active\"\n [attr.aria-current]=\"nested.selected ? 'page' : null\"\n (keydown)=\"onFlyoutKeydown(area, $event)\"\n (click)=\"onItemClick(area, nested)\">\n <span class=\"flyout-text\">{{ nested.text }}</span>\n @if (badgeOf(nested); as badge) {\n <span class=\"flyout-badge\" [class.attention]=\"badge.attention\" [attr.data-badge]=\"nested.id\" aria-hidden=\"true\">{{ badgeText(badge) }}</span><span class=\"visually-hidden\">, {{ badgeLabel(badge) }}</span>\n }\n @if (nested.external) {\n <span class=\"flyout-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n </button>\n </li>\n }\n }\n }\n }\n </ul>\n </div>\n </div>\n }\n </li>\n</ng-template>\n", styles: [":host{display:block;height:100%}.rail{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;height:100%;width:56px;padding-block:var(--theme-space-2);background:var(--theme-bg-app);border-right:1px solid var(--theme-border-subtle)}.rail-list{display:flex;flex-direction:column;align-items:center;gap:var(--theme-space-1);margin:0;padding:0;list-style:none}.rail-bottom{margin-top:auto}.rail-entry{position:relative}.rail-item{position:relative;display:grid;place-items:center;width:40px;height:40px;padding:0;border:0;border-radius:var(--theme-radius-md);background:none;color:var(--theme-text-secondary);cursor:pointer;transition:background-color var(--theme-motion-fast) ease,color var(--theme-motion-fast) ease}.rail-item:hover{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.rail-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.rail-item[aria-current=true]{background:var(--theme-bg-selected);color:var(--theme-text-accent)}.rail-item[aria-current=true]:before{content:\"\";position:absolute;left:-8px;top:10px;bottom:10px;width:2px;border-radius:var(--theme-radius-xs);background:currentColor}.rail-icon{width:20px;height:20px}.rail-badge,.flyout-badge{box-sizing:border-box;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--theme-accent);color:var(--theme-text-on-accent);font-size:.625rem;font-weight:600;line-height:16px;text-align:center;white-space:nowrap;pointer-events:none}.rail-badge.attention,.flyout-badge.attention{background:var(--theme-status-warning)}.rail-badge{position:absolute;top:2px;right:0}.flyout-badge{flex:0 0 auto}.rail-flyout-host{display:none;position:absolute;left:100%;top:0;z-index:20;padding-left:var(--theme-space-2)}.rail-flyout{min-width:200px;max-width:280px;max-height:70vh;overflow-y:auto;padding:var(--theme-space-1);background:var(--theme-bg-overlay);border-radius:var(--theme-radius-lg);box-shadow:var(--theme-shadow-popup)}.rail-entry.open .rail-flyout-host{display:block}.rail-bottom .rail-flyout-host{top:auto;bottom:0}.flyout-title{padding:var(--theme-space-1) var(--theme-space-2);font-family:var(--theme-font-display);font-size:.8125rem;font-weight:600;color:var(--theme-text-primary)}.flyout-list{margin:0;padding:0;list-style:none}.flyout-separator{height:1px;margin:var(--theme-space-1) var(--theme-space-2);background:var(--theme-border-subtle)}.flyout-item{display:flex;align-items:center;gap:var(--theme-space-2);width:100%;padding:var(--theme-space-1) var(--theme-space-2);border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);font:inherit;font-size:.8125rem;text-align:left;cursor:pointer}.flyout-item:hover,.flyout-item:focus-visible{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.flyout-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.flyout-item.active{color:var(--theme-text-accent)}.flyout-item.nested{padding-left:var(--theme-space-6)}.flyout-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.flyout-external{color:var(--theme-text-muted)}@media(prefers-reduced-motion:reduce){.rail-item{transition:none}}@media(max-width:640px){.rail{flex-direction:row;justify-content:space-around;width:100%;height:56px;padding:0 var(--theme-space-2) env(safe-area-inset-bottom,0px);border-right:0;border-top:1px solid var(--theme-border-subtle)}.rail-list{flex-direction:row;flex:1 1 auto;min-width:0;justify-content:space-around}.rail-entry{flex:0 1 40px;min-width:32px}.rail-item{width:100%}.rail-bottom{flex:0 0 auto;margin-top:0}.rail-item[aria-current=true]:before{inset:auto 10px -8px;width:auto;height:2px}.rail-entry.open .rail-flyout-host{display:none}}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"], dependencies: [{ kind: "component", type: SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
985
+ }
986
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellRailComponent, decorators: [{
987
+ type: Component,
988
+ args: [{ selector: 'mm-shell-rail', imports: [SVGIconComponent, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
989
+ '(document:pointerdown)': 'onDocumentPointerDown($event)',
990
+ '(document:keydown.escape)': 'onDocumentEscape()'
991
+ }, template: "<nav class=\"rail\" [attr.aria-label]=\"m().railLabel\">\n <ul class=\"rail-list\" role=\"list\">\n @for (area of areas(); track area.id) {\n <ng-container *ngTemplateOutlet=\"entry; context: { $implicit: area }\"></ng-container>\n }\n </ul>\n @if (bottomAreas().length > 0) {\n <ul class=\"rail-list rail-bottom\" role=\"list\">\n @for (area of bottomAreas(); track area.id) {\n <ng-container *ngTemplateOutlet=\"entry; context: { $implicit: area }\"></ng-container>\n }\n </ul>\n }\n</nav>\n\n<ng-template #entry let-area>\n <li class=\"rail-entry\"\n [attr.data-entry-id]=\"area.id\"\n [class.open]=\"openId() === area.id\"\n [class.pinned]=\"pinned() && openId() === area.id\"\n (mouseenter)=\"onPointerEnter(area)\"\n (mouseleave)=\"onPointerLeave(area)\"\n (focusout)=\"onEntryFocusOut(area, $event)\">\n <button type=\"button\"\n class=\"rail-item\"\n [attr.data-area-id]=\"area.id\"\n [attr.aria-label]=\"areaLabel(area)\"\n [attr.aria-current]=\"area.id === activeAreaId() ? 'true' : null\"\n [attr.aria-expanded]=\"hasFlyout(area) ? openId() === area.id : null\"\n [attr.aria-controls]=\"hasFlyout(area) ? flyoutId(area) : null\"\n [tabindex]=\"tabStopId() === area.id ? 0 : -1\"\n (focus)=\"onAreaFocus(area)\"\n (keydown)=\"onAreaKeydown(area, $event)\"\n (click)=\"onAreaClick(area, $event)\">\n @if (area.svgIcon) {\n <kendo-svgicon class=\"rail-icon\" [icon]=\"area.svgIcon\"></kendo-svgicon>\n }\n @if (badgeOf(area); as badge) {\n <span class=\"rail-badge\" [class.attention]=\"badge.attention\" [attr.data-badge]=\"area.id\" aria-hidden=\"true\">{{ badgeText(badge) }}</span>\n }\n </button>\n @if (hasFlyout(area)) {\n <!-- The host starts right at the icon's edge; its transparent padding bridges the gap to the panel. -->\n <div class=\"rail-flyout-host\">\n <div class=\"rail-flyout\" [id]=\"flyoutId(area)\">\n <div class=\"flyout-title\" aria-hidden=\"true\">{{ area.text }}</div>\n <ul class=\"flyout-list\" [attr.aria-label]=\"area.text\">\n @for (child of area.children; track child.id) {\n @if (child.separator) {\n <li class=\"flyout-separator\" aria-hidden=\"true\"></li>\n } @else {\n <li>\n <button type=\"button\"\n class=\"flyout-item\"\n [class.active]=\"child.active\"\n [attr.aria-current]=\"child.selected ? 'page' : null\"\n [title]=\"child.tooltip ?? ''\"\n (keydown)=\"onFlyoutKeydown(area, $event)\"\n (click)=\"onItemClick(area, child)\">\n <span class=\"flyout-text\">{{ child.text }}</span>\n @if (badgeOf(child); as badge) {\n <span class=\"flyout-badge\" [class.attention]=\"badge.attention\" [attr.data-badge]=\"child.id\" aria-hidden=\"true\">{{ badgeText(badge) }}</span><span class=\"visually-hidden\">, {{ badgeLabel(badge) }}</span>\n }\n @if (child.external) {\n <span class=\"flyout-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n </button>\n </li>\n @for (nested of child.children; track nested.id) {\n @if (!nested.separator) {\n <li>\n <button type=\"button\"\n class=\"flyout-item nested\"\n [class.active]=\"nested.active\"\n [attr.aria-current]=\"nested.selected ? 'page' : null\"\n (keydown)=\"onFlyoutKeydown(area, $event)\"\n (click)=\"onItemClick(area, nested)\">\n <span class=\"flyout-text\">{{ nested.text }}</span>\n @if (badgeOf(nested); as badge) {\n <span class=\"flyout-badge\" [class.attention]=\"badge.attention\" [attr.data-badge]=\"nested.id\" aria-hidden=\"true\">{{ badgeText(badge) }}</span><span class=\"visually-hidden\">, {{ badgeLabel(badge) }}</span>\n }\n @if (nested.external) {\n <span class=\"flyout-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n </button>\n </li>\n }\n }\n }\n }\n </ul>\n </div>\n </div>\n }\n </li>\n</ng-template>\n", styles: [":host{display:block;height:100%}.rail{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;height:100%;width:56px;padding-block:var(--theme-space-2);background:var(--theme-bg-app);border-right:1px solid var(--theme-border-subtle)}.rail-list{display:flex;flex-direction:column;align-items:center;gap:var(--theme-space-1);margin:0;padding:0;list-style:none}.rail-bottom{margin-top:auto}.rail-entry{position:relative}.rail-item{position:relative;display:grid;place-items:center;width:40px;height:40px;padding:0;border:0;border-radius:var(--theme-radius-md);background:none;color:var(--theme-text-secondary);cursor:pointer;transition:background-color var(--theme-motion-fast) ease,color var(--theme-motion-fast) ease}.rail-item:hover{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.rail-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.rail-item[aria-current=true]{background:var(--theme-bg-selected);color:var(--theme-text-accent)}.rail-item[aria-current=true]:before{content:\"\";position:absolute;left:-8px;top:10px;bottom:10px;width:2px;border-radius:var(--theme-radius-xs);background:currentColor}.rail-icon{width:20px;height:20px}.rail-badge,.flyout-badge{box-sizing:border-box;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--theme-accent);color:var(--theme-text-on-accent);font-size:.625rem;font-weight:600;line-height:16px;text-align:center;white-space:nowrap;pointer-events:none}.rail-badge.attention,.flyout-badge.attention{background:var(--theme-status-warning)}.rail-badge{position:absolute;top:2px;right:0}.flyout-badge{flex:0 0 auto}.rail-flyout-host{display:none;position:absolute;left:100%;top:0;z-index:20;padding-left:var(--theme-space-2)}.rail-flyout{min-width:200px;max-width:280px;max-height:70vh;overflow-y:auto;padding:var(--theme-space-1);background:var(--theme-bg-overlay);border-radius:var(--theme-radius-lg);box-shadow:var(--theme-shadow-popup)}.rail-entry.open .rail-flyout-host{display:block}.rail-bottom .rail-flyout-host{top:auto;bottom:0}.flyout-title{padding:var(--theme-space-1) var(--theme-space-2);font-family:var(--theme-font-display);font-size:.8125rem;font-weight:600;color:var(--theme-text-primary)}.flyout-list{margin:0;padding:0;list-style:none}.flyout-separator{height:1px;margin:var(--theme-space-1) var(--theme-space-2);background:var(--theme-border-subtle)}.flyout-item{display:flex;align-items:center;gap:var(--theme-space-2);width:100%;padding:var(--theme-space-1) var(--theme-space-2);border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);font:inherit;font-size:.8125rem;text-align:left;cursor:pointer}.flyout-item:hover,.flyout-item:focus-visible{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.flyout-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.flyout-item.active{color:var(--theme-text-accent)}.flyout-item.nested{padding-left:var(--theme-space-6)}.flyout-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.flyout-external{color:var(--theme-text-muted)}@media(prefers-reduced-motion:reduce){.rail-item{transition:none}}@media(max-width:640px){.rail{flex-direction:row;justify-content:space-around;width:100%;height:56px;padding:0 var(--theme-space-2) env(safe-area-inset-bottom,0px);border-right:0;border-top:1px solid var(--theme-border-subtle)}.rail-list{flex-direction:row;flex:1 1 auto;min-width:0;justify-content:space-around}.rail-entry{flex:0 1 40px;min-width:32px}.rail-item{width:100%}.rail-bottom{flex:0 0 auto;margin-top:0}.rail-item[aria-current=true]:before{inset:auto 10px -8px;width:auto;height:2px}.rail-entry.open .rail-flyout-host{display:none}}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] }]
992
+ }], ctorParameters: () => [], propDecorators: { areas: [{ type: i0.Input, args: [{ isSignal: true, alias: "areas", required: false }] }], bottomAreas: [{ type: i0.Input, args: [{ isSignal: true, alias: "bottomAreas", required: false }] }], activeAreaId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeAreaId", required: false }] }], badges: [{ type: i0.Input, args: [{ isSignal: true, alias: "badges", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], areaSelected: [{ type: i0.Output, args: ["areaSelected"] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }] } });
993
+
994
+ /**
995
+ * The header of a space (concept §3.3): breadcrumbs, the area title with
996
+ * optional status chips and a primary-action slot, and the tab strip built
997
+ * from the area's children — the same entries the rail flyout lists.
998
+ *
999
+ * Content projection:
1000
+ * - `[spaceCrumbs]` — the breadcrumb line above the title
1001
+ * - `[spaceActions]` — primary actions, right of the title
1002
+ *
1003
+ * Tabs are navigation, not ARIA tabs (there is no tab panel): a `nav` with
1004
+ * buttons and `aria-current="page"` on the active one. A grouping tab
1005
+ * without its own page (e.g. Swagger) is a menu button: opening moves focus
1006
+ * into the menu, Arrow Up/Down/Home/End move, Escape (on the button or in the
1007
+ * menu) closes and returns focus to the button, Tab / focus leaving / a click
1008
+ * outside / scrolling or resizing closes it. The menu is positioned `fixed`
1009
+ * from the button's bounding box, so the scrollable tab strip cannot clip it.
1010
+ * Without an area (a page outside the current mode's navigation) only the
1011
+ * breadcrumbs and actions are shown. A `heading` replaces the area name and a `meta` line
1012
+ * follows it (Home greets the user and names the tenant, wireframe screen 1).
1013
+ *
1014
+ * On an object detail page (`objectDetail`, AB#5547) the page's own object header
1015
+ * (name, chips, actions, object tabs) replaces the area title, the status chips and
1016
+ * the tab strip: the space header shrinks to one line with a back link to the list
1017
+ * the object belongs to (`backTarget`, e.g. "Adapters"), the breadcrumbs and the
1018
+ * actions slot.
1019
+ *
1020
+ * An empty breadcrumb line (AB#5621) takes no height: when the `[spaceCrumbs]` content shows
1021
+ * nothing (no text, no list item, link, button or image — e.g. a Kendo breadcrumb without
1022
+ * items) and there is no back link, the line collapses. It stays in the DOM with its width, so
1023
+ * a breadcrumb that measures its width (Kendo's collapse mode) is laid out right once items arrive.
1024
+ */
1025
+ class SpaceShellComponent {
1026
+ host = inject(ElementRef);
1027
+ area = input(null, /* @ts-ignore */
1028
+ ...(ngDevMode ? [{ debugName: "area" }] : /* istanbul ignore next */ []));
1029
+ activeTabId = input(null, /* @ts-ignore */
1030
+ ...(ngDevMode ? [{ debugName: "activeTabId" }] : /* istanbul ignore next */ []));
1031
+ chips = input([], /* @ts-ignore */
1032
+ ...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
1033
+ /** Replaces the area name as the title (Home: the greeting, AB#5558). */
1034
+ heading = input(null, /* @ts-ignore */
1035
+ ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
1036
+ /** A short meta line right of the title (Home: the tenant), hidden on phones. */
1037
+ meta = input([], /* @ts-ignore */
1038
+ ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
1039
+ /** The page renders its own object header; area title, chips and tabs are left out. */
1040
+ objectDetail = input(false, /* @ts-ignore */
1041
+ ...(ngDevMode ? [{ debugName: "objectDetail" }] : /* istanbul ignore next */ []));
1042
+ /** Show the area's tab strip; false when the page lists the same entries itself (Settings home). */
1043
+ showTabs = input(true, /* @ts-ignore */
1044
+ ...(ngDevMode ? [{ debugName: "showTabs" }] : /* istanbul ignore next */ []));
1045
+ /** Target of the back link on an object detail page (the list of the object). */
1046
+ backTarget = input(null, /* @ts-ignore */
1047
+ ...(ngDevMode ? [{ debugName: "backTarget" }] : /* istanbul ignore next */ []));
1048
+ /** Translations; members left out fall back to {@link SHELL_MESSAGES}, then English. */
1049
+ messages = input(null, /* @ts-ignore */
1050
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1051
+ m = shellMessages(this.messages);
1052
+ tabSelected = output();
1053
+ /** The grouping tab whose menu is open. */
1054
+ openMenuId = signal(null, /* @ts-ignore */
1055
+ ...(ngDevMode ? [{ debugName: "openMenuId" }] : /* istanbul ignore next */ []));
1056
+ menuPosition = signal({ top: 0, left: 0 }, /* @ts-ignore */
1057
+ ...(ngDevMode ? [{ debugName: "menuPosition" }] : /* istanbul ignore next */ []));
1058
+ tabs = computed(() => this.area()?.children ?? [], /* @ts-ignore */
1059
+ ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
1060
+ /** The breadcrumb line shows something (projected crumbs or the back link). */
1061
+ hasCrumbs = signal(true, /* @ts-ignore */
1062
+ ...(ngDevMode ? [{ debugName: "hasCrumbs" }] : /* istanbul ignore next */ []));
1063
+ constructor() {
1064
+ // The projected crumbs change outside this component (async items): watch the line itself.
1065
+ const destroyRef = inject(DestroyRef);
1066
+ afterNextRender(() => {
1067
+ const crumbs = this.crumbsElement();
1068
+ if (!crumbs) {
1069
+ return;
1070
+ }
1071
+ this.updateHasCrumbs();
1072
+ if (typeof MutationObserver === 'function') {
1073
+ const observer = new MutationObserver(() => this.updateHasCrumbs());
1074
+ observer.observe(crumbs, { childList: true, subtree: true, characterData: true });
1075
+ destroyRef.onDestroy(() => observer.disconnect());
1076
+ }
1077
+ });
1078
+ // A tab strip wider than the screen (phones) scrolls sideways: keep the active tab visible.
1079
+ afterRenderEffect(() => {
1080
+ const id = this.activeTabId();
1081
+ this.tabs();
1082
+ if (id) {
1083
+ this.revealTab(id);
1084
+ }
1085
+ });
1086
+ }
1087
+ /** Scrolls the tab strip (only it, never the page) so the tab is fully visible. */
1088
+ revealTab(id) {
1089
+ const strip = this.host.nativeElement.querySelector('.space-tabs');
1090
+ const tab = Array.from(strip?.querySelectorAll('[data-tab-id]') ?? [])
1091
+ .find(button => button.getAttribute('data-tab-id') === id);
1092
+ if (!strip || !tab) {
1093
+ return;
1094
+ }
1095
+ const stripRect = strip.getBoundingClientRect();
1096
+ const tabRect = tab.getBoundingClientRect();
1097
+ const margin = 16;
1098
+ if (tabRect.left < stripRect.left) {
1099
+ strip.scrollLeft += tabRect.left - stripRect.left - margin;
1100
+ }
1101
+ else if (tabRect.right > stripRect.right) {
1102
+ strip.scrollLeft += tabRect.right - stripRect.right + margin;
1103
+ }
1104
+ }
1105
+ crumbsElement() {
1106
+ return this.host.nativeElement.querySelector('.space-crumbs');
1107
+ }
1108
+ updateHasCrumbs() {
1109
+ const crumbs = this.crumbsElement();
1110
+ const visible = !!crumbs && ((crumbs.textContent ?? '').trim().length > 0 ||
1111
+ crumbs.querySelector('li, a, button, img, svg, input, [role="img"]') !== null);
1112
+ if (visible !== this.hasCrumbs()) {
1113
+ this.hasCrumbs.set(visible);
1114
+ }
1115
+ }
1116
+ format(message, values) {
1117
+ return formatShellMessage(message, values);
1118
+ }
1119
+ onBackClick(target) {
1120
+ this.tabSelected.emit(target);
1121
+ }
1122
+ isGroup(tab) {
1123
+ return !tab.navigable && tab.children.some(child => !child.separator);
1124
+ }
1125
+ menuId(tab) {
1126
+ return `space-tab-menu-${tab.id}`;
1127
+ }
1128
+ onTabClick(tab, button) {
1129
+ if (this.isGroup(tab)) {
1130
+ if (this.openMenuId() === tab.id) {
1131
+ this.closeMenu(false);
1132
+ }
1133
+ else {
1134
+ this.openMenu(tab, button);
1135
+ }
1136
+ return;
1137
+ }
1138
+ this.closeMenu(false);
1139
+ this.tabSelected.emit(tab);
1140
+ }
1141
+ onTabKeydown(tab, button, event) {
1142
+ if (!this.isGroup(tab)) {
1143
+ return;
1144
+ }
1145
+ if (event.key === 'ArrowDown') {
1146
+ event.preventDefault();
1147
+ this.openMenu(tab, button);
1148
+ }
1149
+ else if (event.key === 'Escape' && this.openMenuId() === tab.id) {
1150
+ event.preventDefault();
1151
+ this.closeMenu(true);
1152
+ }
1153
+ }
1154
+ onMenuItemClick(item) {
1155
+ this.closeMenu(true);
1156
+ this.tabSelected.emit(item);
1157
+ }
1158
+ onMenuKeydown(event) {
1159
+ const items = this.menuItems();
1160
+ const index = items.indexOf(event.target);
1161
+ switch (event.key) {
1162
+ case 'ArrowDown':
1163
+ event.preventDefault();
1164
+ items[(index + 1) % items.length]?.focus();
1165
+ break;
1166
+ case 'ArrowUp':
1167
+ event.preventDefault();
1168
+ items[(index - 1 + items.length) % items.length]?.focus();
1169
+ break;
1170
+ case 'Home':
1171
+ event.preventDefault();
1172
+ items[0]?.focus();
1173
+ break;
1174
+ case 'End':
1175
+ event.preventDefault();
1176
+ items[items.length - 1]?.focus();
1177
+ break;
1178
+ case 'Escape':
1179
+ event.preventDefault();
1180
+ this.closeMenu(true);
1181
+ break;
1182
+ case 'Tab':
1183
+ this.closeMenu(false);
1184
+ break;
1185
+ }
1186
+ }
1187
+ onWrapperFocusOut(tab, event) {
1188
+ const next = event.relatedTarget;
1189
+ const wrapper = event.currentTarget;
1190
+ if (this.openMenuId() === tab.id && !(next instanceof Node && wrapper.contains(next))) {
1191
+ this.openMenuId.set(null);
1192
+ }
1193
+ }
1194
+ onDocumentClick(target) {
1195
+ if (!this.openMenuId()) {
1196
+ return;
1197
+ }
1198
+ const wrapper = this.openWrapper();
1199
+ if (!(target instanceof Node && wrapper?.contains(target))) {
1200
+ this.openMenuId.set(null);
1201
+ }
1202
+ }
1203
+ onViewportChange() {
1204
+ this.openMenuId.set(null);
1205
+ }
1206
+ openMenu(tab, button) {
1207
+ const rect = button.getBoundingClientRect();
1208
+ this.menuPosition.set({ top: rect.bottom + 4, left: rect.left });
1209
+ this.openMenuId.set(tab.id);
1210
+ // The menu renders on the next change detection; focus its first entry then.
1211
+ setTimeout(() => this.menuItems()[0]?.focus());
1212
+ }
1213
+ closeMenu(returnFocus) {
1214
+ const wrapper = this.openWrapper();
1215
+ this.openMenuId.set(null);
1216
+ if (returnFocus) {
1217
+ wrapper?.querySelector('.space-tab')?.focus();
1218
+ }
1219
+ }
1220
+ openWrapper() {
1221
+ const id = this.openMenuId();
1222
+ if (!id) {
1223
+ return null;
1224
+ }
1225
+ const wrappers = this.host.nativeElement.querySelectorAll('[data-wrapper-id]');
1226
+ return Array.from(wrappers).find(wrapper => wrapper.getAttribute('data-wrapper-id') === id) ?? null;
1227
+ }
1228
+ menuItems() {
1229
+ return Array.from(this.openWrapper()?.querySelectorAll('[role="menuitem"]') ?? []);
1230
+ }
1231
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SpaceShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1232
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: SpaceShellComponent, isStandalone: true, selector: "mm-space-shell", inputs: { area: { classPropertyName: "area", publicName: "area", isSignal: true, isRequired: false, transformFunction: null }, activeTabId: { classPropertyName: "activeTabId", publicName: "activeTabId", isSignal: true, isRequired: false, transformFunction: null }, chips: { classPropertyName: "chips", publicName: "chips", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, objectDetail: { classPropertyName: "objectDetail", publicName: "objectDetail", isSignal: true, isRequired: false, transformFunction: null }, showTabs: { classPropertyName: "showTabs", publicName: "showTabs", isSignal: true, isRequired: false, transformFunction: null }, backTarget: { classPropertyName: "backTarget", publicName: "backTarget", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { tabSelected: "tabSelected" }, host: { listeners: { "document:click": "onDocumentClick($event.target)", "window:resize": "onViewportChange()", "window:scroll": "onViewportChange()" } }, ngImport: i0, template: "<header class=\"space-head\" [class.no-area]=\"!area()\" [class.object-detail]=\"objectDetail()\" [class.no-crumbs]=\"!hasCrumbs()\">\n <div class=\"space-crumbs\" [class.is-empty]=\"!hasCrumbs()\">\n @if (objectDetail() && backTarget(); as back) {\n <button type=\"button\"\n class=\"space-back\"\n data-testid=\"space-back\"\n [attr.aria-label]=\"format(m().backTo, { target: back.text })\"\n (click)=\"onBackClick(back)\">\n <span aria-hidden=\"true\">\u2190</span> {{ back.text }}\n </button>\n }\n <ng-content select=\"[spaceCrumbs]\"></ng-content>\n </div>\n <div class=\"space-title-row\">\n @if (!objectDetail()) {\n @if (area(); as current) {\n <h1 class=\"space-title\">{{ heading() ?? current.text }}</h1>\n @if (meta().length > 0) {\n <p class=\"space-meta\">\n @for (item of meta(); track item.label) {\n <span class=\"space-meta-item\">{{ item.label }} <span class=\"space-meta-value\">{{ item.value }}</span></span>\n }\n </p>\n }\n }\n }\n @if (!objectDetail() && chips().length > 0) {\n <ul class=\"space-chips\" [attr.aria-label]=\"m().statusChips\">\n @for (chip of chips(); track chip.label) {\n <li class=\"space-chip\" [class]=\"'space-chip status-' + chip.status\">{{ chip.label }}</li>\n }\n </ul>\n }\n <div class=\"space-actions\">\n <ng-content select=\"[spaceActions]\"></ng-content>\n </div>\n </div>\n @if (area(); as current) {\n @if (!objectDetail() && showTabs() && tabs().length > 0) {\n <nav class=\"space-tabs\" [attr.aria-label]=\"format(m().areaPages, { area: current.text })\" (scroll)=\"onViewportChange()\">\n @for (tab of tabs(); track tab.id) {\n @if (tab.separator) {\n <span class=\"tab-separator\" aria-hidden=\"true\"></span>\n } @else {\n <div class=\"tab-wrapper\" [attr.data-wrapper-id]=\"tab.id\" (focusout)=\"onWrapperFocusOut(tab, $event)\">\n <button #tabButton\n type=\"button\"\n class=\"space-tab\"\n [attr.data-tab-id]=\"tab.id\"\n [attr.aria-current]=\"tab.id === activeTabId() ? 'page' : null\"\n [attr.aria-haspopup]=\"isGroup(tab) ? 'menu' : null\"\n [attr.aria-expanded]=\"isGroup(tab) ? openMenuId() === tab.id : null\"\n [attr.aria-controls]=\"isGroup(tab) && openMenuId() === tab.id ? menuId(tab) : null\"\n [title]=\"tab.tooltip ?? ''\"\n (keydown)=\"onTabKeydown(tab, tabButton, $event)\"\n (click)=\"onTabClick(tab, tabButton)\">\n {{ tab.text }}\n @if (tab.external) {\n <span class=\"tab-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n @if (isGroup(tab)) {\n <span class=\"tab-caret\" aria-hidden=\"true\">\u25BE</span>\n }\n </button>\n @if (isGroup(tab) && openMenuId() === tab.id) {\n <ul class=\"tab-menu\"\n role=\"menu\"\n [id]=\"menuId(tab)\"\n [attr.aria-label]=\"tab.text\"\n [style.top.px]=\"menuPosition().top\"\n [style.left.px]=\"menuPosition().left\"\n (keydown)=\"onMenuKeydown($event)\">\n @for (item of tab.children; track item.id) {\n @if (!item.separator) {\n <li role=\"none\">\n <button type=\"button\" role=\"menuitem\" tabindex=\"-1\" class=\"tab-menu-item\" (click)=\"onMenuItemClick(item)\">\n {{ item.text }}\n @if (item.external) {\n <span class=\"tab-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n </button>\n </li>\n }\n }\n </ul>\n }\n </div>\n }\n }\n </nav>\n }\n }\n</header>\n", styles: [":host{display:block}.space-head{padding:var(--theme-space-3) var(--theme-space-6) 0;background:var(--theme-bg-app);border-bottom:1px solid var(--theme-border-subtle)}.space-head.no-area{padding-bottom:var(--theme-space-2)}.space-head.object-detail{display:flex;flex-wrap:wrap;align-items:center;gap:var(--theme-space-2) var(--theme-space-4);padding-bottom:0;border-bottom:0}.space-head.object-detail .space-crumbs{display:flex;align-items:center;gap:var(--theme-space-3);flex:1 1 auto;min-width:0}.space-head.object-detail .space-crumbs ::ng-deep kendo-breadcrumb{flex:1 1 auto;min-width:0}.space-head.object-detail .space-title-row{margin-top:0}.space-back{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);background:none;color:var(--theme-text-secondary);font:inherit;cursor:pointer;white-space:nowrap}.space-back:hover{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.space-back:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.space-crumbs{min-height:20px;font-size:.75rem;color:var(--theme-text-muted)}.space-crumbs.is-empty{min-height:0;height:0;overflow:hidden}.space-head.no-crumbs:not(.object-detail) .space-title-row{margin-top:0}.space-title-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--theme-space-2) var(--theme-space-4);margin-top:var(--theme-space-1)}.space-title{margin:0;font-family:var(--theme-font-display);font-size:1.25rem;line-height:1.75rem;font-weight:600;color:var(--theme-text-primary)}.space-meta{display:flex;flex-wrap:wrap;gap:var(--theme-space-3);margin:0;font-size:.8125rem;color:var(--theme-text-secondary)}.space-meta-value{font-family:var(--theme-font-mono);color:var(--theme-text-primary)}.space-chips{display:flex;flex-wrap:wrap;gap:var(--theme-space-2);margin:0;padding:0;list-style:none}.space-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 7px;border-radius:var(--theme-radius-xs);font-size:.71875rem;font-weight:500;white-space:nowrap}.space-chip:before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor}.space-chip.status-success{color:var(--theme-status-success);background:var(--theme-status-success-subtle)}.space-chip.status-warning{color:var(--theme-status-warning);background:var(--theme-status-warning-subtle)}.space-chip.status-error{color:var(--theme-status-error);background:var(--theme-status-error-subtle)}.space-chip.status-info{color:var(--theme-status-info);background:var(--theme-status-info-subtle)}.space-chip.status-neutral{color:var(--theme-status-neutral);background:var(--theme-status-neutral-subtle)}.space-actions{display:flex;align-items:center;gap:var(--theme-space-2);margin-left:auto}.space-tabs{display:flex;align-items:stretch;gap:2px;margin-top:var(--theme-space-3);overflow-x:auto;scrollbar-width:thin}.tab-wrapper{position:relative;display:flex}.space-tab{display:inline-flex;align-items:center;gap:var(--theme-space-1);padding:var(--theme-space-2) var(--theme-space-3);border:0;border-bottom:2px solid transparent;background:none;color:var(--theme-text-secondary);font:inherit;white-space:nowrap;cursor:pointer}.space-tab:hover{color:var(--theme-text-primary)}.space-tab:focus-visible{outline:none;box-shadow:var(--theme-focus-ring);border-radius:var(--theme-radius-sm)}.space-tab[aria-current=page]{color:var(--theme-text-primary);font-weight:500;border-bottom-color:var(--theme-accent)}.tab-separator{align-self:center;width:1px;height:16px;margin-inline:var(--theme-space-2);background:var(--theme-border-default)}.tab-external,.tab-caret{color:var(--theme-text-muted);font-size:.75rem}.tab-menu{position:fixed;z-index:20;min-width:200px;margin:0;padding:var(--theme-space-1);list-style:none;background:var(--theme-bg-overlay);border-radius:var(--theme-radius-lg);box-shadow:var(--theme-shadow-popup)}.tab-menu-item{display:flex;align-items:center;gap:var(--theme-space-2);width:100%;padding:var(--theme-space-1) var(--theme-space-2);border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);font:inherit;text-align:left;cursor:pointer}.tab-menu-item:hover,.tab-menu-item:focus-visible{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.tab-menu-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}@media(max-width:640px){.space-head{padding:var(--theme-space-2) var(--theme-space-4) 0}.space-crumbs{overflow:hidden;white-space:nowrap}.space-title-row{flex-wrap:nowrap;gap:var(--theme-space-2)}.space-title{flex:none;font-size:1.0625rem;line-height:1.5rem}.space-meta{display:none}.space-chips{flex:1 1 auto;flex-wrap:nowrap;min-width:0;overflow-x:auto;scrollbar-width:none}.space-actions{flex:none;margin-left:auto}.space-tabs{margin-top:var(--theme-space-1);scrollbar-width:none}.space-tabs::-webkit-scrollbar{display:none}.space-tab{padding:var(--theme-space-2)}.space-head.object-detail{flex-wrap:nowrap}}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1233
+ }
1234
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SpaceShellComponent, decorators: [{
1235
+ type: Component,
1236
+ args: [{ selector: 'mm-space-shell', changeDetection: ChangeDetectionStrategy.OnPush, template: "<header class=\"space-head\" [class.no-area]=\"!area()\" [class.object-detail]=\"objectDetail()\" [class.no-crumbs]=\"!hasCrumbs()\">\n <div class=\"space-crumbs\" [class.is-empty]=\"!hasCrumbs()\">\n @if (objectDetail() && backTarget(); as back) {\n <button type=\"button\"\n class=\"space-back\"\n data-testid=\"space-back\"\n [attr.aria-label]=\"format(m().backTo, { target: back.text })\"\n (click)=\"onBackClick(back)\">\n <span aria-hidden=\"true\">\u2190</span> {{ back.text }}\n </button>\n }\n <ng-content select=\"[spaceCrumbs]\"></ng-content>\n </div>\n <div class=\"space-title-row\">\n @if (!objectDetail()) {\n @if (area(); as current) {\n <h1 class=\"space-title\">{{ heading() ?? current.text }}</h1>\n @if (meta().length > 0) {\n <p class=\"space-meta\">\n @for (item of meta(); track item.label) {\n <span class=\"space-meta-item\">{{ item.label }} <span class=\"space-meta-value\">{{ item.value }}</span></span>\n }\n </p>\n }\n }\n }\n @if (!objectDetail() && chips().length > 0) {\n <ul class=\"space-chips\" [attr.aria-label]=\"m().statusChips\">\n @for (chip of chips(); track chip.label) {\n <li class=\"space-chip\" [class]=\"'space-chip status-' + chip.status\">{{ chip.label }}</li>\n }\n </ul>\n }\n <div class=\"space-actions\">\n <ng-content select=\"[spaceActions]\"></ng-content>\n </div>\n </div>\n @if (area(); as current) {\n @if (!objectDetail() && showTabs() && tabs().length > 0) {\n <nav class=\"space-tabs\" [attr.aria-label]=\"format(m().areaPages, { area: current.text })\" (scroll)=\"onViewportChange()\">\n @for (tab of tabs(); track tab.id) {\n @if (tab.separator) {\n <span class=\"tab-separator\" aria-hidden=\"true\"></span>\n } @else {\n <div class=\"tab-wrapper\" [attr.data-wrapper-id]=\"tab.id\" (focusout)=\"onWrapperFocusOut(tab, $event)\">\n <button #tabButton\n type=\"button\"\n class=\"space-tab\"\n [attr.data-tab-id]=\"tab.id\"\n [attr.aria-current]=\"tab.id === activeTabId() ? 'page' : null\"\n [attr.aria-haspopup]=\"isGroup(tab) ? 'menu' : null\"\n [attr.aria-expanded]=\"isGroup(tab) ? openMenuId() === tab.id : null\"\n [attr.aria-controls]=\"isGroup(tab) && openMenuId() === tab.id ? menuId(tab) : null\"\n [title]=\"tab.tooltip ?? ''\"\n (keydown)=\"onTabKeydown(tab, tabButton, $event)\"\n (click)=\"onTabClick(tab, tabButton)\">\n {{ tab.text }}\n @if (tab.external) {\n <span class=\"tab-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n @if (isGroup(tab)) {\n <span class=\"tab-caret\" aria-hidden=\"true\">\u25BE</span>\n }\n </button>\n @if (isGroup(tab) && openMenuId() === tab.id) {\n <ul class=\"tab-menu\"\n role=\"menu\"\n [id]=\"menuId(tab)\"\n [attr.aria-label]=\"tab.text\"\n [style.top.px]=\"menuPosition().top\"\n [style.left.px]=\"menuPosition().left\"\n (keydown)=\"onMenuKeydown($event)\">\n @for (item of tab.children; track item.id) {\n @if (!item.separator) {\n <li role=\"none\">\n <button type=\"button\" role=\"menuitem\" tabindex=\"-1\" class=\"tab-menu-item\" (click)=\"onMenuItemClick(item)\">\n {{ item.text }}\n @if (item.external) {\n <span class=\"tab-external\" aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n }\n </button>\n </li>\n }\n }\n </ul>\n }\n </div>\n }\n }\n </nav>\n }\n }\n</header>\n", styles: [":host{display:block}.space-head{padding:var(--theme-space-3) var(--theme-space-6) 0;background:var(--theme-bg-app);border-bottom:1px solid var(--theme-border-subtle)}.space-head.no-area{padding-bottom:var(--theme-space-2)}.space-head.object-detail{display:flex;flex-wrap:wrap;align-items:center;gap:var(--theme-space-2) var(--theme-space-4);padding-bottom:0;border-bottom:0}.space-head.object-detail .space-crumbs{display:flex;align-items:center;gap:var(--theme-space-3);flex:1 1 auto;min-width:0}.space-head.object-detail .space-crumbs ::ng-deep kendo-breadcrumb{flex:1 1 auto;min-width:0}.space-head.object-detail .space-title-row{margin-top:0}.space-back{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);background:none;color:var(--theme-text-secondary);font:inherit;cursor:pointer;white-space:nowrap}.space-back:hover{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.space-back:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.space-crumbs{min-height:20px;font-size:.75rem;color:var(--theme-text-muted)}.space-crumbs.is-empty{min-height:0;height:0;overflow:hidden}.space-head.no-crumbs:not(.object-detail) .space-title-row{margin-top:0}.space-title-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--theme-space-2) var(--theme-space-4);margin-top:var(--theme-space-1)}.space-title{margin:0;font-family:var(--theme-font-display);font-size:1.25rem;line-height:1.75rem;font-weight:600;color:var(--theme-text-primary)}.space-meta{display:flex;flex-wrap:wrap;gap:var(--theme-space-3);margin:0;font-size:.8125rem;color:var(--theme-text-secondary)}.space-meta-value{font-family:var(--theme-font-mono);color:var(--theme-text-primary)}.space-chips{display:flex;flex-wrap:wrap;gap:var(--theme-space-2);margin:0;padding:0;list-style:none}.space-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 7px;border-radius:var(--theme-radius-xs);font-size:.71875rem;font-weight:500;white-space:nowrap}.space-chip:before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor}.space-chip.status-success{color:var(--theme-status-success);background:var(--theme-status-success-subtle)}.space-chip.status-warning{color:var(--theme-status-warning);background:var(--theme-status-warning-subtle)}.space-chip.status-error{color:var(--theme-status-error);background:var(--theme-status-error-subtle)}.space-chip.status-info{color:var(--theme-status-info);background:var(--theme-status-info-subtle)}.space-chip.status-neutral{color:var(--theme-status-neutral);background:var(--theme-status-neutral-subtle)}.space-actions{display:flex;align-items:center;gap:var(--theme-space-2);margin-left:auto}.space-tabs{display:flex;align-items:stretch;gap:2px;margin-top:var(--theme-space-3);overflow-x:auto;scrollbar-width:thin}.tab-wrapper{position:relative;display:flex}.space-tab{display:inline-flex;align-items:center;gap:var(--theme-space-1);padding:var(--theme-space-2) var(--theme-space-3);border:0;border-bottom:2px solid transparent;background:none;color:var(--theme-text-secondary);font:inherit;white-space:nowrap;cursor:pointer}.space-tab:hover{color:var(--theme-text-primary)}.space-tab:focus-visible{outline:none;box-shadow:var(--theme-focus-ring);border-radius:var(--theme-radius-sm)}.space-tab[aria-current=page]{color:var(--theme-text-primary);font-weight:500;border-bottom-color:var(--theme-accent)}.tab-separator{align-self:center;width:1px;height:16px;margin-inline:var(--theme-space-2);background:var(--theme-border-default)}.tab-external,.tab-caret{color:var(--theme-text-muted);font-size:.75rem}.tab-menu{position:fixed;z-index:20;min-width:200px;margin:0;padding:var(--theme-space-1);list-style:none;background:var(--theme-bg-overlay);border-radius:var(--theme-radius-lg);box-shadow:var(--theme-shadow-popup)}.tab-menu-item{display:flex;align-items:center;gap:var(--theme-space-2);width:100%;padding:var(--theme-space-1) var(--theme-space-2);border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);font:inherit;text-align:left;cursor:pointer}.tab-menu-item:hover,.tab-menu-item:focus-visible{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.tab-menu-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}@media(max-width:640px){.space-head{padding:var(--theme-space-2) var(--theme-space-4) 0}.space-crumbs{overflow:hidden;white-space:nowrap}.space-title-row{flex-wrap:nowrap;gap:var(--theme-space-2)}.space-title{flex:none;font-size:1.0625rem;line-height:1.5rem}.space-meta{display:none}.space-chips{flex:1 1 auto;flex-wrap:nowrap;min-width:0;overflow-x:auto;scrollbar-width:none}.space-actions{flex:none;margin-left:auto}.space-tabs{margin-top:var(--theme-space-1);scrollbar-width:none}.space-tabs::-webkit-scrollbar{display:none}.space-tab{padding:var(--theme-space-2)}.space-head.object-detail{flex-wrap:nowrap}}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] }]
1237
+ }], ctorParameters: () => [], propDecorators: { area: [{ type: i0.Input, args: [{ isSignal: true, alias: "area", required: false }] }], activeTabId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTabId", required: false }] }], chips: [{ type: i0.Input, args: [{ isSignal: true, alias: "chips", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], objectDetail: [{ type: i0.Input, args: [{ isSignal: true, alias: "objectDetail", required: false }] }], showTabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTabs", required: false }] }], backTarget: [{ type: i0.Input, args: [{ isSignal: true, alias: "backTarget", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], tabSelected: [{ type: i0.Output, args: ["tabSelected"] }], onDocumentClick: [{
1238
+ type: HostListener,
1239
+ args: ['document:click', ['$event.target']]
1240
+ }], onViewportChange: [{
1241
+ type: HostListener,
1242
+ args: ['window:resize']
1243
+ }, {
1244
+ type: HostListener,
1245
+ args: ['window:scroll']
1246
+ }] } });
1247
+
1248
+ let nextSideNavId = 0;
1249
+ /**
1250
+ * The one navigation of the Settings space (concept §5.6, AB#5523): "All settings" plus the
1251
+ * categories of the Settings area on the left, on every Settings page — the home, the category
1252
+ * pages, the generated forms and the hand-written pages (secrets, identity providers, …). The
1253
+ * space header shows no tab strip in Settings, so opening a category never switches between two
1254
+ * navigation layouts.
1255
+ *
1256
+ * The entries are the area's children from the command tree (the same entries as the rail
1257
+ * flyout); counts and the re-entry badge come from {@link SettingsSideNavBadgesService}.
1258
+ * Navigation, not ARIA tabs: a `nav` with buttons and `aria-current="page"` on the active one.
1259
+ *
1260
+ * Groups (AB#5621, optional): `groups` puts headings over the categories they list (in the
1261
+ * group's order); categories in no group follow without a heading, groups without a visible
1262
+ * category are left out. Without groups the list is flat, as before.
1263
+ *
1264
+ * Labels are shown exactly as given (AB#5621): the area name, the categories and the group
1265
+ * headings keep the host's case — no lower-casing, no CSS `text-transform`. A language that
1266
+ * needs a different case in "All {area}" sets `allOfArea` to the whole text.
1267
+ */
1268
+ class SettingsSideNavComponent {
1269
+ area = input.required(/* @ts-ignore */
1270
+ ...(ngDevMode ? [{ debugName: "area" }] : /* istanbul ignore next */ []));
1271
+ activeTabId = input(null, /* @ts-ignore */
1272
+ ...(ngDevMode ? [{ debugName: "activeTabId" }] : /* istanbul ignore next */ []));
1273
+ /** The Settings home is open ("All settings" is the current entry). */
1274
+ homeActive = input(false, /* @ts-ignore */
1275
+ ...(ngDevMode ? [{ debugName: "homeActive" }] : /* istanbul ignore next */ []));
1276
+ badges = input({}, /* @ts-ignore */
1277
+ ...(ngDevMode ? [{ debugName: "badges" }] : /* istanbul ignore next */ []));
1278
+ /** Optional headings over the categories; empty keeps the flat list. */
1279
+ groups = input([], /* @ts-ignore */
1280
+ ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
1281
+ /** Translations; members left out fall back to {@link SHELL_MESSAGES}, then English. */
1282
+ messages = input(null, /* @ts-ignore */
1283
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1284
+ m = shellMessages(this.messages);
1285
+ homeSelected = output();
1286
+ tabSelected = output();
1287
+ categories = computed(() => this.area().children.filter((child) => !child.separator), /* @ts-ignore */
1288
+ ...(ngDevMode ? [{ debugName: "categories" }] : /* istanbul ignore next */ []));
1289
+ sections = computed(() => {
1290
+ const categories = this.categories();
1291
+ const groups = this.groups();
1292
+ if (groups.length === 0) {
1293
+ return [{ id: '', label: null, categories }];
1294
+ }
1295
+ const byId = new Map(categories.map((category) => [category.id, category]));
1296
+ const placed = new Set();
1297
+ const sections = [];
1298
+ for (const group of groups) {
1299
+ const members = group.tabIds
1300
+ .filter((id) => byId.has(id) && !placed.has(id))
1301
+ .map((id) => byId.get(id));
1302
+ members.forEach((member) => placed.add(member.id));
1303
+ if (members.length > 0) {
1304
+ sections.push({ id: group.id, label: group.label, categories: members });
1305
+ }
1306
+ }
1307
+ const rest = categories.filter((category) => !placed.has(category.id));
1308
+ if (rest.length > 0) {
1309
+ sections.push({ id: '', label: null, categories: rest });
1310
+ }
1311
+ return sections;
1312
+ }, /* @ts-ignore */
1313
+ ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
1314
+ /** Unique per instance, so two side navs on one page never share heading ids. */
1315
+ instanceId = nextSideNavId++;
1316
+ groupHeadingId(section) {
1317
+ return `mm-settings-side-nav-${this.instanceId}-group-${section.id}`;
1318
+ }
1319
+ navLabel = computed(() => formatShellMessage(this.m().areaCategories, { area: this.area().text }), /* @ts-ignore */
1320
+ ...(ngDevMode ? [{ debugName: "navLabel" }] : /* istanbul ignore next */ []));
1321
+ homeLabel = computed(() => formatShellMessage(this.m().allOfArea, { area: this.area().text }), /* @ts-ignore */
1322
+ ...(ngDevMode ? [{ debugName: "homeLabel" }] : /* istanbul ignore next */ []));
1323
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SettingsSideNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1324
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: SettingsSideNavComponent, isStandalone: true, selector: "mm-settings-side-nav", inputs: { area: { classPropertyName: "area", publicName: "area", isSignal: true, isRequired: true, transformFunction: null }, activeTabId: { classPropertyName: "activeTabId", publicName: "activeTabId", isSignal: true, isRequired: false, transformFunction: null }, homeActive: { classPropertyName: "homeActive", publicName: "homeActive", isSignal: true, isRequired: false, transformFunction: null }, badges: { classPropertyName: "badges", publicName: "badges", isSignal: true, isRequired: false, transformFunction: null }, groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { homeSelected: "homeSelected", tabSelected: "tabSelected" }, ngImport: i0, template: "<nav class=\"side-nav\" [attr.aria-label]=\"navLabel()\">\n <button type=\"button\"\n class=\"side-nav__item\"\n data-side-nav=\"home\"\n [attr.aria-current]=\"homeActive() ? 'page' : null\"\n (click)=\"homeSelected.emit()\">\n <span>{{ homeLabel() }}</span>\n </button>\n @for (section of sections(); track section.id) {\n @if (section.label) {\n <div class=\"side-nav__group\" role=\"group\" [attr.data-group]=\"section.id\" [attr.aria-labelledby]=\"groupHeadingId(section)\">\n <span class=\"side-nav__group-label\" [id]=\"groupHeadingId(section)\">{{ section.label }}</span>\n @for (category of section.categories; track category.id) {\n <ng-container *ngTemplateOutlet=\"item; context: { $implicit: category }\"></ng-container>\n }\n </div>\n } @else {\n @for (category of section.categories; track category.id) {\n <ng-container *ngTemplateOutlet=\"item; context: { $implicit: category }\"></ng-container>\n }\n }\n }\n</nav>\n\n<ng-template #item let-category>\n <button type=\"button\"\n class=\"side-nav__item\"\n [attr.data-tab-id]=\"category.id\"\n [attr.aria-current]=\"!homeActive() && category.id === activeTabId() ? 'page' : null\"\n [title]=\"category.tooltip ?? ''\"\n (click)=\"tabSelected.emit(category)\">\n <span>{{ category.text }}</span>\n @if (badges()[category.id]; as badge) {\n <span class=\"side-nav__count\"\n [class.is-attention]=\"badge.attention\"\n [attr.data-badge]=\"category.id\"\n [attr.aria-label]=\"badge.label ?? null\">{{ badge.count }}</span>\n }\n </button>\n</ng-template>\n", styles: [":host{display:block}.side-nav{display:flex;flex-direction:column;gap:2px;padding:var(--theme-space-4) var(--theme-space-3) var(--theme-space-4) var(--theme-space-6)}.side-nav__item{display:flex;justify-content:space-between;align-items:center;gap:var(--theme-space-2);width:100%;padding:8px 12px;border:0;border-radius:6px;background:none;color:var(--theme-text-secondary);font:inherit;text-align:left;cursor:pointer}.side-nav__item:hover{background:var(--theme-bg-elevated);color:var(--theme-text-primary)}.side-nav__item:focus-visible{outline:2px solid var(--theme-accent);outline-offset:-2px}.side-nav__item[aria-current=page]{background:var(--theme-bg-elevated);color:var(--theme-text-accent);font-weight:600}.side-nav__count{font-size:.75rem;color:var(--theme-text-muted)}.side-nav__count.is-attention{min-width:18px;padding:0 5px;border-radius:var(--theme-radius-xs);background:var(--theme-status-warning-subtle);color:var(--theme-status-warning);font-size:.7rem;font-weight:600;line-height:18px;text-align:center}.side-nav__group{display:flex;flex-direction:column;gap:2px;margin-top:var(--theme-space-3)}.side-nav__group-label{padding:0 12px var(--theme-space-1);color:var(--theme-text-muted);font-size:.6875rem;font-weight:600;letter-spacing:.02em}@media(max-width:640px){.side-nav{flex-direction:row;overflow-x:auto;padding:var(--theme-space-2) var(--theme-space-4)}.side-nav__group{display:contents}.side-nav__group-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.side-nav__item{width:auto;flex:0 0 auto;white-space:nowrap}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1325
+ }
1326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SettingsSideNavComponent, decorators: [{
1327
+ type: Component,
1328
+ args: [{ selector: 'mm-settings-side-nav', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<nav class=\"side-nav\" [attr.aria-label]=\"navLabel()\">\n <button type=\"button\"\n class=\"side-nav__item\"\n data-side-nav=\"home\"\n [attr.aria-current]=\"homeActive() ? 'page' : null\"\n (click)=\"homeSelected.emit()\">\n <span>{{ homeLabel() }}</span>\n </button>\n @for (section of sections(); track section.id) {\n @if (section.label) {\n <div class=\"side-nav__group\" role=\"group\" [attr.data-group]=\"section.id\" [attr.aria-labelledby]=\"groupHeadingId(section)\">\n <span class=\"side-nav__group-label\" [id]=\"groupHeadingId(section)\">{{ section.label }}</span>\n @for (category of section.categories; track category.id) {\n <ng-container *ngTemplateOutlet=\"item; context: { $implicit: category }\"></ng-container>\n }\n </div>\n } @else {\n @for (category of section.categories; track category.id) {\n <ng-container *ngTemplateOutlet=\"item; context: { $implicit: category }\"></ng-container>\n }\n }\n }\n</nav>\n\n<ng-template #item let-category>\n <button type=\"button\"\n class=\"side-nav__item\"\n [attr.data-tab-id]=\"category.id\"\n [attr.aria-current]=\"!homeActive() && category.id === activeTabId() ? 'page' : null\"\n [title]=\"category.tooltip ?? ''\"\n (click)=\"tabSelected.emit(category)\">\n <span>{{ category.text }}</span>\n @if (badges()[category.id]; as badge) {\n <span class=\"side-nav__count\"\n [class.is-attention]=\"badge.attention\"\n [attr.data-badge]=\"category.id\"\n [attr.aria-label]=\"badge.label ?? null\">{{ badge.count }}</span>\n }\n </button>\n</ng-template>\n", styles: [":host{display:block}.side-nav{display:flex;flex-direction:column;gap:2px;padding:var(--theme-space-4) var(--theme-space-3) var(--theme-space-4) var(--theme-space-6)}.side-nav__item{display:flex;justify-content:space-between;align-items:center;gap:var(--theme-space-2);width:100%;padding:8px 12px;border:0;border-radius:6px;background:none;color:var(--theme-text-secondary);font:inherit;text-align:left;cursor:pointer}.side-nav__item:hover{background:var(--theme-bg-elevated);color:var(--theme-text-primary)}.side-nav__item:focus-visible{outline:2px solid var(--theme-accent);outline-offset:-2px}.side-nav__item[aria-current=page]{background:var(--theme-bg-elevated);color:var(--theme-text-accent);font-weight:600}.side-nav__count{font-size:.75rem;color:var(--theme-text-muted)}.side-nav__count.is-attention{min-width:18px;padding:0 5px;border-radius:var(--theme-radius-xs);background:var(--theme-status-warning-subtle);color:var(--theme-status-warning);font-size:.7rem;font-weight:600;line-height:18px;text-align:center}.side-nav__group{display:flex;flex-direction:column;gap:2px;margin-top:var(--theme-space-3)}.side-nav__group-label{padding:0 12px var(--theme-space-1);color:var(--theme-text-muted);font-size:.6875rem;font-weight:600;letter-spacing:.02em}@media(max-width:640px){.side-nav{flex-direction:row;overflow-x:auto;padding:var(--theme-space-2) var(--theme-space-4)}.side-nav__group{display:contents}.side-nav__group-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.side-nav__item{width:auto;flex:0 0 auto;white-space:nowrap}}\n"] }]
1329
+ }], propDecorators: { area: [{ type: i0.Input, args: [{ isSignal: true, alias: "area", required: true }] }], activeTabId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTabId", required: false }] }], homeActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "homeActive", required: false }] }], badges: [{ type: i0.Input, args: [{ isSignal: true, alias: "badges", required: false }] }], groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], homeSelected: [{ type: i0.Output, args: ["homeSelected"] }], tabSelected: [{ type: i0.Output, args: ["tabSelected"] }] } });
1330
+
1331
+ /**
1332
+ * Counts and badges of the Settings side navigation, keyed by shell tab id
1333
+ * (`settings-<category>`). The Settings home fills them when it has loaded its
1334
+ * categories (forms + custom pages) and the secrets inventory; until then, and on a
1335
+ * deep link to another Settings page, the categories are shown without counts.
1336
+ */
1337
+ class SettingsSideNavBadgesService {
1338
+ _badges = signal({}, /* @ts-ignore */
1339
+ ...(ngDevMode ? [{ debugName: "_badges" }] : /* istanbul ignore next */ []));
1340
+ badges = this._badges.asReadonly();
1341
+ set(badges) {
1342
+ this._badges.set(badges);
1343
+ }
1344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SettingsSideNavBadgesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1345
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SettingsSideNavBadgesService, providedIn: 'root' });
1346
+ }
1347
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: SettingsSideNavBadgesService, decorators: [{
1348
+ type: Injectable,
1349
+ args: [{ providedIn: 'root' }]
1350
+ }] });
1351
+
1352
+ /** The default language list: English only, shown as a fact instead of a choice. */
1353
+ const DEFAULT_SHELL_LANGUAGES = [{ code: 'en', label: 'English' }];
1354
+ /**
1355
+ * Avatar menu of the top bar. Besides sign-out and the profile link it holds
1356
+ * the personal settings (concept §3.1, §10): theme mode, density and
1357
+ * language. Tenant configuration belongs elsewhere (the Settings space).
1358
+ *
1359
+ * - **Session**: `sessionLoading` shows a placeholder, `user` null offers "Sign in"
1360
+ * (`signIn` output), otherwise the avatar opens the panel; "Sign out" emits `signOut`.
1361
+ * The host wires its auth service (e.g. shared-auth's `AuthorizeService`).
1362
+ * - **Theme**: `theme` is the current preference, a choice emits `themeChange` (e.g. wired to
1363
+ * octo-ui's `ThemeModeService`).
1364
+ * - **Density** lists `densityOptions` (none: the group is left out); a choice emits
1365
+ * `densityChange`, the host applies and stores it and feeds `density` back.
1366
+ * - **Language** lists `languages`. With one language (the default, English) the entry
1367
+ * states it instead of offering a choice that changes nothing; with several it is a
1368
+ * radio group that emits `languageChange`. Persisting the choice (per user) and
1369
+ * switching the translations is the host's job; it feeds the current code back
1370
+ * through `language`.
1371
+ * - **Version** shows `version` when set.
1372
+ * - **Host entries** (AB#5621): `items` lists the host's own entries above "Manage profile" and
1373
+ * "Sign out"; choosing one closes the panel and emits `itemSelected` (entries with `href`
1374
+ * open in a new tab instead). For richer content the host may project elements with the
1375
+ * `shellUserMenuItems` attribute into the same place; a click on a button or link there
1376
+ * closes the panel as well.
1377
+ *
1378
+ * Opening moves focus into the panel, Escape or the avatar button close it
1379
+ * and return focus to the avatar. Theme, density and language are native radio groups
1380
+ * (arrow keys move and select, one tab stop each).
1381
+ */
1382
+ class ShellUserMenuComponent {
1383
+ host = inject(ElementRef);
1384
+ /** The signed-in user; null shows the "Sign in" button. */
1385
+ user = input(null, /* @ts-ignore */
1386
+ ...(ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []));
1387
+ /** The session is being restored: a placeholder instead of avatar or sign-in. */
1388
+ sessionLoading = input(false, /* @ts-ignore */
1389
+ ...(ngDevMode ? [{ debugName: "sessionLoading" }] : /* istanbul ignore next */ []));
1390
+ /** The current theme preference. */
1391
+ theme = input('system', /* @ts-ignore */
1392
+ ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
1393
+ /** Density choices; empty leaves the density group out. */
1394
+ densityOptions = input([], /* @ts-ignore */
1395
+ ...(ngDevMode ? [{ debugName: "densityOptions" }] : /* istanbul ignore next */ []));
1396
+ /** The current density mode (one of `densityOptions`). */
1397
+ density = input(null, /* @ts-ignore */
1398
+ ...(ngDevMode ? [{ debugName: "density" }] : /* istanbul ignore next */ []));
1399
+ /** Languages to choose from; one entry is shown as a fact, none hides the entry. */
1400
+ languages = input(DEFAULT_SHELL_LANGUAGES, /* @ts-ignore */
1401
+ ...(ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []));
1402
+ /** The current language code; defaults to the first of `languages`. */
1403
+ language = input(null, /* @ts-ignore */
1404
+ ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
1405
+ /** App version shown at the bottom of the panel; null leaves the line out. */
1406
+ version = input(null, /* @ts-ignore */
1407
+ ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ []));
1408
+ /** Host entries above "Manage profile" / "Sign out"; empty leaves the list out. */
1409
+ items = input([], /* @ts-ignore */
1410
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1411
+ /** Translations; members left out fall back to {@link SHELL_MESSAGES}, then English. */
1412
+ messages = input(null, /* @ts-ignore */
1413
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1414
+ signIn = output();
1415
+ signOut = output();
1416
+ /** The person picked a theme preference. */
1417
+ themeChange = output();
1418
+ /** The person picked a density mode. */
1419
+ densityChange = output();
1420
+ /** The person picked a language (its code). */
1421
+ languageChange = output();
1422
+ /** The person chose a host entry without `href`. */
1423
+ itemSelected = output();
1424
+ m = shellMessages(this.messages);
1425
+ open = signal(false, /* @ts-ignore */
1426
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1427
+ initials = computed(() => this.user()?.initials || '?', /* @ts-ignore */
1428
+ ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
1429
+ fullName = computed(() => this.user()?.displayName || null, /* @ts-ignore */
1430
+ ...(ngDevMode ? [{ debugName: "fullName" }] : /* istanbul ignore next */ []));
1431
+ userName = computed(() => this.user()?.userName || null, /* @ts-ignore */
1432
+ ...(ngDevMode ? [{ debugName: "userName" }] : /* istanbul ignore next */ []));
1433
+ profileUri = computed(() => this.user()?.profileUri || null, /* @ts-ignore */
1434
+ ...(ngDevMode ? [{ debugName: "profileUri" }] : /* istanbul ignore next */ []));
1435
+ avatarLabel = computed(() => {
1436
+ const name = this.fullName();
1437
+ return name ? formatShellMessage(this.m().accountAndPreferencesOf, { name }) : this.m().accountAndPreferences;
1438
+ }, /* @ts-ignore */
1439
+ ...(ngDevMode ? [{ debugName: "avatarLabel" }] : /* istanbul ignore next */ []));
1440
+ versionText = computed(() => {
1441
+ const version = this.version();
1442
+ return version ? formatShellMessage(this.m().version, { version }) : null;
1443
+ }, /* @ts-ignore */
1444
+ ...(ngDevMode ? [{ debugName: "versionText" }] : /* istanbul ignore next */ []));
1445
+ themeOptions = computed(() => [
1446
+ { value: 'system', label: this.m().themeSystem },
1447
+ { value: 'light', label: this.m().themeLight },
1448
+ { value: 'dark', label: this.m().themeDark }
1449
+ ], /* @ts-ignore */
1450
+ ...(ngDevMode ? [{ debugName: "themeOptions" }] : /* istanbul ignore next */ []));
1451
+ /** The code of the current language: `language`, else the first of `languages`. */
1452
+ currentLanguage = computed(() => this.language() ?? this.languages()[0]?.code ?? null, /* @ts-ignore */
1453
+ ...(ngDevMode ? [{ debugName: "currentLanguage" }] : /* istanbul ignore next */ []));
1454
+ currentLanguageLabel = computed(() => {
1455
+ const code = this.currentLanguage();
1456
+ const languages = this.languages();
1457
+ return (languages.find(language => language.code === code) ?? languages[0])?.label ?? null;
1458
+ }, /* @ts-ignore */
1459
+ ...(ngDevMode ? [{ debugName: "currentLanguageLabel" }] : /* istanbul ignore next */ []));
1460
+ toggle() {
1461
+ if (this.open()) {
1462
+ this.close(true);
1463
+ }
1464
+ else {
1465
+ this.open.set(true);
1466
+ // Move focus into the panel once it is rendered: the checked theme radio.
1467
+ setTimeout(() => this.host.nativeElement.querySelector('.user-panel input:checked')?.focus());
1468
+ }
1469
+ }
1470
+ /** Closes the panel; `restoreFocus` puts focus back on the avatar button. */
1471
+ close(restoreFocus) {
1472
+ this.open.set(false);
1473
+ if (restoreFocus) {
1474
+ this.host.nativeElement.querySelector('.avatar-button')?.focus();
1475
+ }
1476
+ }
1477
+ setTheme(value) {
1478
+ this.themeChange.emit(value);
1479
+ }
1480
+ setDensity(mode) {
1481
+ this.densityChange.emit(mode);
1482
+ }
1483
+ setLanguage(code) {
1484
+ if (code !== this.currentLanguage()) {
1485
+ this.languageChange.emit(code);
1486
+ }
1487
+ }
1488
+ /** A host entry: close the panel (focus back on the avatar), then let the host act. */
1489
+ selectItem(item) {
1490
+ this.close(true);
1491
+ this.itemSelected.emit(item);
1492
+ }
1493
+ /** An external host entry opens in a new tab; the panel closes and focus returns to the avatar. */
1494
+ followItemLink() {
1495
+ this.close(true);
1496
+ }
1497
+ /** A click on a button or link of the projected `shellUserMenuItems` content closes the panel. */
1498
+ onProjectedClick(event) {
1499
+ const target = event.target;
1500
+ if (target instanceof Element && target.closest('button, a[href]')) {
1501
+ this.close(true);
1502
+ }
1503
+ }
1504
+ login() {
1505
+ this.signIn.emit();
1506
+ }
1507
+ logout() {
1508
+ this.open.set(false);
1509
+ this.signOut.emit();
1510
+ }
1511
+ onEscape() {
1512
+ if (this.open()) {
1513
+ this.close(true);
1514
+ }
1515
+ }
1516
+ /** A click outside closes without stealing focus from what was clicked. */
1517
+ onDocumentClick(target) {
1518
+ if (this.open() && target instanceof Node && !this.host.nativeElement.contains(target)) {
1519
+ this.close(false);
1520
+ }
1521
+ }
1522
+ /** Tabbing out of the panel closes it. */
1523
+ onPanelFocusOut(event) {
1524
+ const next = event.relatedTarget;
1525
+ if (next instanceof Node && !this.host.nativeElement.contains(next)) {
1526
+ this.close(false);
1527
+ }
1528
+ }
1529
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellUserMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1530
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: ShellUserMenuComponent, isStandalone: true, selector: "mm-shell-user-menu", inputs: { user: { classPropertyName: "user", publicName: "user", isSignal: true, isRequired: false, transformFunction: null }, sessionLoading: { classPropertyName: "sessionLoading", publicName: "sessionLoading", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, densityOptions: { classPropertyName: "densityOptions", publicName: "densityOptions", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, version: { classPropertyName: "version", publicName: "version", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { signIn: "signIn", signOut: "signOut", themeChange: "themeChange", densityChange: "densityChange", languageChange: "languageChange", itemSelected: "itemSelected" }, host: { listeners: { "document:keydown.escape": "onEscape()", "document:click": "onDocumentClick($event.target)" } }, ngImport: i0, template: "@if (sessionLoading()) {\n <span class=\"avatar placeholder\" aria-hidden=\"true\"></span>\n} @else if (!user()) {\n <button type=\"button\" class=\"sign-in\" (click)=\"login()\">{{ m().signIn }}</button>\n} @else {\n <button type=\"button\"\n class=\"avatar-button\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n aria-controls=\"user-menu-panel\"\n [attr.aria-label]=\"avatarLabel()\"\n (click)=\"toggle()\">\n <span class=\"avatar\">{{ initials() }}</span>\n </button>\n @if (open()) {\n <div class=\"user-panel\" id=\"user-menu-panel\" role=\"dialog\" [attr.aria-label]=\"m().accountAndPreferences\" (focusout)=\"onPanelFocusOut($event)\">\n <div class=\"user-identity\">\n <span class=\"avatar large\" aria-hidden=\"true\">{{ initials() }}</span>\n <div class=\"user-names\">\n @if (fullName(); as name) {\n <span class=\"user-full-name\">{{ name }}</span>\n }\n @if (userName(); as account) {\n <span class=\"user-login\">{{ account }}</span>\n }\n </div>\n </div>\n\n <fieldset class=\"pref\">\n <legend class=\"pref-label\">{{ m().theme }}</legend>\n <div class=\"segmented\">\n @for (option of themeOptions(); track option.value) {\n <label class=\"segment\">\n <input type=\"radio\"\n name=\"user-menu-theme\"\n class=\"pref-radio\"\n [value]=\"option.value\"\n [checked]=\"theme() === option.value\"\n (change)=\"setTheme(option.value)\" />\n <span>{{ option.label }}</span>\n </label>\n }\n </div>\n </fieldset>\n\n @if (densityOptions().length > 0) {\n <fieldset class=\"pref\">\n <legend class=\"pref-label\">{{ m().density }}</legend>\n <div class=\"option-list\">\n @for (option of densityOptions(); track option.mode) {\n <label class=\"option\">\n <input type=\"radio\"\n name=\"user-menu-density\"\n class=\"pref-radio\"\n [value]=\"option.mode\"\n [checked]=\"density() === option.mode\"\n (change)=\"setDensity(option.mode)\" />\n <span>{{ option.label }}</span>\n </label>\n }\n </div>\n </fieldset>\n }\n\n @if (languages().length > 1) {\n <fieldset class=\"pref\">\n <legend class=\"pref-label\">{{ m().language }}</legend>\n <div class=\"option-list\">\n @for (option of languages(); track option.code) {\n <label class=\"option\">\n <input type=\"radio\"\n name=\"user-menu-language\"\n class=\"pref-radio\"\n [value]=\"option.code\"\n [checked]=\"currentLanguage() === option.code\"\n (change)=\"setLanguage(option.code)\" />\n <span [attr.lang]=\"option.code\">{{ option.label }}</span>\n </label>\n }\n </div>\n </fieldset>\n } @else if (currentLanguageLabel(); as languageLabel) {\n <div class=\"pref\">\n <span class=\"pref-label\">{{ m().language }}</span>\n <span class=\"pref-value\">{{ languageLabel }}</span>\n </div>\n }\n\n @if (items().length > 0) {\n <ul class=\"user-items\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (item.href) {\n <a class=\"user-item\" [attr.data-user-item]=\"item.id\" [href]=\"item.href\" target=\"_blank\" rel=\"noopener noreferrer\" (click)=\"followItemLink()\">\n @if (item.svgIcon) {\n <kendo-svgicon class=\"user-item-icon\" [icon]=\"item.svgIcon\"></kendo-svgicon>\n }\n <span class=\"user-item-text\">{{ item.text }}</span>\n <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n </a>\n } @else {\n <button type=\"button\" class=\"user-item\" [attr.data-user-item]=\"item.id\" (click)=\"selectItem(item)\">\n @if (item.svgIcon) {\n <kendo-svgicon class=\"user-item-icon\" [icon]=\"item.svgIcon\"></kendo-svgicon>\n }\n <span class=\"user-item-text\">{{ item.text }}</span>\n </button>\n }\n </li>\n }\n </ul>\n }\n <div class=\"user-items-slot\" (click)=\"onProjectedClick($event)\">\n <ng-content select=\"[shellUserMenuItems]\"></ng-content>\n </div>\n\n <div class=\"user-actions\">\n @if (profileUri(); as uri) {\n <a class=\"action-link\" [href]=\"uri\" target=\"_blank\" rel=\"noopener noreferrer\">{{ m().manageProfile }} <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span></a>\n }\n <button type=\"button\" class=\"action-button\" (click)=\"logout()\">{{ m().signOut }}</button>\n </div>\n\n @if (versionText(); as text) {\n <div class=\"user-version\">{{ text }}</div>\n }\n </div>\n }\n}\n", styles: [":host{position:relative;display:inline-flex;align-items:center}.avatar{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;border:1px solid var(--theme-border-default);background:var(--theme-bg-overlay);color:var(--theme-text-secondary);font-family:var(--theme-font-display);font-size:.6875rem;font-weight:600}.avatar.large{width:40px;height:40px;font-size:.875rem}.avatar.placeholder{opacity:.5}.avatar-button,.sign-in{padding:2px;border:0;border-radius:var(--theme-radius-sm);background:none;color:inherit;font:inherit;cursor:pointer}.avatar-button:hover .avatar,.sign-in:hover .avatar{border-color:var(--theme-border-strong);color:var(--theme-text-primary)}.avatar-button:focus-visible,.sign-in:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.sign-in{padding:var(--theme-space-1) var(--theme-space-3);background:var(--theme-accent);color:var(--theme-text-on-accent);font-weight:500}.user-panel{position:absolute;top:calc(100% + var(--theme-space-2));right:0;z-index:30;display:grid;gap:var(--theme-space-3);width:280px;padding:var(--theme-space-4);background:var(--theme-bg-overlay);border-radius:var(--theme-radius-lg);box-shadow:var(--theme-shadow-popup);color:var(--theme-text-primary)}.user-identity{display:flex;align-items:center;gap:var(--theme-space-3)}.user-names{display:grid;min-width:0}.user-full-name{font-weight:500}.user-login{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--theme-text-secondary);font-family:var(--theme-font-mono);font-size:.75rem}.pref{display:grid;gap:var(--theme-space-1);min-width:0;margin:0;padding:var(--theme-space-3) 0 0;border:0;border-top:1px solid var(--theme-border-subtle)}.pref-label{padding:0;color:var(--theme-text-secondary);font-size:.75rem}.pref-value{color:var(--theme-text-primary)}.pref-radio{position:absolute;inset:0;z-index:1;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}.segmented{display:inline-flex;padding:2px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm)}.segment{flex:1;position:relative}.segment span{display:block;padding:2px var(--theme-space-2);border-radius:var(--theme-radius-xs);color:var(--theme-text-secondary);font-weight:500;text-align:center;cursor:pointer}.segment input:checked+span{background:var(--theme-bg-elevated);color:var(--theme-text-primary);box-shadow:0 0 0 1px var(--theme-border-default)}.segment input:focus-visible+span{box-shadow:var(--theme-focus-ring)}.option-list{display:grid}.option{position:relative}.option span{display:flex;align-items:center;gap:var(--theme-space-2);padding:var(--theme-space-1) var(--theme-space-2);border-radius:var(--theme-radius-sm);color:var(--theme-text-secondary);cursor:pointer}.option span:before{content:\"\";width:6px;height:6px;border-radius:50%;border:1px solid currentColor}.option:hover span{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.option input:checked+span{color:var(--theme-text-accent)}.option input:checked+span:before{background:currentColor}.option input:focus-visible+span{box-shadow:var(--theme-focus-ring)}.user-items{display:flex;flex-direction:column;gap:2px;margin:0;padding:var(--theme-space-2) 0 0;list-style:none;border-top:1px solid var(--theme-border-subtle)}.user-item{display:flex;align-items:center;gap:var(--theme-space-2);width:100%;box-sizing:border-box;padding:var(--theme-space-1) var(--theme-space-2);border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;text-align:left;text-decoration:none;cursor:pointer}.user-item:hover{background:var(--theme-bg-hover)}.user-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.user-item-icon{width:16px;height:16px;color:var(--theme-text-secondary)}.user-item-text{flex:1}.user-items-slot:empty{display:none}.user-actions{display:flex;align-items:center;justify-content:space-between;gap:var(--theme-space-2);padding-top:var(--theme-space-3);border-top:1px solid var(--theme-border-subtle)}.action-link{border-radius:var(--theme-radius-xs);color:var(--theme-text-accent);text-decoration:none}.action-link:hover{text-decoration:underline}.action-link:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.action-button{margin-left:auto;padding:var(--theme-space-1) var(--theme-space-3);border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-surface);color:var(--theme-text-primary);font:inherit;cursor:pointer}.action-button:hover{border-color:var(--theme-border-strong)}.action-button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.user-version{color:var(--theme-text-muted);font-family:var(--theme-font-mono);font-size:.6875rem}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"], dependencies: [{ kind: "component", type: SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1531
+ }
1532
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellUserMenuComponent, decorators: [{
1533
+ type: Component,
1534
+ args: [{ selector: 'mm-shell-user-menu', imports: [SVGIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (sessionLoading()) {\n <span class=\"avatar placeholder\" aria-hidden=\"true\"></span>\n} @else if (!user()) {\n <button type=\"button\" class=\"sign-in\" (click)=\"login()\">{{ m().signIn }}</button>\n} @else {\n <button type=\"button\"\n class=\"avatar-button\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n aria-controls=\"user-menu-panel\"\n [attr.aria-label]=\"avatarLabel()\"\n (click)=\"toggle()\">\n <span class=\"avatar\">{{ initials() }}</span>\n </button>\n @if (open()) {\n <div class=\"user-panel\" id=\"user-menu-panel\" role=\"dialog\" [attr.aria-label]=\"m().accountAndPreferences\" (focusout)=\"onPanelFocusOut($event)\">\n <div class=\"user-identity\">\n <span class=\"avatar large\" aria-hidden=\"true\">{{ initials() }}</span>\n <div class=\"user-names\">\n @if (fullName(); as name) {\n <span class=\"user-full-name\">{{ name }}</span>\n }\n @if (userName(); as account) {\n <span class=\"user-login\">{{ account }}</span>\n }\n </div>\n </div>\n\n <fieldset class=\"pref\">\n <legend class=\"pref-label\">{{ m().theme }}</legend>\n <div class=\"segmented\">\n @for (option of themeOptions(); track option.value) {\n <label class=\"segment\">\n <input type=\"radio\"\n name=\"user-menu-theme\"\n class=\"pref-radio\"\n [value]=\"option.value\"\n [checked]=\"theme() === option.value\"\n (change)=\"setTheme(option.value)\" />\n <span>{{ option.label }}</span>\n </label>\n }\n </div>\n </fieldset>\n\n @if (densityOptions().length > 0) {\n <fieldset class=\"pref\">\n <legend class=\"pref-label\">{{ m().density }}</legend>\n <div class=\"option-list\">\n @for (option of densityOptions(); track option.mode) {\n <label class=\"option\">\n <input type=\"radio\"\n name=\"user-menu-density\"\n class=\"pref-radio\"\n [value]=\"option.mode\"\n [checked]=\"density() === option.mode\"\n (change)=\"setDensity(option.mode)\" />\n <span>{{ option.label }}</span>\n </label>\n }\n </div>\n </fieldset>\n }\n\n @if (languages().length > 1) {\n <fieldset class=\"pref\">\n <legend class=\"pref-label\">{{ m().language }}</legend>\n <div class=\"option-list\">\n @for (option of languages(); track option.code) {\n <label class=\"option\">\n <input type=\"radio\"\n name=\"user-menu-language\"\n class=\"pref-radio\"\n [value]=\"option.code\"\n [checked]=\"currentLanguage() === option.code\"\n (change)=\"setLanguage(option.code)\" />\n <span [attr.lang]=\"option.code\">{{ option.label }}</span>\n </label>\n }\n </div>\n </fieldset>\n } @else if (currentLanguageLabel(); as languageLabel) {\n <div class=\"pref\">\n <span class=\"pref-label\">{{ m().language }}</span>\n <span class=\"pref-value\">{{ languageLabel }}</span>\n </div>\n }\n\n @if (items().length > 0) {\n <ul class=\"user-items\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (item.href) {\n <a class=\"user-item\" [attr.data-user-item]=\"item.id\" [href]=\"item.href\" target=\"_blank\" rel=\"noopener noreferrer\" (click)=\"followItemLink()\">\n @if (item.svgIcon) {\n <kendo-svgicon class=\"user-item-icon\" [icon]=\"item.svgIcon\"></kendo-svgicon>\n }\n <span class=\"user-item-text\">{{ item.text }}</span>\n <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span>\n </a>\n } @else {\n <button type=\"button\" class=\"user-item\" [attr.data-user-item]=\"item.id\" (click)=\"selectItem(item)\">\n @if (item.svgIcon) {\n <kendo-svgicon class=\"user-item-icon\" [icon]=\"item.svgIcon\"></kendo-svgicon>\n }\n <span class=\"user-item-text\">{{ item.text }}</span>\n </button>\n }\n </li>\n }\n </ul>\n }\n <div class=\"user-items-slot\" (click)=\"onProjectedClick($event)\">\n <ng-content select=\"[shellUserMenuItems]\"></ng-content>\n </div>\n\n <div class=\"user-actions\">\n @if (profileUri(); as uri) {\n <a class=\"action-link\" [href]=\"uri\" target=\"_blank\" rel=\"noopener noreferrer\">{{ m().manageProfile }} <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\">{{ m().opensInNewTab }}</span></a>\n }\n <button type=\"button\" class=\"action-button\" (click)=\"logout()\">{{ m().signOut }}</button>\n </div>\n\n @if (versionText(); as text) {\n <div class=\"user-version\">{{ text }}</div>\n }\n </div>\n }\n}\n", styles: [":host{position:relative;display:inline-flex;align-items:center}.avatar{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;border:1px solid var(--theme-border-default);background:var(--theme-bg-overlay);color:var(--theme-text-secondary);font-family:var(--theme-font-display);font-size:.6875rem;font-weight:600}.avatar.large{width:40px;height:40px;font-size:.875rem}.avatar.placeholder{opacity:.5}.avatar-button,.sign-in{padding:2px;border:0;border-radius:var(--theme-radius-sm);background:none;color:inherit;font:inherit;cursor:pointer}.avatar-button:hover .avatar,.sign-in:hover .avatar{border-color:var(--theme-border-strong);color:var(--theme-text-primary)}.avatar-button:focus-visible,.sign-in:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.sign-in{padding:var(--theme-space-1) var(--theme-space-3);background:var(--theme-accent);color:var(--theme-text-on-accent);font-weight:500}.user-panel{position:absolute;top:calc(100% + var(--theme-space-2));right:0;z-index:30;display:grid;gap:var(--theme-space-3);width:280px;padding:var(--theme-space-4);background:var(--theme-bg-overlay);border-radius:var(--theme-radius-lg);box-shadow:var(--theme-shadow-popup);color:var(--theme-text-primary)}.user-identity{display:flex;align-items:center;gap:var(--theme-space-3)}.user-names{display:grid;min-width:0}.user-full-name{font-weight:500}.user-login{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--theme-text-secondary);font-family:var(--theme-font-mono);font-size:.75rem}.pref{display:grid;gap:var(--theme-space-1);min-width:0;margin:0;padding:var(--theme-space-3) 0 0;border:0;border-top:1px solid var(--theme-border-subtle)}.pref-label{padding:0;color:var(--theme-text-secondary);font-size:.75rem}.pref-value{color:var(--theme-text-primary)}.pref-radio{position:absolute;inset:0;z-index:1;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}.segmented{display:inline-flex;padding:2px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm)}.segment{flex:1;position:relative}.segment span{display:block;padding:2px var(--theme-space-2);border-radius:var(--theme-radius-xs);color:var(--theme-text-secondary);font-weight:500;text-align:center;cursor:pointer}.segment input:checked+span{background:var(--theme-bg-elevated);color:var(--theme-text-primary);box-shadow:0 0 0 1px var(--theme-border-default)}.segment input:focus-visible+span{box-shadow:var(--theme-focus-ring)}.option-list{display:grid}.option{position:relative}.option span{display:flex;align-items:center;gap:var(--theme-space-2);padding:var(--theme-space-1) var(--theme-space-2);border-radius:var(--theme-radius-sm);color:var(--theme-text-secondary);cursor:pointer}.option span:before{content:\"\";width:6px;height:6px;border-radius:50%;border:1px solid currentColor}.option:hover span{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.option input:checked+span{color:var(--theme-text-accent)}.option input:checked+span:before{background:currentColor}.option input:focus-visible+span{box-shadow:var(--theme-focus-ring)}.user-items{display:flex;flex-direction:column;gap:2px;margin:0;padding:var(--theme-space-2) 0 0;list-style:none;border-top:1px solid var(--theme-border-subtle)}.user-item{display:flex;align-items:center;gap:var(--theme-space-2);width:100%;box-sizing:border-box;padding:var(--theme-space-1) var(--theme-space-2);border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;text-align:left;text-decoration:none;cursor:pointer}.user-item:hover{background:var(--theme-bg-hover)}.user-item:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.user-item-icon{width:16px;height:16px;color:var(--theme-text-secondary)}.user-item-text{flex:1}.user-items-slot:empty{display:none}.user-actions{display:flex;align-items:center;justify-content:space-between;gap:var(--theme-space-2);padding-top:var(--theme-space-3);border-top:1px solid var(--theme-border-subtle)}.action-link{border-radius:var(--theme-radius-xs);color:var(--theme-text-accent);text-decoration:none}.action-link:hover{text-decoration:underline}.action-link:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.action-button{margin-left:auto;padding:var(--theme-space-1) var(--theme-space-3);border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-surface);color:var(--theme-text-primary);font:inherit;cursor:pointer}.action-button:hover{border-color:var(--theme-border-strong)}.action-button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.user-version{color:var(--theme-text-muted);font-family:var(--theme-font-mono);font-size:.6875rem}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] }]
1535
+ }], propDecorators: { user: [{ type: i0.Input, args: [{ isSignal: true, alias: "user", required: false }] }], sessionLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "sessionLoading", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], densityOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "densityOptions", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], version: [{ type: i0.Input, args: [{ isSignal: true, alias: "version", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], signIn: [{ type: i0.Output, args: ["signIn"] }], signOut: [{ type: i0.Output, args: ["signOut"] }], themeChange: [{ type: i0.Output, args: ["themeChange"] }], densityChange: [{ type: i0.Output, args: ["densityChange"] }], languageChange: [{ type: i0.Output, args: ["languageChange"] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }], onEscape: [{
1536
+ type: HostListener,
1537
+ args: ['document:keydown.escape']
1538
+ }], onDocumentClick: [{
1539
+ type: HostListener,
1540
+ args: ['document:click', ['$event.target']]
1541
+ }] } });
1542
+
1543
+ /**
1544
+ * Top bar (concept §3.1, wireframe): brand mark + name, tenant switcher with the
1545
+ * environment chip, the mode switch, the search trigger, the optional ✦ assistant
1546
+ * toggle and the avatar menu (`mm-shell-user-menu`) with the personal settings.
1547
+ *
1548
+ * Pure presentation; the host feeds everything in and handles the outputs:
1549
+ *
1550
+ * - **Brand**: `brandName` and `brandMarkSrc` (an image URL of the host; none: no mark).
1551
+ * - **Tenant**: inside a tenant (`tenantId`) the host projects its tenant switcher with the
1552
+ * `shellTenant` attribute (e.g. octo-ui's `<mm-tenant-switcher shellTenant …>`, which the
1553
+ * bar styles flat); the environment chip follows it.
1554
+ * - **Environment**: `environment` (e.g. from {@link shellEnvironmentChip}); null hides the
1555
+ * chip (also while the tenant mode is loading). `production: true` tints the bar.
1556
+ * - **Mode switch**: `modes` + `currentMode`; shown with two or more modes inside a tenant
1557
+ * the user may open. Picking the other mode emits `modeChange`; the host applies it.
1558
+ * - **Search**: the button calls {@link ShellSearchTriggerService.request} and emits
1559
+ * `searchRequested`; the host (or the palette) opens the command palette.
1560
+ * - **Assistant**: `assistantAvailable` shows the ✦ toggle (inside a tenant the user may
1561
+ * open); a click emits `assistantToggle`, `assistantOpen` reflects the panel state.
1562
+ * - **User menu**: `user`, `sessionLoading`, `theme`, `densityOptions`, `density`,
1563
+ * `languages`, `language`, `version` and the outputs `signIn`, `signOut`, `themeChange`,
1564
+ * `densityChange` and `languageChange` are passed through to `mm-shell-user-menu`, as are the
1565
+ * host entries (AB#5621): `userMenuItems` → `items`, `itemSelected` → `userMenuItemSelected`,
1566
+ * and content with the `shellUserMenuItems` attribute, projected into the panel.
1567
+ */
1568
+ class ShellTopBarComponent {
1569
+ searchTrigger = inject(ShellSearchTriggerService);
1570
+ tenantId = input(null, /* @ts-ignore */
1571
+ ...(ngDevMode ? [{ debugName: "tenantId" }] : /* istanbul ignore next */ []));
1572
+ isDenied = input(false, /* @ts-ignore */
1573
+ ...(ngDevMode ? [{ debugName: "isDenied" }] : /* istanbul ignore next */ []));
1574
+ /** Product name next to the mark, e.g. "OctoMesh Studio". */
1575
+ brandName = input('OctoMesh', /* @ts-ignore */
1576
+ ...(ngDevMode ? [{ debugName: "brandName" }] : /* istanbul ignore next */ []));
1577
+ /** URL of the brand mark image (decorative); null leaves the mark out. */
1578
+ brandMarkSrc = input(null, /* @ts-ignore */
1579
+ ...(ngDevMode ? [{ debugName: "brandMarkSrc" }] : /* istanbul ignore next */ []));
1580
+ /** The tenant's environment chip; null hides it. */
1581
+ environment = input(null, /* @ts-ignore */
1582
+ ...(ngDevMode ? [{ debugName: "environment" }] : /* istanbul ignore next */ []));
1583
+ /** Choices of the mode switch; fewer than two hide it. */
1584
+ modes = input([], /* @ts-ignore */
1585
+ ...(ngDevMode ? [{ debugName: "modes" }] : /* istanbul ignore next */ []));
1586
+ /** Id of the mode in effect. */
1587
+ currentMode = input(null, /* @ts-ignore */
1588
+ ...(ngDevMode ? [{ debugName: "currentMode" }] : /* istanbul ignore next */ []));
1589
+ /** Shows the ✦ assistant toggle (feature flag on and the panel can be hosted). */
1590
+ assistantAvailable = input(false, /* @ts-ignore */
1591
+ ...(ngDevMode ? [{ debugName: "assistantAvailable" }] : /* istanbul ignore next */ []));
1592
+ /** The assistant panel is open (`aria-expanded` of the toggle). */
1593
+ assistantOpen = input(false, /* @ts-ignore */
1594
+ ...(ngDevMode ? [{ debugName: "assistantOpen" }] : /* istanbul ignore next */ []));
1595
+ /** Id of the assistant panel element (`aria-controls` while open). */
1596
+ assistantPanelId = input('assistant-panel', /* @ts-ignore */
1597
+ ...(ngDevMode ? [{ debugName: "assistantPanelId" }] : /* istanbul ignore next */ []));
1598
+ // --- passed through to mm-shell-user-menu ---
1599
+ user = input(null, /* @ts-ignore */
1600
+ ...(ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []));
1601
+ sessionLoading = input(false, /* @ts-ignore */
1602
+ ...(ngDevMode ? [{ debugName: "sessionLoading" }] : /* istanbul ignore next */ []));
1603
+ theme = input('system', /* @ts-ignore */
1604
+ ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
1605
+ densityOptions = input([], /* @ts-ignore */
1606
+ ...(ngDevMode ? [{ debugName: "densityOptions" }] : /* istanbul ignore next */ []));
1607
+ density = input(null, /* @ts-ignore */
1608
+ ...(ngDevMode ? [{ debugName: "density" }] : /* istanbul ignore next */ []));
1609
+ languages = input(DEFAULT_SHELL_LANGUAGES, /* @ts-ignore */
1610
+ ...(ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []));
1611
+ language = input(null, /* @ts-ignore */
1612
+ ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
1613
+ version = input(null, /* @ts-ignore */
1614
+ ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ []));
1615
+ /** Host entries of the user menu (its `items`). */
1616
+ userMenuItems = input([], /* @ts-ignore */
1617
+ ...(ngDevMode ? [{ debugName: "userMenuItems" }] : /* istanbul ignore next */ []));
1618
+ /** Translations; members left out fall back to {@link SHELL_MESSAGES}, then English. */
1619
+ messages = input(null, /* @ts-ignore */
1620
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1621
+ /** The person picked the other mode; the host applies it and rebuilds the navigation. */
1622
+ modeChange = output();
1623
+ /** The search trigger was used. */
1624
+ searchRequested = output();
1625
+ /** The ✦ toggle was used. */
1626
+ assistantToggle = output();
1627
+ signIn = output();
1628
+ signOut = output();
1629
+ themeChange = output();
1630
+ densityChange = output();
1631
+ languageChange = output();
1632
+ /** A host entry of the user menu was chosen (its `itemSelected`). */
1633
+ userMenuItemSelected = output();
1634
+ m = shellMessages(this.messages);
1635
+ isProduction = computed(() => this.environment()?.production === true, /* @ts-ignore */
1636
+ ...(ngDevMode ? [{ debugName: "isProduction" }] : /* istanbul ignore next */ []));
1637
+ showEnvironment = computed(() => !!this.environment() && !this.isDenied(), /* @ts-ignore */
1638
+ ...(ngDevMode ? [{ debugName: "showEnvironment" }] : /* istanbul ignore next */ []));
1639
+ /** A switch with one choice would be noise (e.g. users without builder roles). */
1640
+ showModeSwitch = computed(() => !!this.tenantId() && !this.isDenied() && this.modes().length > 1, /* @ts-ignore */
1641
+ ...(ngDevMode ? [{ debugName: "showModeSwitch" }] : /* istanbul ignore next */ []));
1642
+ /** The ✦ toggle: only when available and inside a tenant the user may open. */
1643
+ showAssistant = computed(() => this.assistantAvailable() && !!this.tenantId() && !this.isDenied(), /* @ts-ignore */
1644
+ ...(ngDevMode ? [{ debugName: "showAssistant" }] : /* istanbul ignore next */ []));
1645
+ /** ⌘K on Apple platforms, Ctrl K elsewhere — the shortcut the palette binds. */
1646
+ isApple = /Mac|iPhone|iPad/i.test(navigator.platform ?? '');
1647
+ shortcutLabel = this.isApple ? '⌘K' : 'Ctrl K';
1648
+ /** ⌘J / Ctrl J — the assistant shortcut the host binds. */
1649
+ assistantShortcutLabel = this.isApple ? '⌘J' : 'Ctrl J';
1650
+ /** Both chords work everywhere; the platform's usual one is listed first. */
1651
+ assistantKeyShortcut = this.isApple ? 'Meta+J Control+J' : 'Control+J Meta+J';
1652
+ selectMode(mode) {
1653
+ if (mode !== this.currentMode()) {
1654
+ this.modeChange.emit(mode);
1655
+ }
1656
+ }
1657
+ toggleAssistant() {
1658
+ this.assistantToggle.emit();
1659
+ }
1660
+ requestSearch() {
1661
+ this.searchTrigger.request();
1662
+ this.searchRequested.emit();
1663
+ }
1664
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellTopBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1665
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: ShellTopBarComponent, isStandalone: true, selector: "mm-shell-top-bar", inputs: { tenantId: { classPropertyName: "tenantId", publicName: "tenantId", isSignal: true, isRequired: false, transformFunction: null }, isDenied: { classPropertyName: "isDenied", publicName: "isDenied", isSignal: true, isRequired: false, transformFunction: null }, brandName: { classPropertyName: "brandName", publicName: "brandName", isSignal: true, isRequired: false, transformFunction: null }, brandMarkSrc: { classPropertyName: "brandMarkSrc", publicName: "brandMarkSrc", isSignal: true, isRequired: false, transformFunction: null }, environment: { classPropertyName: "environment", publicName: "environment", isSignal: true, isRequired: false, transformFunction: null }, modes: { classPropertyName: "modes", publicName: "modes", isSignal: true, isRequired: false, transformFunction: null }, currentMode: { classPropertyName: "currentMode", publicName: "currentMode", isSignal: true, isRequired: false, transformFunction: null }, assistantAvailable: { classPropertyName: "assistantAvailable", publicName: "assistantAvailable", isSignal: true, isRequired: false, transformFunction: null }, assistantOpen: { classPropertyName: "assistantOpen", publicName: "assistantOpen", isSignal: true, isRequired: false, transformFunction: null }, assistantPanelId: { classPropertyName: "assistantPanelId", publicName: "assistantPanelId", isSignal: true, isRequired: false, transformFunction: null }, user: { classPropertyName: "user", publicName: "user", isSignal: true, isRequired: false, transformFunction: null }, sessionLoading: { classPropertyName: "sessionLoading", publicName: "sessionLoading", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, densityOptions: { classPropertyName: "densityOptions", publicName: "densityOptions", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, version: { classPropertyName: "version", publicName: "version", isSignal: true, isRequired: false, transformFunction: null }, userMenuItems: { classPropertyName: "userMenuItems", publicName: "userMenuItems", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { modeChange: "modeChange", searchRequested: "searchRequested", assistantToggle: "assistantToggle", signIn: "signIn", signOut: "signOut", themeChange: "themeChange", densityChange: "densityChange", languageChange: "languageChange", userMenuItemSelected: "userMenuItemSelected" }, ngImport: i0, template: "<header class=\"top-bar\" [class.production]=\"isProduction()\">\n <div class=\"brand\">\n @if (brandMarkSrc(); as markSrc) {\n <img [src]=\"markSrc\" class=\"brand-mark\" alt=\"\" aria-hidden=\"true\" />\n }\n <span class=\"brand-name\">{{ brandName() }}</span>\n </div>\n\n @if (tenantId()) {\n <span class=\"separator\" aria-hidden=\"true\"></span>\n <div class=\"tenant\">\n <ng-content select=\"[shellTenant]\"></ng-content>\n @if (showEnvironment() && environment(); as env) {\n <span class=\"env-chip\" [class]=\"'env-chip status-' + env.status\" [title]=\"env.label\">\n <span class=\"env-label\">{{ env.label }}</span>\n <span class=\"env-short\" aria-hidden=\"true\">{{ env.short }}</span>\n </span>\n }\n </div>\n }\n\n @if (showModeSwitch()) {\n <div class=\"mode-switch\" role=\"group\" [attr.aria-label]=\"m().modeSwitch\">\n @for (option of modes(); track option.id) {\n <button type=\"button\"\n [attr.aria-pressed]=\"currentMode() === option.id\"\n [title]=\"option.description ?? ''\"\n (click)=\"selectMode(option.id)\">{{ option.label }}</button>\n }\n </div>\n }\n\n <button type=\"button\" class=\"search\" [attr.aria-label]=\"m().searchAriaLabel\" (click)=\"requestSearch()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"7\" /><path d=\"m20 20-3.5-3.5\" /></svg>\n <span class=\"search-label\">{{ m().search }}</span>\n <kbd class=\"search-shortcut\">{{ shortcutLabel }}</kbd>\n </button>\n\n <div class=\"top-actions\">\n @if (showAssistant()) {\n <button type=\"button\" class=\"ai-btn\" data-assistant-toggle\n [attr.aria-expanded]=\"assistantOpen()\"\n [attr.aria-controls]=\"assistantOpen() ? assistantPanelId() : null\"\n [attr.aria-keyshortcuts]=\"assistantKeyShortcut\"\n (click)=\"toggleAssistant()\">\n <span class=\"ai-sparkle\" aria-hidden=\"true\">\u2726</span>\n <span class=\"ai-label\">{{ m().assistant }}</span>\n <kbd class=\"ai-shortcut\" aria-hidden=\"true\">{{ assistantShortcutLabel }}</kbd>\n </button>\n }\n <mm-shell-user-menu\n [user]=\"user()\"\n [sessionLoading]=\"sessionLoading()\"\n [theme]=\"theme()\"\n [densityOptions]=\"densityOptions()\"\n [density]=\"density()\"\n [languages]=\"languages()\"\n [language]=\"language()\"\n [version]=\"version()\"\n [items]=\"userMenuItems()\"\n [messages]=\"messages()\"\n (signIn)=\"signIn.emit()\"\n (signOut)=\"signOut.emit()\"\n (themeChange)=\"themeChange.emit($event)\"\n (densityChange)=\"densityChange.emit($event)\"\n (languageChange)=\"languageChange.emit($event)\"\n (itemSelected)=\"userMenuItemSelected.emit($event)\">\n <ng-container ngProjectAs=\"[shellUserMenuItems]\">\n <ng-content select=\"[shellUserMenuItems]\"></ng-content>\n </ng-container>\n </mm-shell-user-menu>\n </div>\n</header>\n", styles: [":host{display:block}.top-bar{position:relative;display:flex;align-items:center;gap:var(--theme-space-3);height:48px;min-width:0;padding-inline:var(--theme-space-3);background:var(--theme-bg-app);border-bottom:1px solid var(--theme-border-subtle);color:var(--theme-text-primary)}.top-bar.production{border-bottom:2px solid var(--theme-status-error)}.brand{display:flex;align-items:center;gap:var(--theme-space-2);flex:none;white-space:nowrap}.brand-mark{width:22px;height:22px}.brand-name{font-family:var(--theme-font-display);font-size:.875rem;line-height:1.25rem;font-weight:600}.separator{flex:none;width:1px;height:20px;background:var(--theme-border-default)}.tenant{display:flex;align-items:center;gap:var(--theme-space-2);flex:0 1 auto;min-width:0}.tenant ::ng-deep mm-tenant-switcher{--mm-tenant-switcher-bg: transparent;--mm-tenant-switcher-bg-hover: var(--theme-bg-hover);--mm-tenant-switcher-border: var(--theme-border-default);--mm-tenant-switcher-radius: var(--theme-radius-sm);--mm-tenant-switcher-shadow: none;--mm-tenant-switcher-shadow-hover: none;--mm-tenant-switcher-accent: var(--theme-text-secondary);--mm-tenant-switcher-font: var(--theme-font-mono);--mm-tenant-switcher-text-shadow: none;--mm-tenant-switcher-icon-animation: none;--mm-tenant-switcher-denied-bg: var(--theme-status-error-subtle);--mm-tenant-switcher-denied-border: var(--theme-status-error);--mm-tenant-switcher-denied-shadow: none;--mm-tenant-switcher-denied-accent: var(--theme-status-error);--mm-tenant-switcher-denied-text-shadow: none;--mm-tenant-switcher-denied-label-bg: var(--theme-status-error-subtle);flex:0 1 auto;min-width:0}.env-chip{flex:none;display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 7px;border-radius:var(--theme-radius-xs);font-size:.71875rem;font-weight:500;white-space:nowrap}.env-chip:before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor}.env-chip.status-success{color:var(--theme-status-success);background:var(--theme-status-success-subtle)}.env-chip.status-warning{color:var(--theme-status-warning);background:var(--theme-status-warning-subtle)}.env-chip.status-error{color:var(--theme-status-error);background:var(--theme-status-error-subtle)}.env-chip.status-info{color:var(--theme-status-info);background:var(--theme-status-info-subtle)}.env-chip.status-neutral{color:var(--theme-status-neutral);background:var(--theme-status-neutral-subtle)}.mode-switch{display:inline-flex;flex:none;padding:2px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm)}.mode-switch button{padding:2px 10px;border:0;border-radius:var(--theme-radius-xs);background:none;color:var(--theme-text-secondary);font:inherit;font-weight:500;cursor:pointer}.mode-switch button:hover{color:var(--theme-text-primary)}.mode-switch button[aria-pressed=true]{background:var(--theme-bg-overlay);color:var(--theme-text-primary);box-shadow:0 0 0 1px var(--theme-border-default)}.mode-switch button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.search{display:flex;align-items:center;gap:var(--theme-space-2);flex:1 1 260px;min-width:0;max-width:420px;height:30px;margin-inline:auto;padding-inline:10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-surface);color:var(--theme-text-muted);font:inherit;cursor:pointer}.search:hover{border-color:var(--theme-border-strong)}.search:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.search svg{flex:none;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round}.search-label{flex:1;overflow:hidden;text-align:left;white-space:nowrap;text-overflow:ellipsis}.search-shortcut{padding:0 5px;border:1px solid var(--theme-border-strong);border-bottom-width:2px;border-radius:var(--theme-radius-xs);background:var(--theme-bg-elevated);color:var(--theme-text-primary);font-family:var(--theme-font-mono);font-size:.6875rem;line-height:1rem}.top-actions{display:flex;align-items:center;gap:6px;flex:none}.ai-btn{display:flex;align-items:center;gap:6px;height:30px;padding-inline:10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;cursor:pointer}.ai-btn:hover{border-color:var(--theme-border-strong)}.ai-btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.ai-btn[aria-expanded=true]{border-color:color-mix(in srgb,var(--theme-ai) 55%,transparent);background:var(--theme-ai-subtle)}.ai-sparkle{color:var(--theme-ai)}.ai-shortcut{padding:0 5px;border:1px solid var(--theme-border-strong);border-bottom-width:2px;border-radius:var(--theme-radius-xs);background:var(--theme-bg-elevated);font-family:var(--theme-font-mono);font-size:.6875rem;line-height:1rem}@media(max-width:760px){.brand-name,.separator{display:none}.search{flex:0 0 auto;margin-left:auto;margin-right:0}.search-label,.search-shortcut,.ai-shortcut{display:none}.ai-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}}.env-short{display:none}@media(max-width:480px){.top-bar{gap:var(--theme-space-2);padding-inline:var(--theme-space-2)}mm-tenant-switcher{--mm-tenant-switcher-padding: 4px 8px}.mode-switch button{padding:2px 8px}.env-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.env-short{display:inline}}\n"], dependencies: [{ kind: "component", type: ShellUserMenuComponent, selector: "mm-shell-user-menu", inputs: ["user", "sessionLoading", "theme", "densityOptions", "density", "languages", "language", "version", "items", "messages"], outputs: ["signIn", "signOut", "themeChange", "densityChange", "languageChange", "itemSelected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1666
+ }
1667
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ShellTopBarComponent, decorators: [{
1668
+ type: Component,
1669
+ args: [{ selector: 'mm-shell-top-bar', imports: [ShellUserMenuComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<header class=\"top-bar\" [class.production]=\"isProduction()\">\n <div class=\"brand\">\n @if (brandMarkSrc(); as markSrc) {\n <img [src]=\"markSrc\" class=\"brand-mark\" alt=\"\" aria-hidden=\"true\" />\n }\n <span class=\"brand-name\">{{ brandName() }}</span>\n </div>\n\n @if (tenantId()) {\n <span class=\"separator\" aria-hidden=\"true\"></span>\n <div class=\"tenant\">\n <ng-content select=\"[shellTenant]\"></ng-content>\n @if (showEnvironment() && environment(); as env) {\n <span class=\"env-chip\" [class]=\"'env-chip status-' + env.status\" [title]=\"env.label\">\n <span class=\"env-label\">{{ env.label }}</span>\n <span class=\"env-short\" aria-hidden=\"true\">{{ env.short }}</span>\n </span>\n }\n </div>\n }\n\n @if (showModeSwitch()) {\n <div class=\"mode-switch\" role=\"group\" [attr.aria-label]=\"m().modeSwitch\">\n @for (option of modes(); track option.id) {\n <button type=\"button\"\n [attr.aria-pressed]=\"currentMode() === option.id\"\n [title]=\"option.description ?? ''\"\n (click)=\"selectMode(option.id)\">{{ option.label }}</button>\n }\n </div>\n }\n\n <button type=\"button\" class=\"search\" [attr.aria-label]=\"m().searchAriaLabel\" (click)=\"requestSearch()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"7\" /><path d=\"m20 20-3.5-3.5\" /></svg>\n <span class=\"search-label\">{{ m().search }}</span>\n <kbd class=\"search-shortcut\">{{ shortcutLabel }}</kbd>\n </button>\n\n <div class=\"top-actions\">\n @if (showAssistant()) {\n <button type=\"button\" class=\"ai-btn\" data-assistant-toggle\n [attr.aria-expanded]=\"assistantOpen()\"\n [attr.aria-controls]=\"assistantOpen() ? assistantPanelId() : null\"\n [attr.aria-keyshortcuts]=\"assistantKeyShortcut\"\n (click)=\"toggleAssistant()\">\n <span class=\"ai-sparkle\" aria-hidden=\"true\">\u2726</span>\n <span class=\"ai-label\">{{ m().assistant }}</span>\n <kbd class=\"ai-shortcut\" aria-hidden=\"true\">{{ assistantShortcutLabel }}</kbd>\n </button>\n }\n <mm-shell-user-menu\n [user]=\"user()\"\n [sessionLoading]=\"sessionLoading()\"\n [theme]=\"theme()\"\n [densityOptions]=\"densityOptions()\"\n [density]=\"density()\"\n [languages]=\"languages()\"\n [language]=\"language()\"\n [version]=\"version()\"\n [items]=\"userMenuItems()\"\n [messages]=\"messages()\"\n (signIn)=\"signIn.emit()\"\n (signOut)=\"signOut.emit()\"\n (themeChange)=\"themeChange.emit($event)\"\n (densityChange)=\"densityChange.emit($event)\"\n (languageChange)=\"languageChange.emit($event)\"\n (itemSelected)=\"userMenuItemSelected.emit($event)\">\n <ng-container ngProjectAs=\"[shellUserMenuItems]\">\n <ng-content select=\"[shellUserMenuItems]\"></ng-content>\n </ng-container>\n </mm-shell-user-menu>\n </div>\n</header>\n", styles: [":host{display:block}.top-bar{position:relative;display:flex;align-items:center;gap:var(--theme-space-3);height:48px;min-width:0;padding-inline:var(--theme-space-3);background:var(--theme-bg-app);border-bottom:1px solid var(--theme-border-subtle);color:var(--theme-text-primary)}.top-bar.production{border-bottom:2px solid var(--theme-status-error)}.brand{display:flex;align-items:center;gap:var(--theme-space-2);flex:none;white-space:nowrap}.brand-mark{width:22px;height:22px}.brand-name{font-family:var(--theme-font-display);font-size:.875rem;line-height:1.25rem;font-weight:600}.separator{flex:none;width:1px;height:20px;background:var(--theme-border-default)}.tenant{display:flex;align-items:center;gap:var(--theme-space-2);flex:0 1 auto;min-width:0}.tenant ::ng-deep mm-tenant-switcher{--mm-tenant-switcher-bg: transparent;--mm-tenant-switcher-bg-hover: var(--theme-bg-hover);--mm-tenant-switcher-border: var(--theme-border-default);--mm-tenant-switcher-radius: var(--theme-radius-sm);--mm-tenant-switcher-shadow: none;--mm-tenant-switcher-shadow-hover: none;--mm-tenant-switcher-accent: var(--theme-text-secondary);--mm-tenant-switcher-font: var(--theme-font-mono);--mm-tenant-switcher-text-shadow: none;--mm-tenant-switcher-icon-animation: none;--mm-tenant-switcher-denied-bg: var(--theme-status-error-subtle);--mm-tenant-switcher-denied-border: var(--theme-status-error);--mm-tenant-switcher-denied-shadow: none;--mm-tenant-switcher-denied-accent: var(--theme-status-error);--mm-tenant-switcher-denied-text-shadow: none;--mm-tenant-switcher-denied-label-bg: var(--theme-status-error-subtle);flex:0 1 auto;min-width:0}.env-chip{flex:none;display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 7px;border-radius:var(--theme-radius-xs);font-size:.71875rem;font-weight:500;white-space:nowrap}.env-chip:before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor}.env-chip.status-success{color:var(--theme-status-success);background:var(--theme-status-success-subtle)}.env-chip.status-warning{color:var(--theme-status-warning);background:var(--theme-status-warning-subtle)}.env-chip.status-error{color:var(--theme-status-error);background:var(--theme-status-error-subtle)}.env-chip.status-info{color:var(--theme-status-info);background:var(--theme-status-info-subtle)}.env-chip.status-neutral{color:var(--theme-status-neutral);background:var(--theme-status-neutral-subtle)}.mode-switch{display:inline-flex;flex:none;padding:2px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm)}.mode-switch button{padding:2px 10px;border:0;border-radius:var(--theme-radius-xs);background:none;color:var(--theme-text-secondary);font:inherit;font-weight:500;cursor:pointer}.mode-switch button:hover{color:var(--theme-text-primary)}.mode-switch button[aria-pressed=true]{background:var(--theme-bg-overlay);color:var(--theme-text-primary);box-shadow:0 0 0 1px var(--theme-border-default)}.mode-switch button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.search{display:flex;align-items:center;gap:var(--theme-space-2);flex:1 1 260px;min-width:0;max-width:420px;height:30px;margin-inline:auto;padding-inline:10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-surface);color:var(--theme-text-muted);font:inherit;cursor:pointer}.search:hover{border-color:var(--theme-border-strong)}.search:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.search svg{flex:none;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round}.search-label{flex:1;overflow:hidden;text-align:left;white-space:nowrap;text-overflow:ellipsis}.search-shortcut{padding:0 5px;border:1px solid var(--theme-border-strong);border-bottom-width:2px;border-radius:var(--theme-radius-xs);background:var(--theme-bg-elevated);color:var(--theme-text-primary);font-family:var(--theme-font-mono);font-size:.6875rem;line-height:1rem}.top-actions{display:flex;align-items:center;gap:6px;flex:none}.ai-btn{display:flex;align-items:center;gap:6px;height:30px;padding-inline:10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;cursor:pointer}.ai-btn:hover{border-color:var(--theme-border-strong)}.ai-btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.ai-btn[aria-expanded=true]{border-color:color-mix(in srgb,var(--theme-ai) 55%,transparent);background:var(--theme-ai-subtle)}.ai-sparkle{color:var(--theme-ai)}.ai-shortcut{padding:0 5px;border:1px solid var(--theme-border-strong);border-bottom-width:2px;border-radius:var(--theme-radius-xs);background:var(--theme-bg-elevated);font-family:var(--theme-font-mono);font-size:.6875rem;line-height:1rem}@media(max-width:760px){.brand-name,.separator{display:none}.search{flex:0 0 auto;margin-left:auto;margin-right:0}.search-label,.search-shortcut,.ai-shortcut{display:none}.ai-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}}.env-short{display:none}@media(max-width:480px){.top-bar{gap:var(--theme-space-2);padding-inline:var(--theme-space-2)}mm-tenant-switcher{--mm-tenant-switcher-padding: 4px 8px}.mode-switch button{padding:2px 8px}.env-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.env-short{display:inline}}\n"] }]
1670
+ }], propDecorators: { tenantId: [{ type: i0.Input, args: [{ isSignal: true, alias: "tenantId", required: false }] }], isDenied: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDenied", required: false }] }], brandName: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandName", required: false }] }], brandMarkSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandMarkSrc", required: false }] }], environment: [{ type: i0.Input, args: [{ isSignal: true, alias: "environment", required: false }] }], modes: [{ type: i0.Input, args: [{ isSignal: true, alias: "modes", required: false }] }], currentMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentMode", required: false }] }], assistantAvailable: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantAvailable", required: false }] }], assistantOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantOpen", required: false }] }], assistantPanelId: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantPanelId", required: false }] }], user: [{ type: i0.Input, args: [{ isSignal: true, alias: "user", required: false }] }], sessionLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "sessionLoading", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], densityOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "densityOptions", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], version: [{ type: i0.Input, args: [{ isSignal: true, alias: "version", required: false }] }], userMenuItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "userMenuItems", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], modeChange: [{ type: i0.Output, args: ["modeChange"] }], searchRequested: [{ type: i0.Output, args: ["searchRequested"] }], assistantToggle: [{ type: i0.Output, args: ["assistantToggle"] }], signIn: [{ type: i0.Output, args: ["signIn"] }], signOut: [{ type: i0.Output, args: ["signOut"] }], themeChange: [{ type: i0.Output, args: ["themeChange"] }], densityChange: [{ type: i0.Output, args: ["densityChange"] }], languageChange: [{ type: i0.Output, args: ["languageChange"] }], userMenuItemSelected: [{ type: i0.Output, args: ["userMenuItemSelected"] }] } });
1671
+
1672
+ /*
1673
+ * Public API Surface of @meshmakers/shared-ui/shell (AB#5621)
1674
+ */
1675
+ /**
1676
+ * App shell building blocks of the OctoMesh Refinery Studio, usable by any host app
1677
+ * (independent of OctoMesh: no octo-* or shared-auth imports):
1678
+ * rail, space header with tab strip, settings side navigation, top bar and user menu,
1679
+ * plus the services that turn the host's command tree into areas and tabs.
1680
+ *
1681
+ * Usage (host AppComponent):
1682
+ *
1683
+ * ```ts
1684
+ * import {
1685
+ * ShellNavigationService, SpaceHeaderService, ShellRailComponent, SpaceShellComponent,
1686
+ * ShellTopBarComponent, SettingsSideNavComponent, shellEnvironmentChip
1687
+ * } from '@meshmakers/shared-ui/shell';
1688
+ *
1689
+ * @Component({
1690
+ * imports: [ShellTopBarComponent, ShellRailComponent, SpaceShellComponent, SettingsSideNavComponent, RouterOutlet],
1691
+ * template: `
1692
+ * <mm-shell-top-bar brandName="My App" brandMarkSrc="assets/mark.svg"
1693
+ * [tenantId]="tenantId()" [environment]="environment()"
1694
+ * [user]="user()" [sessionLoading]="auth.sessionLoading()" (signIn)="auth.login()" (signOut)="auth.logout()"
1695
+ * [theme]="themeMode.preference()" (themeChange)="themeMode.setPreference($event)"
1696
+ * [languages]="languages" [language]="language()" (languageChange)="setLanguage($event)"
1697
+ * (searchRequested)="palette.open()">
1698
+ * <mm-tenant-switcher shellTenant [currentTenantId]="tenantId()" (tenantSelected)="openTenant($event)" />
1699
+ * </mm-shell-top-bar>
1700
+ * <mm-shell-rail [areas]="nav.topAreas()" [bottomAreas]="nav.bottomAreas()"
1701
+ * [activeAreaId]="nav.activeArea()?.id ?? null" [badges]="{ 'area-inbox': inboxCount() }"
1702
+ * (areaSelected)="nav.openArea($event)" (itemSelected)="nav.open($event)" />
1703
+ * <mm-space-shell [area]="nav.activeArea()" [activeTabId]="nav.activeTab()?.id ?? null"
1704
+ * [chips]="spaceHeader.chips()" [showTabs]="nav.showTabs()" (tabSelected)="nav.open($event)">
1705
+ * <kendo-breadcrumb spaceCrumbs [items]="crumbs()" />
1706
+ * </mm-space-shell>
1707
+ * <router-outlet />`
1708
+ * })
1709
+ * export class AppComponent {
1710
+ * protected readonly nav = inject(ShellNavigationService);
1711
+ * protected readonly spaceHeader = inject(SpaceHeaderService);
1712
+ * }
1713
+ * ```
1714
+ *
1715
+ * The navigation model is the host's `CommandSettingsService` command tree (shared-services):
1716
+ * top-level items with id `areaId('<space>')` become rail areas, their children
1717
+ * (`tabId('<space>', '<tab>')`) tabs; a top-level separator with id `RAIL_BOTTOM_SEPARATOR_ID`
1718
+ * pins the following areas to the bottom. Route data `{ space, tab, objectDetail, spaceTabs }`
1719
+ * refines the active area/tab.
1720
+ *
1721
+ * Optional providers:
1722
+ * - `SHELL_MESSAGES` — translations (`Partial<ShellMessages>`, English defaults; every component
1723
+ * also has a `messages` input that wins over the token);
1724
+ * - `SHELL_SETTINGS_SPACE` — the space that uses the settings side navigation (default `'settings'`,
1725
+ * `null` for none);
1726
+ * - `NOTIFICATION_DISPLAY_OPTIONS` (shared-ui) with `SHELL_NOTIFICATION_OPTIONS` for the toast stacking.
1727
+ *
1728
+ * User, theme, mode, density, language and environment are inputs with matching outputs;
1729
+ * applying and persisting them (per user) is the host's job.
1730
+ */
1731
+ // --- Messages / i18n ---
1732
+
1733
+ /**
1734
+ * Generated bundle index. Do not edit.
1735
+ */
1736
+
1737
+ export { DEFAULT_SHELL_LANGUAGES, DEFAULT_SHELL_MESSAGES, RAIL_BOTTOM_SEPARATOR_ID, RAIL_FLYOUT_CLOSE_DELAY, RAIL_FLYOUT_OPEN_DELAY, SHELL_BADGE_MAX, SHELL_MESSAGES, SHELL_NOTIFICATION_OPTIONS, SHELL_SETTINGS_SPACE, SettingsSideNavBadgesService, SettingsSideNavComponent, ShellNavigationService, ShellRailComponent, ShellSearchTriggerService, ShellTopBarComponent, ShellUserMenuComponent, SpaceHeaderService, SpaceShellComponent, areaId, crumbPath, firstNameOf, formatShellMessage, greetingPhrase, homeGreeting, homeMeta, isAreaCrumb, resolveShellBadge, resolveShellMessages, shellBadgeText, shellBreadcrumbs, shellEnvironmentChip, spaceOfAreaId, tabId };
1738
+ //# sourceMappingURL=meshmakers-shared-ui-shell.mjs.map