@ardimedia/angular-portal-azure 0.2.346 → 0.3.1

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.
@@ -0,0 +1,1576 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, computed, Injectable, inject, makeEnvironmentProviders, APP_INITIALIZER, input, output, Component, ElementRef, Injector, effect, afterNextRender } from '@angular/core';
3
+ import { DOCUMENT } from '@angular/common';
4
+
5
+ function clearStatusBar() {
6
+ return { text: '', style: 'none' };
7
+ }
8
+ function statusBarInfo(text) {
9
+ return { text, style: 'info' };
10
+ }
11
+ function statusBarError(text) {
12
+ return { text, style: 'error' };
13
+ }
14
+ function statusBarSuccess(text) {
15
+ return { text, style: 'success' };
16
+ }
17
+
18
+ /** Creates a blade definition with sensible defaults */
19
+ function createBlade(path, title, width = 315) {
20
+ return {
21
+ path: path.toLowerCase(),
22
+ title,
23
+ subtitle: '',
24
+ width,
25
+ isInnerHtml: true,
26
+ commands: [],
27
+ statusBar: clearStatusBar(),
28
+ };
29
+ }
30
+
31
+ /** Creates a command with sensible defaults (visible, enabled, no-op action) */
32
+ function createCommand(key, label, action, icon) {
33
+ return { key, label, visible: true, enabled: true, icon, action };
34
+ }
35
+
36
+ /** Creates a data blade definition with sensible defaults */
37
+ function createDataBlade(path, title, width = 315) {
38
+ return {
39
+ ...createBlade(path, title, width),
40
+ item: {},
41
+ items: [],
42
+ lifecycle: {},
43
+ };
44
+ }
45
+ /**
46
+ * Execute a load-item operation with lifecycle hooks and status bar updates.
47
+ * Replaces the BladeData.loadItem() template method from v0.2.346.
48
+ */
49
+ async function executeLoadItem(blade, loadFn) {
50
+ blade.lifecycle.onLoadItem?.();
51
+ blade.statusBar = statusBarInfo('Laden...');
52
+ try {
53
+ const result = await loadFn();
54
+ blade.item = result;
55
+ blade.statusBar = clearStatusBar();
56
+ blade.lifecycle.onLoadedItem?.();
57
+ return result;
58
+ }
59
+ catch (ex) {
60
+ blade.statusBar = statusBarError(ex.message || 'Fehler beim Laden');
61
+ blade.lifecycle.onLoadItemError?.(ex);
62
+ }
63
+ }
64
+ /**
65
+ * Execute a load-items operation with lifecycle hooks and status bar updates.
66
+ */
67
+ async function executeLoadItems(blade, loadFn) {
68
+ blade.lifecycle.onLoadItems?.();
69
+ blade.statusBar = statusBarInfo('Laden...');
70
+ try {
71
+ const result = await loadFn();
72
+ blade.items = result;
73
+ blade.statusBar = clearStatusBar();
74
+ blade.lifecycle.onLoadedItems?.();
75
+ return result;
76
+ }
77
+ catch (ex) {
78
+ blade.statusBar = statusBarError(ex.message || 'Fehler beim Laden');
79
+ blade.lifecycle.onLoadItemsError?.(ex);
80
+ }
81
+ }
82
+ /**
83
+ * Execute a save-item operation with lifecycle hooks and status bar updates.
84
+ */
85
+ async function executeSaveItem(blade, saveFn) {
86
+ if (blade.lifecycle.isFormValid && !blade.lifecycle.isFormValid()) {
87
+ return;
88
+ }
89
+ blade.lifecycle.onSaveItem?.();
90
+ blade.statusBar = statusBarInfo('Speichern...');
91
+ try {
92
+ const result = await saveFn();
93
+ blade.item = result;
94
+ blade.statusBar = statusBarSuccess('Gespeichert');
95
+ blade.lifecycle.onSavedItem?.();
96
+ return result;
97
+ }
98
+ catch (ex) {
99
+ blade.statusBar = statusBarError(ex.message || 'Fehler beim Speichern');
100
+ blade.lifecycle.onSaveItemError?.(ex);
101
+ }
102
+ }
103
+ /**
104
+ * Execute a delete-item operation with lifecycle hooks and status bar updates.
105
+ * Returns true if the blade should be closed after deletion.
106
+ */
107
+ async function executeDeleteItem(blade, deleteFn) {
108
+ blade.lifecycle.onDeleteItem?.();
109
+ blade.statusBar = statusBarInfo('Loeschen...');
110
+ try {
111
+ await deleteFn();
112
+ blade.statusBar = statusBarSuccess('Geloescht');
113
+ const shouldClose = blade.lifecycle.onDeletedItem?.() ?? true;
114
+ return shouldClose;
115
+ }
116
+ catch (ex) {
117
+ blade.statusBar = statusBarError(ex.message || 'Fehler beim Loeschen');
118
+ blade.lifecycle.onDeleteItemError?.(ex);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ function createNavItem(title, bladePath, cssClass) {
124
+ return { title, bladePath, cssClass, isVisible: true };
125
+ }
126
+
127
+ /**
128
+ * Tile size enum matching v0.2.346 TileSizes.
129
+ * The names are used in CSS class names (e.g. 'fxs-tilesize-normal').
130
+ */
131
+ var TileSize;
132
+ (function (TileSize) {
133
+ TileSize["Small"] = "small";
134
+ TileSize["Mini"] = "mini";
135
+ TileSize["Normal"] = "normal";
136
+ TileSize["HeroWide"] = "herowide";
137
+ })(TileSize || (TileSize = {}));
138
+ /** Pixel dimensions for each tile size */
139
+ const TILE_DIMENSIONS = {
140
+ [TileSize.Small]: { width: 90, height: 90 },
141
+ [TileSize.Mini]: { width: 180, height: 90 },
142
+ [TileSize.Normal]: { width: 180, height: 180 },
143
+ [TileSize.HeroWide]: { width: 540, height: 360 },
144
+ };
145
+ function createTile(title, bladePath, size = TileSize.Normal) {
146
+ return { title, bladePath, size };
147
+ }
148
+
149
+ /** Computed full name from first and last name */
150
+ function getUserDisplayName(account) {
151
+ return `${account.firstName || ''} ${account.lastName || ''}`.trim();
152
+ }
153
+
154
+ function createNotificationPanel() {
155
+ return {
156
+ path: '',
157
+ width: 250,
158
+ isVisible: false,
159
+ backgroundColor: '#32383f',
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Lay out tiles in a flowing grid pattern matching v0.2.346's Tiles.addTile() algorithm.
165
+ * Tiles wrap after 540px width (3 normal tiles or 1 hero-wide tile).
166
+ */
167
+ function layoutTiles(tiles) {
168
+ const maxWidth = 720;
169
+ let nextLeft = 0;
170
+ let nextTop = 0;
171
+ let columnHeightMax = 0;
172
+ return tiles.map((tile) => {
173
+ const dim = TILE_DIMENSIONS[tile.size];
174
+ // Wrap to next row if tile exceeds max width
175
+ if (nextLeft + dim.width > maxWidth && nextLeft > 0) {
176
+ nextLeft = 0;
177
+ nextTop += columnHeightMax;
178
+ columnHeightMax = 0;
179
+ }
180
+ const positioned = {
181
+ ...tile,
182
+ left: nextLeft,
183
+ top: nextTop,
184
+ };
185
+ nextLeft += dim.width;
186
+ if (dim.height > columnHeightMax) {
187
+ columnHeightMax = dim.height;
188
+ }
189
+ return positioned;
190
+ });
191
+ }
192
+ function createPanorama(title) {
193
+ return {
194
+ title,
195
+ isVisible: true,
196
+ isTilesLoaded: false,
197
+ showTiles: true,
198
+ hideTileIfOnlyOne: true,
199
+ tiles: [],
200
+ };
201
+ }
202
+
203
+ function createAvatarMenu() {
204
+ return {
205
+ userAccount: { userName: '', firstName: '', lastName: '' },
206
+ isOpen: false,
207
+ items: [],
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Multi-word search filter for grid/list blades.
213
+ * Ported from BladeGrid.onFilter() in v0.2.346.
214
+ *
215
+ * Splits the search string by spaces and checks if ALL words
216
+ * are found in ANY string property of the item.
217
+ */
218
+ function filterItems(items, searchText) {
219
+ if (!searchText || !searchText.trim()) {
220
+ return items;
221
+ }
222
+ const words = searchText.toLowerCase().split(/\s+/).filter(Boolean);
223
+ return items.filter((item) => {
224
+ const allValues = getAllStringValues(item).toLowerCase();
225
+ return words.every((word) => allValues.includes(word));
226
+ });
227
+ }
228
+ /** Concatenate all string property values of an object into one searchable string */
229
+ function getAllStringValues(obj) {
230
+ if (obj === null || obj === undefined)
231
+ return '';
232
+ if (typeof obj === 'string')
233
+ return obj;
234
+ if (typeof obj !== 'object')
235
+ return String(obj);
236
+ return Object.values(obj)
237
+ .filter((v) => typeof v === 'string')
238
+ .join(' ');
239
+ }
240
+
241
+ /**
242
+ * Central state service for the angular-portal-azure library.
243
+ * Replaces v0.2.346's PortalService + Panorama + AreaNotification state.
244
+ *
245
+ * All state is managed via Angular signals for reactive updates.
246
+ */
247
+ class PortalService {
248
+ /** The blade stack — ordered left-to-right */
249
+ blades = signal([], ...(ngDevMode ? [{ debugName: "blades" }] : []));
250
+ /** Panorama (startboard/dashboard) state */
251
+ panorama = signal(createPanorama(''), ...(ngDevMode ? [{ debugName: "panorama" }] : []));
252
+ /** Notification panel state */
253
+ notification = signal(createNotificationPanel(), ...(ngDevMode ? [{ debugName: "notification" }] : []));
254
+ /** Avatar menu state */
255
+ avatarMenu = signal(createAvatarMenu(), ...(ngDevMode ? [{ debugName: "avatarMenu" }] : []));
256
+ /** Shared parameter for passing data between blades */
257
+ parameter = signal({ action: 'none', itemId: 0 }, ...(ngDevMode ? [{ debugName: "parameter" }] : []));
258
+ /** Portal theme identifier */
259
+ theme = signal('azure-blue', ...(ngDevMode ? [{ debugName: "theme" }] : []));
260
+ /** Whether the panorama is visible (true when no blades are open) */
261
+ isPanoramaVisible = computed(() => this.blades().length === 0, ...(ngDevMode ? [{ debugName: "isPanoramaVisible" }] : []));
262
+ /** Number of open blades */
263
+ bladeCount = computed(() => this.blades().length, ...(ngDevMode ? [{ debugName: "bladeCount" }] : []));
264
+ /** Positioned tiles with layout coordinates */
265
+ positionedTiles = computed(() => {
266
+ const pano = this.panorama();
267
+ return pano.tiles;
268
+ }, ...(ngDevMode ? [{ debugName: "positionedTiles" }] : []));
269
+ /**
270
+ * Initialize the portal with configuration.
271
+ * Called by providePortalAzure() during app bootstrap.
272
+ */
273
+ configure(config) {
274
+ this.panorama.update((p) => ({
275
+ ...p,
276
+ title: config.title,
277
+ tiles: config.tiles ? layoutTiles(config.tiles) : [],
278
+ isTilesLoaded: !!config.tiles,
279
+ showTiles: true,
280
+ }));
281
+ if (config.userAccount) {
282
+ this.avatarMenu.update((m) => ({
283
+ ...m,
284
+ userAccount: config.userAccount,
285
+ }));
286
+ }
287
+ if (config.theme) {
288
+ this.theme.set(config.theme);
289
+ }
290
+ }
291
+ /** Update the portal title */
292
+ setTitle(title) {
293
+ this.panorama.update((p) => ({ ...p, title }));
294
+ }
295
+ /** Update the user account */
296
+ setUserAccount(userAccount) {
297
+ this.avatarMenu.update((m) => ({ ...m, userAccount }));
298
+ }
299
+ /** Set tiles on the startboard */
300
+ setTiles(tiles) {
301
+ this.panorama.update((p) => ({
302
+ ...p,
303
+ tiles: layoutTiles(tiles),
304
+ isTilesLoaded: true,
305
+ }));
306
+ }
307
+ /** Set blade parameter for inter-blade communication */
308
+ setParameter(param) {
309
+ this.parameter.update((p) => ({ ...p, ...param }));
310
+ }
311
+ /** Clear the blade parameter back to defaults */
312
+ clearParameter() {
313
+ this.parameter.set({ action: 'none', itemId: 0 });
314
+ }
315
+ // --- Notification panel ---
316
+ /** Show the notification panel */
317
+ showNotification(path, width = 250) {
318
+ this.notification.update((n) => ({
319
+ ...n,
320
+ path,
321
+ width,
322
+ isVisible: true,
323
+ }));
324
+ }
325
+ /** Hide the notification panel */
326
+ hideNotification() {
327
+ this.notification.update((n) => ({
328
+ ...n,
329
+ path: '',
330
+ isVisible: false,
331
+ }));
332
+ }
333
+ // --- Avatar menu ---
334
+ /** Toggle avatar menu open/close */
335
+ toggleAvatarMenu() {
336
+ this.avatarMenu.update((m) => ({ ...m, isOpen: !m.isOpen }));
337
+ }
338
+ /** Close avatar menu */
339
+ closeAvatarMenu() {
340
+ this.avatarMenu.update((m) => ({ ...m, isOpen: false }));
341
+ }
342
+ /** Set avatar menu items */
343
+ setAvatarMenuItems(items) {
344
+ this.avatarMenu.update((m) => ({ ...m, items }));
345
+ }
346
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PortalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
347
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PortalService, providedIn: 'root' });
348
+ }
349
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PortalService, decorators: [{
350
+ type: Injectable,
351
+ args: [{ providedIn: 'root' }]
352
+ }] });
353
+
354
+ /**
355
+ * Blade stack management service.
356
+ * Replaces v0.2.346's AreaBlades class.
357
+ *
358
+ * Manages the blade stack: adding, removing, cascade-closing,
359
+ * and panorama visibility toggling.
360
+ */
361
+ class BladeService {
362
+ portal = inject(PortalService);
363
+ /**
364
+ * Set the first blade (e.g., when opening a top-level item from a tile).
365
+ * Clears all existing blades, hides panorama, and adds the new blade.
366
+ *
367
+ * Ported from AreaBlades.setFirstBlade() in v0.2.346.
368
+ */
369
+ setFirstBlade(path, title = '', width = 315) {
370
+ this.portal.blades.set([]);
371
+ const blade = createBlade(path.toLowerCase(), title, width);
372
+ this.portal.blades.set([blade]);
373
+ return blade;
374
+ }
375
+ /**
376
+ * Add a blade to the stack. If senderPath is provided, all blades
377
+ * after the sender are removed first (cascade close).
378
+ *
379
+ * Ported from AreaBlades.addBlade() in v0.2.346.
380
+ */
381
+ addBlade(path, senderPath = '', title = '', width = 315) {
382
+ if (!path)
383
+ return undefined;
384
+ const normalizedPath = path.toLowerCase();
385
+ const blades = this.portal.blades();
386
+ // Check if blade already exists
387
+ const existing = blades.find((b) => b.path === normalizedPath);
388
+ if (existing) {
389
+ return existing;
390
+ }
391
+ // Cascade close: remove blades after the sender
392
+ if (senderPath) {
393
+ this.clearChild(senderPath);
394
+ }
395
+ const blade = createBlade(normalizedPath, title, width);
396
+ this.portal.blades.update((b) => [...b, blade]);
397
+ return blade;
398
+ }
399
+ /**
400
+ * Open a blade from a navigation event (e.g., tile click, nav item click).
401
+ * Wraps addBlade with AddBladeEventArgs for compatibility.
402
+ */
403
+ openBlade(args, title = '', width = 315) {
404
+ return this.addBlade(args.path, args.pathSender, title, width);
405
+ }
406
+ /**
407
+ * Clear all blades. Shows panorama if appropriate.
408
+ * Ported from AreaBlades.clearAll() in v0.2.346.
409
+ */
410
+ clearAll() {
411
+ this.portal.blades.set([]);
412
+ }
413
+ /**
414
+ * Remove a specific blade and all blades to its right.
415
+ * This is what happens when a blade is closed.
416
+ *
417
+ * Ported from AreaBlades.clearPath() in v0.2.346.
418
+ */
419
+ clearPath(path) {
420
+ const normalizedPath = path.toLowerCase();
421
+ const blades = this.portal.blades();
422
+ const index = blades.findIndex((b) => b.path === normalizedPath);
423
+ if (index >= 0) {
424
+ this.portal.blades.set(blades.slice(0, index));
425
+ }
426
+ else {
427
+ // Check notification area
428
+ const notif = this.portal.notification();
429
+ if (notif.path === normalizedPath) {
430
+ this.portal.hideNotification();
431
+ }
432
+ }
433
+ }
434
+ /**
435
+ * Remove all blades AFTER a given path (keeps the blade itself).
436
+ * Used for cascade close when a blade opens a child.
437
+ *
438
+ * Ported from AreaBlades.clearChild() in v0.2.346.
439
+ */
440
+ clearChild(path) {
441
+ if (!path)
442
+ return;
443
+ const normalizedPath = path.toLowerCase();
444
+ const blades = this.portal.blades();
445
+ const index = blades.findIndex((b) => b.path === normalizedPath);
446
+ if (index >= 0) {
447
+ this.portal.blades.set(blades.slice(0, index + 1));
448
+ }
449
+ }
450
+ /**
451
+ * Remove blades at and beyond a specific 1-based level.
452
+ * Ported from AreaBlades.clearLevel() in v0.2.346.
453
+ */
454
+ clearLevel(level) {
455
+ const adjustedLevel = level <= 0 ? 1 : level;
456
+ const blades = this.portal.blades();
457
+ if (adjustedLevel <= blades.length) {
458
+ this.portal.blades.set(blades.slice(0, adjustedLevel - 1));
459
+ }
460
+ }
461
+ /**
462
+ * Remove only the last (rightmost) blade.
463
+ * Ported from AreaBlades.clearLastLevel() in v0.2.346.
464
+ */
465
+ clearLastLevel() {
466
+ const blades = this.portal.blades();
467
+ if (blades.length > 0) {
468
+ this.portal.blades.set(blades.slice(0, -1));
469
+ }
470
+ }
471
+ /**
472
+ * Close a specific blade by its definition reference.
473
+ * Removes the blade and all blades to its right.
474
+ */
475
+ closeBlade(blade) {
476
+ this.clearPath(blade.path);
477
+ }
478
+ /**
479
+ * Get a blade by path, if it exists in the stack.
480
+ */
481
+ getBlade(path) {
482
+ return this.portal.blades().find((b) => b.path === path.toLowerCase());
483
+ }
484
+ /**
485
+ * Check if a blade with the given path is currently open.
486
+ */
487
+ isBladOpen(path) {
488
+ return this.portal.blades().some((b) => b.path === path.toLowerCase());
489
+ }
490
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
491
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeService, providedIn: 'root' });
492
+ }
493
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeService, decorators: [{
494
+ type: Injectable,
495
+ args: [{ providedIn: 'root' }]
496
+ }] });
497
+
498
+ /**
499
+ * Provide the angular-portal-azure library configuration.
500
+ *
501
+ * Usage in app.config.ts:
502
+ * ```typescript
503
+ * export const appConfig: ApplicationConfig = {
504
+ * providers: [
505
+ * providePortalAzure({
506
+ * title: 'My Portal',
507
+ * tiles: [...],
508
+ * theme: 'azure-blue',
509
+ * }),
510
+ * ],
511
+ * };
512
+ * ```
513
+ *
514
+ * New in v0.3.0 — no equivalent in v0.2.346 (which used PortalShell constructor).
515
+ */
516
+ function providePortalAzure(config) {
517
+ return makeEnvironmentProviders([
518
+ {
519
+ provide: APP_INITIALIZER,
520
+ multi: true,
521
+ useFactory: () => {
522
+ const portalService = inject(PortalService);
523
+ return () => portalService.configure(config);
524
+ },
525
+ },
526
+ ]);
527
+ }
528
+
529
+ /**
530
+ * Individual dashboard tile.
531
+ * Ported from the tile section in home.html (v0.2.346).
532
+ *
533
+ * Usage:
534
+ * ```html
535
+ * <apa-tile [tile]="myTile" (tileClick)="onTileClick($event)" />
536
+ * ```
537
+ */
538
+ class TileComponent {
539
+ tile = input.required(...(ngDevMode ? [{ debugName: "tile" }] : []));
540
+ tileClick = output();
541
+ onClick() {
542
+ this.tileClick.emit(this.tile());
543
+ }
544
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
545
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: TileComponent, isStandalone: true, selector: "apa-tile", inputs: { tile: { classPropertyName: "tile", publicName: "tile", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { tileClick: "tileClick" }, ngImport: i0, template: `
546
+ <section
547
+ class="fxs-tile fx-rightClick fxs-flowlayout-element"
548
+ [class.fxs-tilesize-normal]="tile().size === 'normal'"
549
+ [class.fxs-tilesize-mini]="tile().size === 'mini'"
550
+ [class.fxs-tilesize-herowide]="tile().size === 'herowide'"
551
+ [class.fxs-tilesize-small]="tile().size === 'small'">
552
+ <div class="fxs-part fxs-part-clickable" (click)="onClick()" style="cursor:pointer;">
553
+ <header class="fxs-part-title">
554
+ <h2 class="msportalfx-tooltip-overflow">{{ tile().title }}</h2>
555
+ @if (tile().subtitle) {
556
+ <h3 class="msportalfx-tooltip-overflow">{{ tile().subtitle }}</h3>
557
+ }
558
+ </header>
559
+ <section class="fxs-part-content css-scope-HubsExtension"></section>
560
+ </div>
561
+ <div class="fxs-tile-overlay"></div>
562
+ </section>
563
+ `, isInline: true, styles: [":host{display:contents}\n"] });
564
+ }
565
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TileComponent, decorators: [{
566
+ type: Component,
567
+ args: [{ selector: 'apa-tile', standalone: true, template: `
568
+ <section
569
+ class="fxs-tile fx-rightClick fxs-flowlayout-element"
570
+ [class.fxs-tilesize-normal]="tile().size === 'normal'"
571
+ [class.fxs-tilesize-mini]="tile().size === 'mini'"
572
+ [class.fxs-tilesize-herowide]="tile().size === 'herowide'"
573
+ [class.fxs-tilesize-small]="tile().size === 'small'">
574
+ <div class="fxs-part fxs-part-clickable" (click)="onClick()" style="cursor:pointer;">
575
+ <header class="fxs-part-title">
576
+ <h2 class="msportalfx-tooltip-overflow">{{ tile().title }}</h2>
577
+ @if (tile().subtitle) {
578
+ <h3 class="msportalfx-tooltip-overflow">{{ tile().subtitle }}</h3>
579
+ }
580
+ </header>
581
+ <section class="fxs-part-content css-scope-HubsExtension"></section>
582
+ </div>
583
+ <div class="fxs-tile-overlay"></div>
584
+ </section>
585
+ `, styles: [":host{display:contents}\n"] }]
586
+ }], propDecorators: { tile: [{ type: i0.Input, args: [{ isSignal: true, alias: "tile", required: true }] }], tileClick: [{ type: i0.Output, args: ["tileClick"] }] } });
587
+
588
+ /**
589
+ * Panorama (startboard/dashboard) component.
590
+ * Ported from the fxs-panorama-homearea section in home.html (v0.2.346).
591
+ *
592
+ * Displays tiles on the startboard. When a tile is clicked, it opens
593
+ * the first blade via BladeService.setFirstBlade().
594
+ *
595
+ * Usage:
596
+ * ```html
597
+ * <apa-panorama />
598
+ * ```
599
+ */
600
+ class PanoramaComponent {
601
+ portal = inject(PortalService);
602
+ bladeService = inject(BladeService);
603
+ panorama = this.portal.panorama;
604
+ onTileClick(tile) {
605
+ this.bladeService.setFirstBlade(tile.bladePath, tile.title);
606
+ }
607
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PanoramaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
608
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: PanoramaComponent, isStandalone: true, selector: "apa-panorama", ngImport: i0, template: `
609
+ @if (portal.isPanoramaVisible()) {
610
+ <div class="fxs-panorama-homearea" [class.collapsed]="!panorama().showTiles">
611
+ <div class="fxs-startboard-target fxs-startboard fx-rightClick" [class.collapsed]="!panorama().showTiles">
612
+ @if (panorama().tiles.length === 0 && panorama().isTilesLoaded) {
613
+ <div class="fxs-part fxs-part-clickable" style="background-color:var(--apa-surface-raised); padding:25px; max-width:800px; margin-bottom:15px; height:auto;">
614
+ <header class="fxs-part-title" style="color:var(--apa-text)">
615
+ <h3 class="msportalfx-tooltip-overflow">Keine Applikationen zugeordnet</h3>
616
+ <p class="msportalfx-tooltip-overflow" style="margin:0;padding:0">
617
+ Wenden Sie sich bitte an den Administrator damit die entsprechenden Applikationen zugeordnet werden koennen.
618
+ </p>
619
+ </header>
620
+ </div>
621
+ }
622
+ <div class="fxs-startboard-layout fxs-flowlayout">
623
+ <div class="fxs-flowlayout-childcontainer">
624
+ @for (tile of panorama().tiles; track tile.bladePath) {
625
+ <apa-tile [tile]="tile" (tileClick)="onTileClick($event)" />
626
+ }
627
+ </div>
628
+ </div>
629
+ </div>
630
+ </div>
631
+ }
632
+ `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "component", type: TileComponent, selector: "apa-tile", inputs: ["tile"], outputs: ["tileClick"] }] });
633
+ }
634
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PanoramaComponent, decorators: [{
635
+ type: Component,
636
+ args: [{ selector: 'apa-panorama', standalone: true, imports: [TileComponent], template: `
637
+ @if (portal.isPanoramaVisible()) {
638
+ <div class="fxs-panorama-homearea" [class.collapsed]="!panorama().showTiles">
639
+ <div class="fxs-startboard-target fxs-startboard fx-rightClick" [class.collapsed]="!panorama().showTiles">
640
+ @if (panorama().tiles.length === 0 && panorama().isTilesLoaded) {
641
+ <div class="fxs-part fxs-part-clickable" style="background-color:var(--apa-surface-raised); padding:25px; max-width:800px; margin-bottom:15px; height:auto;">
642
+ <header class="fxs-part-title" style="color:var(--apa-text)">
643
+ <h3 class="msportalfx-tooltip-overflow">Keine Applikationen zugeordnet</h3>
644
+ <p class="msportalfx-tooltip-overflow" style="margin:0;padding:0">
645
+ Wenden Sie sich bitte an den Administrator damit die entsprechenden Applikationen zugeordnet werden koennen.
646
+ </p>
647
+ </header>
648
+ </div>
649
+ }
650
+ <div class="fxs-startboard-layout fxs-flowlayout">
651
+ <div class="fxs-flowlayout-childcontainer">
652
+ @for (tile of panorama().tiles; track tile.bladePath) {
653
+ <apa-tile [tile]="tile" (tileClick)="onTileClick($event)" />
654
+ }
655
+ </div>
656
+ </div>
657
+ </div>
658
+ </div>
659
+ }
660
+ `, styles: [":host{display:contents}\n"] }]
661
+ }] });
662
+
663
+ /**
664
+ * Root portal shell component.
665
+ * Ported from the fxs-portal structure in home.html (v0.2.346).
666
+ *
667
+ * Provides the top bar (with portal title and avatar menu),
668
+ * and the main content area. Child components (panorama, blade-host,
669
+ * notification-panel) are projected via content projection.
670
+ *
671
+ * Usage:
672
+ * ```html
673
+ * <apa-portal-layout>
674
+ * <apa-panorama />
675
+ * <apa-blade-host />
676
+ * </apa-portal-layout>
677
+ * ```
678
+ */
679
+ class PortalLayoutComponent {
680
+ portal = inject(PortalService);
681
+ static STORAGE_KEY = 'apa-dark-mode';
682
+ document = inject(DOCUMENT);
683
+ elementRef = inject(ElementRef);
684
+ injector = inject(Injector);
685
+ isDark = signal(false, ...(ngDevMode ? [{ debugName: "isDark" }] : []));
686
+ constructor() {
687
+ const stored = localStorage.getItem(PortalLayoutComponent.STORAGE_KEY);
688
+ const dark = stored === 'true';
689
+ this.applyTheme(dark);
690
+ // Scroll to the last blade whenever the blade stack changes
691
+ const injector = this.injector;
692
+ effect(() => {
693
+ const blades = this.portal.blades();
694
+ if (blades.length === 0)
695
+ return;
696
+ afterNextRender(() => {
697
+ this.scrollToLastBlade();
698
+ }, { injector });
699
+ });
700
+ }
701
+ toggleDarkMode() {
702
+ const dark = !this.isDark();
703
+ localStorage.setItem(PortalLayoutComponent.STORAGE_KEY, String(dark));
704
+ this.applyTheme(dark);
705
+ }
706
+ applyTheme(dark) {
707
+ this.isDark.set(dark);
708
+ this.document.documentElement.classList.toggle('apa-dark', dark);
709
+ this.document.documentElement.classList.toggle('apa-light', !dark);
710
+ }
711
+ displayName() {
712
+ const account = this.portal.avatarMenu().userAccount;
713
+ return getUserDisplayName(account);
714
+ }
715
+ initials() {
716
+ const a = this.portal.avatarMenu().userAccount;
717
+ const f = (a.firstName || '')[0] || '';
718
+ const l = (a.lastName || '')[0] || '';
719
+ return (f + l).toUpperCase() || '?';
720
+ }
721
+ notificationMargin() {
722
+ const notif = this.portal.notification();
723
+ return notif.isVisible ? notif.width : 0;
724
+ }
725
+ onHomeClick(event) {
726
+ event.preventDefault();
727
+ // Clear all blades to show panorama
728
+ this.portal.blades.set([]);
729
+ }
730
+ scrollToLastBlade() {
731
+ const scrollContainer = this.elementRef.nativeElement.querySelector('.fxs-portal-content');
732
+ if (!scrollContainer)
733
+ return;
734
+ const bladeElements = scrollContainer.querySelectorAll('.azureportalblade');
735
+ if (bladeElements.length === 0)
736
+ return;
737
+ const lastBlade = bladeElements[bladeElements.length - 1];
738
+ const scrollRect = scrollContainer.getBoundingClientRect();
739
+ const bladeRect = lastBlade.getBoundingClientRect();
740
+ // Blade is already fully visible — no scroll needed
741
+ if (bladeRect.left >= scrollRect.left && bladeRect.right <= scrollRect.right) {
742
+ return;
743
+ }
744
+ const currentScroll = scrollContainer.scrollLeft;
745
+ const viewportWidth = scrollContainer.clientWidth;
746
+ let targetScroll;
747
+ if (bladeElements.length >= 2) {
748
+ // Use the previous blade's right edge (= where the new blade starts)
749
+ const prevBlade = bladeElements[bladeElements.length - 2];
750
+ const prevRect = prevBlade.getBoundingClientRect();
751
+ // Distance from viewport's left edge to the previous blade's right edge
752
+ const delta = prevRect.right - scrollRect.left;
753
+ targetScroll = currentScroll + delta;
754
+ }
755
+ else {
756
+ // First blade — scroll to show it
757
+ targetScroll = currentScroll + (bladeRect.left - scrollRect.left);
758
+ }
759
+ // Cap scroll movement to at most one viewport width
760
+ targetScroll = Math.min(targetScroll, currentScroll + viewportWidth);
761
+ scrollContainer.scrollTo({ left: targetScroll, behavior: 'smooth' });
762
+ }
763
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PortalLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
764
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: PortalLayoutComponent, isStandalone: true, selector: "apa-portal-layout", ngImport: i0, template: `
765
+ <div class="fxs-portal fxs-theme-blue">
766
+ <!-- Top bar -->
767
+ <div class="fxs-topbar">
768
+ <div style="padding-left:25px;">
769
+ <a href="#" class="fxs-topbar-home fxs-has-hover" (click)="onHomeClick($event)">
770
+ {{ portal.panorama().title }}
771
+ </a>
772
+ </div>
773
+ <div style="display:flex; align-items:center; gap:12px; padding-right:10px;">
774
+ <button class="apa-darkmode-toggle fxs-has-hover"
775
+ (click)="toggleDarkMode()"
776
+ [attr.aria-label]="isDark() ? 'Switch to light mode' : 'Switch to dark mode'"
777
+ [attr.title]="isDark() ? 'Light mode' : 'Dark mode'">
778
+ <i [class]="isDark() ? 'ti ti-sun' : 'ti ti-moon'"></i>
779
+ </button>
780
+ <div class="fxs-avatarmenu-tenant-container" style="position:relative;">
781
+ <a class="apa-avatar-trigger fxs-has-hover" (click)="portal.toggleAvatarMenu()">
782
+ <span class="apa-avatar-initials">{{ initials() }}</span>
783
+ <span class="apa-avatar-info">
784
+ <span class="apa-avatar-name">{{ displayName() }}</span>
785
+ <span class="apa-avatar-email">{{ portal.avatarMenu().userAccount.userName }}</span>
786
+ </span>
787
+ <i class="ti" [class.ti-chevron-down]="!portal.avatarMenu().isOpen"
788
+ [class.ti-chevron-up]="portal.avatarMenu().isOpen"></i>
789
+ </a>
790
+ @if (portal.avatarMenu().isOpen && portal.avatarMenu().items.length > 0) {
791
+ <div class="apa-avatar-dropdown">
792
+ @for (item of portal.avatarMenu().items; track item.href) {
793
+ <a class="apa-avatar-dropdown-item" [href]="item.href">
794
+ @if (item.icon) {
795
+ <i [class]="item.icon"></i>
796
+ }
797
+ <span>{{ item.label }}</span>
798
+ </a>
799
+ }
800
+ </div>
801
+ }
802
+ </div>
803
+ </div>
804
+ </div>
805
+ <!-- Main content area -->
806
+ <div class="fxs-portal-content fxs-panorama"
807
+ [style.margin-right.px]="notificationMargin()">
808
+ <ng-content />
809
+ </div>
810
+ <!-- Footer -->
811
+ <div class="fxs-portal-footer"></div>
812
+ </div>
813
+ `, isInline: true });
814
+ }
815
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PortalLayoutComponent, decorators: [{
816
+ type: Component,
817
+ args: [{
818
+ selector: 'apa-portal-layout',
819
+ standalone: true,
820
+ template: `
821
+ <div class="fxs-portal fxs-theme-blue">
822
+ <!-- Top bar -->
823
+ <div class="fxs-topbar">
824
+ <div style="padding-left:25px;">
825
+ <a href="#" class="fxs-topbar-home fxs-has-hover" (click)="onHomeClick($event)">
826
+ {{ portal.panorama().title }}
827
+ </a>
828
+ </div>
829
+ <div style="display:flex; align-items:center; gap:12px; padding-right:10px;">
830
+ <button class="apa-darkmode-toggle fxs-has-hover"
831
+ (click)="toggleDarkMode()"
832
+ [attr.aria-label]="isDark() ? 'Switch to light mode' : 'Switch to dark mode'"
833
+ [attr.title]="isDark() ? 'Light mode' : 'Dark mode'">
834
+ <i [class]="isDark() ? 'ti ti-sun' : 'ti ti-moon'"></i>
835
+ </button>
836
+ <div class="fxs-avatarmenu-tenant-container" style="position:relative;">
837
+ <a class="apa-avatar-trigger fxs-has-hover" (click)="portal.toggleAvatarMenu()">
838
+ <span class="apa-avatar-initials">{{ initials() }}</span>
839
+ <span class="apa-avatar-info">
840
+ <span class="apa-avatar-name">{{ displayName() }}</span>
841
+ <span class="apa-avatar-email">{{ portal.avatarMenu().userAccount.userName }}</span>
842
+ </span>
843
+ <i class="ti" [class.ti-chevron-down]="!portal.avatarMenu().isOpen"
844
+ [class.ti-chevron-up]="portal.avatarMenu().isOpen"></i>
845
+ </a>
846
+ @if (portal.avatarMenu().isOpen && portal.avatarMenu().items.length > 0) {
847
+ <div class="apa-avatar-dropdown">
848
+ @for (item of portal.avatarMenu().items; track item.href) {
849
+ <a class="apa-avatar-dropdown-item" [href]="item.href">
850
+ @if (item.icon) {
851
+ <i [class]="item.icon"></i>
852
+ }
853
+ <span>{{ item.label }}</span>
854
+ </a>
855
+ }
856
+ </div>
857
+ }
858
+ </div>
859
+ </div>
860
+ </div>
861
+ <!-- Main content area -->
862
+ <div class="fxs-portal-content fxs-panorama"
863
+ [style.margin-right.px]="notificationMargin()">
864
+ <ng-content />
865
+ </div>
866
+ <!-- Footer -->
867
+ <div class="fxs-portal-footer"></div>
868
+ </div>
869
+ `,
870
+ }]
871
+ }], ctorParameters: () => [] });
872
+
873
+ /**
874
+ * Command bar for a blade.
875
+ * Ported from the fxs-commandBar section in blade.html (v0.2.346).
876
+ *
877
+ * Replaces the 18 individual command buttons with a dynamic BladeCommand[] array.
878
+ *
879
+ * Usage:
880
+ * ```html
881
+ * <apa-command-bar [commands]="blade.commands" />
882
+ * ```
883
+ */
884
+ class CommandBarComponent {
885
+ commands = input([], ...(ngDevMode ? [{ debugName: "commands" }] : []));
886
+ visibleCommands() {
887
+ return this.commands().filter((c) => c.visible);
888
+ }
889
+ onCommand(cmd) {
890
+ if (cmd.enabled && cmd.action) {
891
+ cmd.action();
892
+ }
893
+ }
894
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CommandBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
895
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: CommandBarComponent, isStandalone: true, selector: "apa-command-bar", inputs: { commands: { classPropertyName: "commands", publicName: "commands", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
896
+ <div class="fxs-commandBar">
897
+ <ul class="fxs-commandBar-itemList">
898
+ @for (cmd of visibleCommands(); track cmd.key) {
899
+ <li>
900
+ <a class="fxs-commandBar-item"
901
+ [class.apa-disable-click]="!cmd.enabled"
902
+ (click)="onCommand(cmd)">
903
+ <span class="fxs-commandBar-item-text">{{ cmd.label }}</span>
904
+ @if (cmd.icon) {
905
+ <span class="fxs-commandBar-item-icon apa-commandbar-icon">
906
+ <span [class]="cmd.icon"></span>
907
+ </span>
908
+ }
909
+ </a>
910
+ </li>
911
+ }
912
+ </ul>
913
+ </div>
914
+ `, isInline: true, styles: [":host{display:block}\n"] });
915
+ }
916
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CommandBarComponent, decorators: [{
917
+ type: Component,
918
+ args: [{ selector: 'apa-command-bar', standalone: true, template: `
919
+ <div class="fxs-commandBar">
920
+ <ul class="fxs-commandBar-itemList">
921
+ @for (cmd of visibleCommands(); track cmd.key) {
922
+ <li>
923
+ <a class="fxs-commandBar-item"
924
+ [class.apa-disable-click]="!cmd.enabled"
925
+ (click)="onCommand(cmd)">
926
+ <span class="fxs-commandBar-item-text">{{ cmd.label }}</span>
927
+ @if (cmd.icon) {
928
+ <span class="fxs-commandBar-item-icon apa-commandbar-icon">
929
+ <span [class]="cmd.icon"></span>
930
+ </span>
931
+ }
932
+ </a>
933
+ </li>
934
+ }
935
+ </ul>
936
+ </div>
937
+ `, styles: [":host{display:block}\n"] }]
938
+ }], propDecorators: { commands: [{ type: i0.Input, args: [{ isSignal: true, alias: "commands", required: false }] }] } });
939
+
940
+ /**
941
+ * Blade chrome component — the visual container for a single blade.
942
+ * Ported from blade.html (v0.2.346).
943
+ *
944
+ * Renders the blade header (status bar, close/maximize buttons, title,
945
+ * command bar) and projects blade content via <ng-content>.
946
+ *
947
+ * Usage:
948
+ * ```html
949
+ * <apa-blade [blade]="myBlade">
950
+ * <p>Blade content here</p>
951
+ * </apa-blade>
952
+ * ```
953
+ */
954
+ class BladeComponent {
955
+ blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : []));
956
+ bladeClose = output();
957
+ bladeService = inject(BladeService);
958
+ onClose() {
959
+ const b = this.blade();
960
+ this.bladeClose.emit(b);
961
+ this.bladeService.closeBlade(b);
962
+ }
963
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
964
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BladeComponent, isStandalone: true, selector: "apa-blade", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { bladeClose: "bladeClose" }, ngImport: i0, template: `
965
+ <section class="fxs-blade-locked fxs-blade fx-rightClick fxs-bladesize-small"
966
+ [style.width.px]="blade().width">
967
+ <!-- Header -->
968
+ <header class="fxs-blade-header">
969
+ <!-- Status bar -->
970
+ <div class="fxs-blade-statusbar-wrapper">
971
+ <div class="fxs-blade-statusbar"
972
+ [class.apa-statusbar-info]="blade().statusBar.style === 'info'"
973
+ [class.apa-statusbar-error]="blade().statusBar.style === 'error' || blade().statusBar.style === 'warning'"
974
+ [class.apa-statusbar-success]="blade().statusBar.style === 'success'">
975
+ {{ blade().statusBar.text }}
976
+ </div>
977
+ </div>
978
+
979
+ <!-- Action buttons -->
980
+ <div class="fxs-blade-actions">
981
+ <button (click)="onClose()" title="Schliessen">
982
+ <svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
983
+ <g>
984
+ <polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
985
+ </g>
986
+ </svg>
987
+ </button>
988
+ </div>
989
+
990
+ <!-- Title -->
991
+ <div class="fxs-blade-title">
992
+ <div class="fxs-blade-title-text-container">
993
+ <h2>{{ blade().title }}</h2>
994
+ @if (blade().subtitle) {
995
+ <h3>{{ blade().subtitle }}</h3>
996
+ }
997
+ </div>
998
+ </div>
999
+
1000
+ <!-- Command bar -->
1001
+ <div class="fxs-blade-commandBarContainer">
1002
+ <apa-command-bar [commands]="blade().commands" />
1003
+ </div>
1004
+ </header>
1005
+
1006
+ <!-- Content area -->
1007
+ <div class="fxs-blade-content fxs-pannable">
1008
+ <div class="fxs-blade-stacklayout">
1009
+ <div class="fxs-stacklayout-child">
1010
+ <ng-content />
1011
+ </div>
1012
+ </div>
1013
+ </div>
1014
+ </section>
1015
+ `, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "component", type: CommandBarComponent, selector: "apa-command-bar", inputs: ["commands"] }] });
1016
+ }
1017
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeComponent, decorators: [{
1018
+ type: Component,
1019
+ args: [{ selector: 'apa-blade', standalone: true, imports: [CommandBarComponent], template: `
1020
+ <section class="fxs-blade-locked fxs-blade fx-rightClick fxs-bladesize-small"
1021
+ [style.width.px]="blade().width">
1022
+ <!-- Header -->
1023
+ <header class="fxs-blade-header">
1024
+ <!-- Status bar -->
1025
+ <div class="fxs-blade-statusbar-wrapper">
1026
+ <div class="fxs-blade-statusbar"
1027
+ [class.apa-statusbar-info]="blade().statusBar.style === 'info'"
1028
+ [class.apa-statusbar-error]="blade().statusBar.style === 'error' || blade().statusBar.style === 'warning'"
1029
+ [class.apa-statusbar-success]="blade().statusBar.style === 'success'">
1030
+ {{ blade().statusBar.text }}
1031
+ </div>
1032
+ </div>
1033
+
1034
+ <!-- Action buttons -->
1035
+ <div class="fxs-blade-actions">
1036
+ <button (click)="onClose()" title="Schliessen">
1037
+ <svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
1038
+ <g>
1039
+ <polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
1040
+ </g>
1041
+ </svg>
1042
+ </button>
1043
+ </div>
1044
+
1045
+ <!-- Title -->
1046
+ <div class="fxs-blade-title">
1047
+ <div class="fxs-blade-title-text-container">
1048
+ <h2>{{ blade().title }}</h2>
1049
+ @if (blade().subtitle) {
1050
+ <h3>{{ blade().subtitle }}</h3>
1051
+ }
1052
+ </div>
1053
+ </div>
1054
+
1055
+ <!-- Command bar -->
1056
+ <div class="fxs-blade-commandBarContainer">
1057
+ <apa-command-bar [commands]="blade().commands" />
1058
+ </div>
1059
+ </header>
1060
+
1061
+ <!-- Content area -->
1062
+ <div class="fxs-blade-content fxs-pannable">
1063
+ <div class="fxs-blade-stacklayout">
1064
+ <div class="fxs-stacklayout-child">
1065
+ <ng-content />
1066
+ </div>
1067
+ </div>
1068
+ </div>
1069
+ </section>
1070
+ `, styles: [":host{display:block;height:100%}\n"] }]
1071
+ }], propDecorators: { blade: [{ type: i0.Input, args: [{ isSignal: true, alias: "blade", required: true }] }], bladeClose: [{ type: i0.Output, args: ["bladeClose"] }] } });
1072
+
1073
+ /**
1074
+ * Blade host — renders the blade stack (journey area).
1075
+ * Ported from the apa-blade-area section in home.html (v0.2.346).
1076
+ *
1077
+ * Each blade in the stack is rendered horizontally. When a new blade
1078
+ * is added, the portal layout scrolls to show it.
1079
+ *
1080
+ * Consumer apps provide blade content via the [bladeTemplate] input
1081
+ * or by routing. For now, blades are rendered with their path as content.
1082
+ *
1083
+ * Usage:
1084
+ * ```html
1085
+ * <apa-blade-host />
1086
+ * ```
1087
+ */
1088
+ class BladeHostComponent {
1089
+ portal = inject(PortalService);
1090
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1091
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BladeHostComponent, isStandalone: true, selector: "apa-blade-host", ngImport: i0, template: `
1092
+ <div id="apa-blade-area" class="fxs-journey-target fxs-journey">
1093
+ <div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
1094
+ @for (blade of portal.blades(); track blade.path) {
1095
+ <div class="azureportalblade fxs-stacklayout-child">
1096
+ <apa-blade [blade]="blade">
1097
+ <!-- Default content: blade path (consumers override via content projection or custom templates) -->
1098
+ <p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
1099
+ </apa-blade>
1100
+ </div>
1101
+ }
1102
+ </div>
1103
+ </div>
1104
+ `, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "component", type: BladeComponent, selector: "apa-blade", inputs: ["blade"], outputs: ["bladeClose"] }] });
1105
+ }
1106
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeHostComponent, decorators: [{
1107
+ type: Component,
1108
+ args: [{ selector: 'apa-blade-host', standalone: true, imports: [BladeComponent], template: `
1109
+ <div id="apa-blade-area" class="fxs-journey-target fxs-journey">
1110
+ <div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
1111
+ @for (blade of portal.blades(); track blade.path) {
1112
+ <div class="azureportalblade fxs-stacklayout-child">
1113
+ <apa-blade [blade]="blade">
1114
+ <!-- Default content: blade path (consumers override via content projection or custom templates) -->
1115
+ <p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
1116
+ </apa-blade>
1117
+ </div>
1118
+ }
1119
+ </div>
1120
+ </div>
1121
+ `, styles: [":host{display:block;height:100%}\n"] }]
1122
+ }] });
1123
+
1124
+ /**
1125
+ * Navigation blade content — renders a list of nav items.
1126
+ * Ported from nav.html (v0.2.346).
1127
+ *
1128
+ * Each nav item shows an icon and title. Clicking a row opens
1129
+ * the target blade via BladeService.
1130
+ *
1131
+ * Usage:
1132
+ * ```html
1133
+ * <apa-blade [blade]="navBlade">
1134
+ * <apa-blade-nav [items]="navItems" [senderPath]="navBlade.path" />
1135
+ * </apa-blade>
1136
+ * ```
1137
+ */
1138
+ class BladeNavComponent {
1139
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
1140
+ senderPath = input('', ...(ngDevMode ? [{ debugName: "senderPath" }] : []));
1141
+ bladeService = inject(BladeService);
1142
+ visibleItems() {
1143
+ return this.items().filter((item) => item.isVisible);
1144
+ }
1145
+ onItemClick(item) {
1146
+ if (item.hrefPath) {
1147
+ // External link — let the <a> handle it
1148
+ return;
1149
+ }
1150
+ if (item.bladePath) {
1151
+ this.bladeService.addBlade(item.bladePath, this.senderPath());
1152
+ }
1153
+ item.callback?.();
1154
+ }
1155
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1156
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BladeNavComponent, isStandalone: true, selector: "apa-blade-nav", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1157
+ <table class="azc-grid-full azc-grid-multiselectable">
1158
+ <colgroup>
1159
+ <col class="col0" style="width:28px;">
1160
+ <col class="col1">
1161
+ </colgroup>
1162
+ <tbody class="azc-grid-groupdata" role="rowgroup">
1163
+ @for (item of visibleItems(); track item.bladePath) {
1164
+ <tr role="row" style="cursor:pointer" (click)="onItemClick(item)">
1165
+ <td class="msportalfx-gridcolumn-asseticon" role="gridcell">
1166
+ @if (item.cssClass) {
1167
+ <i [class]="item.cssClass"></i>
1168
+ }
1169
+ </td>
1170
+ <td tabindex="0" role="gridcell">
1171
+ @if (item.hrefPath) {
1172
+ <a [href]="item.hrefPath" target="_blank">{{ item.title }}</a>
1173
+ } @else {
1174
+ <span>{{ item.title }}</span>
1175
+ }
1176
+ </td>
1177
+ </tr>
1178
+ }
1179
+ </tbody>
1180
+ </table>
1181
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1}\n"] });
1182
+ }
1183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeNavComponent, decorators: [{
1184
+ type: Component,
1185
+ args: [{ selector: 'apa-blade-nav', standalone: true, template: `
1186
+ <table class="azc-grid-full azc-grid-multiselectable">
1187
+ <colgroup>
1188
+ <col class="col0" style="width:28px;">
1189
+ <col class="col1">
1190
+ </colgroup>
1191
+ <tbody class="azc-grid-groupdata" role="rowgroup">
1192
+ @for (item of visibleItems(); track item.bladePath) {
1193
+ <tr role="row" style="cursor:pointer" (click)="onItemClick(item)">
1194
+ <td class="msportalfx-gridcolumn-asseticon" role="gridcell">
1195
+ @if (item.cssClass) {
1196
+ <i [class]="item.cssClass"></i>
1197
+ }
1198
+ </td>
1199
+ <td tabindex="0" role="gridcell">
1200
+ @if (item.hrefPath) {
1201
+ <a [href]="item.hrefPath" target="_blank">{{ item.title }}</a>
1202
+ } @else {
1203
+ <span>{{ item.title }}</span>
1204
+ }
1205
+ </td>
1206
+ </tr>
1207
+ }
1208
+ </tbody>
1209
+ </table>
1210
+ `, styles: [":host{display:flex;flex-direction:column;flex:1}\n"] }]
1211
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], senderPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "senderPath", required: false }] }] } });
1212
+
1213
+ /**
1214
+ * Grid/list blade content — renders a searchable list of items.
1215
+ * Ported from grid.html + BladeGrid (v0.2.346).
1216
+ *
1217
+ * Displays items in a table with optional search filtering.
1218
+ * Each row shows a title and optional subtitle. Clicking a row
1219
+ * opens the target blade.
1220
+ *
1221
+ * Usage:
1222
+ * ```html
1223
+ * <apa-blade [blade]="gridBlade">
1224
+ * <apa-blade-grid
1225
+ * [items]="items"
1226
+ * [senderPath]="gridBlade.path"
1227
+ * [displayField]="'title'"
1228
+ * (itemClick)="onItemClick($event)" />
1229
+ * </apa-blade>
1230
+ * ```
1231
+ */
1232
+ class BladeGridComponent {
1233
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
1234
+ senderPath = input('', ...(ngDevMode ? [{ debugName: "senderPath" }] : []));
1235
+ displayField = input('title', ...(ngDevMode ? [{ debugName: "displayField" }] : []));
1236
+ bladePathField = input('bladePath', ...(ngDevMode ? [{ debugName: "bladePathField" }] : []));
1237
+ searchable = input(true, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
1238
+ itemClick = output();
1239
+ searchText = '';
1240
+ bladeService = inject(BladeService);
1241
+ filteredItems() {
1242
+ return filterItems(this.items(), this.searchText);
1243
+ }
1244
+ getDisplayValue(item) {
1245
+ return item[this.displayField()] ?? '';
1246
+ }
1247
+ onSearchInput(event) {
1248
+ this.searchText = event.target.value;
1249
+ }
1250
+ onRowClick(item) {
1251
+ this.itemClick.emit(item);
1252
+ const bladePath = item[this.bladePathField()];
1253
+ if (bladePath) {
1254
+ this.bladeService.addBlade(bladePath, this.senderPath());
1255
+ }
1256
+ }
1257
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1258
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BladeGridComponent, isStandalone: true, selector: "apa-blade-grid", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null }, displayField: { classPropertyName: "displayField", publicName: "displayField", isSignal: true, isRequired: false, transformFunction: null }, bladePathField: { classPropertyName: "bladePathField", publicName: "bladePathField", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, ngImport: i0, template: `
1259
+ @if (searchable()) {
1260
+ <div style="padding:0 0 10px 0;">
1261
+ <input type="text"
1262
+ class="form-control"
1263
+ placeholder="Suchen..."
1264
+ [value]="searchText"
1265
+ (input)="onSearchInput($event)" />
1266
+ </div>
1267
+ }
1268
+ <table class="azc-grid-full azc-grid-multiselectable">
1269
+ <colgroup>
1270
+ <col class="col0" style="width:41px;">
1271
+ <col class="col1">
1272
+ </colgroup>
1273
+ <tbody class="azc-grid-groupdata" role="rowgroup">
1274
+ @for (item of filteredItems(); track $index) {
1275
+ <tr role="row" style="cursor:pointer" (click)="onRowClick(item)">
1276
+ <td class="msportalfx-gridcolumn-asseticon" role="gridcell">
1277
+ <svg xmlns="http://www.w3.org/2000/svg" class="msportal-fx-svg-placeholder" viewBox="0 0 50 50" focusable="false" style="height:21px;width:21px;">
1278
+ <rect class="msportalfx-svg-c04" x="19.8" y="39.4" width="10.6" height="3.4"/>
1279
+ <polygon class="msportalfx-svg-c04" points="23.1,50 27,50 30.3,46.5 19.8,46.5"/>
1280
+ <path class="msportalfx-svg-c20" d="M41.2 14.7v-.3c0-7.7-6.6-14.1-14.7-14.2-.2-.3-4.8.1-4.8.1-7.3.9-13 7-13 14.1 0 .2-.8 5.8 4.9 10.5 2.6 2.3 5.3 8.5 5.7 10.3l.3.6h10.6l.3-.6c.4-1.8 3.2-8 5.7-10.2C41.9 20.2 41.2 14.9 41.2 14.7z"/>
1281
+ </svg>
1282
+ </td>
1283
+ <td tabindex="0" role="gridcell">
1284
+ <span>{{ getDisplayValue(item) }}</span>
1285
+ </td>
1286
+ </tr>
1287
+ }
1288
+ </tbody>
1289
+ </table>
1290
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1}\n"] });
1291
+ }
1292
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeGridComponent, decorators: [{
1293
+ type: Component,
1294
+ args: [{ selector: 'apa-blade-grid', standalone: true, template: `
1295
+ @if (searchable()) {
1296
+ <div style="padding:0 0 10px 0;">
1297
+ <input type="text"
1298
+ class="form-control"
1299
+ placeholder="Suchen..."
1300
+ [value]="searchText"
1301
+ (input)="onSearchInput($event)" />
1302
+ </div>
1303
+ }
1304
+ <table class="azc-grid-full azc-grid-multiselectable">
1305
+ <colgroup>
1306
+ <col class="col0" style="width:41px;">
1307
+ <col class="col1">
1308
+ </colgroup>
1309
+ <tbody class="azc-grid-groupdata" role="rowgroup">
1310
+ @for (item of filteredItems(); track $index) {
1311
+ <tr role="row" style="cursor:pointer" (click)="onRowClick(item)">
1312
+ <td class="msportalfx-gridcolumn-asseticon" role="gridcell">
1313
+ <svg xmlns="http://www.w3.org/2000/svg" class="msportal-fx-svg-placeholder" viewBox="0 0 50 50" focusable="false" style="height:21px;width:21px;">
1314
+ <rect class="msportalfx-svg-c04" x="19.8" y="39.4" width="10.6" height="3.4"/>
1315
+ <polygon class="msportalfx-svg-c04" points="23.1,50 27,50 30.3,46.5 19.8,46.5"/>
1316
+ <path class="msportalfx-svg-c20" d="M41.2 14.7v-.3c0-7.7-6.6-14.1-14.7-14.2-.2-.3-4.8.1-4.8.1-7.3.9-13 7-13 14.1 0 .2-.8 5.8 4.9 10.5 2.6 2.3 5.3 8.5 5.7 10.3l.3.6h10.6l.3-.6c.4-1.8 3.2-8 5.7-10.2C41.9 20.2 41.2 14.9 41.2 14.7z"/>
1317
+ </svg>
1318
+ </td>
1319
+ <td tabindex="0" role="gridcell">
1320
+ <span>{{ getDisplayValue(item) }}</span>
1321
+ </td>
1322
+ </tr>
1323
+ }
1324
+ </tbody>
1325
+ </table>
1326
+ `, styles: [":host{display:flex;flex-direction:column;flex:1}\n"] }]
1327
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], senderPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "senderPath", required: false }] }], displayField: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayField", required: false }] }], bladePathField: [{ type: i0.Input, args: [{ isSignal: true, alias: "bladePathField", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
1328
+
1329
+ /**
1330
+ * Detail/edit blade content — renders a form area for editing an item.
1331
+ * Ported from BladeDetail<T> (v0.2.346).
1332
+ *
1333
+ * Provides a content projection area for forms. The blade's commands
1334
+ * (save, delete, cancel, new) are managed via the blade definition.
1335
+ *
1336
+ * This is a thin wrapper that projects content and provides
1337
+ * convenience methods for setting up typical detail blade commands.
1338
+ *
1339
+ * Usage:
1340
+ * ```html
1341
+ * <apa-blade [blade]="detailBlade">
1342
+ * <apa-blade-detail [blade]="detailBlade">
1343
+ * <form>
1344
+ * <!-- form fields here -->
1345
+ * </form>
1346
+ * </apa-blade-detail>
1347
+ * </apa-blade>
1348
+ * ```
1349
+ */
1350
+ class BladeDetailComponent {
1351
+ blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : []));
1352
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1353
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.5", type: BladeDetailComponent, isStandalone: true, selector: "apa-blade-detail", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
1354
+ <div class="apa-blade-detail">
1355
+ <ng-content />
1356
+ </div>
1357
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1}.apa-blade-detail{flex:1}\n"] });
1358
+ }
1359
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BladeDetailComponent, decorators: [{
1360
+ type: Component,
1361
+ args: [{ selector: 'apa-blade-detail', standalone: true, template: `
1362
+ <div class="apa-blade-detail">
1363
+ <ng-content />
1364
+ </div>
1365
+ `, styles: [":host{display:flex;flex-direction:column;flex:1}.apa-blade-detail{flex:1}\n"] }]
1366
+ }], propDecorators: { blade: [{ type: i0.Input, args: [{ isSignal: true, alias: "blade", required: true }] }] } });
1367
+ /**
1368
+ * Create standard detail blade commands (new, save, delete, cancel).
1369
+ * Convenience function for setting up typical detail/edit blade commands
1370
+ * matching the v0.2.346 BladeDetail default commands.
1371
+ */
1372
+ function createDetailCommands(handlers) {
1373
+ const commands = [];
1374
+ if (handlers.onNew) {
1375
+ commands.push(createCommand('new', 'neu', handlers.onNew));
1376
+ }
1377
+ if (handlers.onSave) {
1378
+ commands.push(createCommand('save', 'speichern', handlers.onSave));
1379
+ }
1380
+ if (handlers.onDelete) {
1381
+ commands.push(createCommand('delete', 'loeschen', handlers.onDelete));
1382
+ }
1383
+ if (handlers.onCancel) {
1384
+ commands.push(createCommand('cancel', 'abbrechen', handlers.onCancel));
1385
+ }
1386
+ return commands;
1387
+ }
1388
+
1389
+ /**
1390
+ * Notification panel — right-side overlay panel.
1391
+ * Ported from apa-notification-area in home.html (v0.2.346).
1392
+ *
1393
+ * Usage:
1394
+ * ```html
1395
+ * <apa-notification-panel>
1396
+ * <!-- Custom notification content -->
1397
+ * </apa-notification-panel>
1398
+ * ```
1399
+ */
1400
+ class NotificationPanelComponent {
1401
+ portal = inject(PortalService);
1402
+ onClose() {
1403
+ this.portal.hideNotification();
1404
+ }
1405
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NotificationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1406
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: NotificationPanelComponent, isStandalone: true, selector: "apa-notification-panel", ngImport: i0, template: `
1407
+ @if (portal.notification().isVisible) {
1408
+ <div class="apa-notification-panel"
1409
+ [style.width.px]="portal.notification().width">
1410
+ <button class="apa-notification-close" (click)="onClose()" title="Schliessen">
1411
+ <svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
1412
+ <polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
1413
+ </svg>
1414
+ </button>
1415
+ <ng-content />
1416
+ </div>
1417
+ }
1418
+ `, isInline: true, styles: [":host{display:contents}\n"] });
1419
+ }
1420
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NotificationPanelComponent, decorators: [{
1421
+ type: Component,
1422
+ args: [{ selector: 'apa-notification-panel', standalone: true, template: `
1423
+ @if (portal.notification().isVisible) {
1424
+ <div class="apa-notification-panel"
1425
+ [style.width.px]="portal.notification().width">
1426
+ <button class="apa-notification-close" (click)="onClose()" title="Schliessen">
1427
+ <svg viewBox="0 0 11 11" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg">
1428
+ <polygon class="msportal-fx-svg-placeholder" points="10.4,1.4 9.6,0.6 5.5,4.7 1.4,0.6 0.6,1.4 4.7,5.5 0.6,9.6 1.4,10.4 5.5,6.3 9.6,10.4 10.4,9.6 6.3,5.5"/>
1429
+ </svg>
1430
+ </button>
1431
+ <ng-content />
1432
+ </div>
1433
+ }
1434
+ `, styles: [":host{display:contents}\n"] }]
1435
+ }] });
1436
+
1437
+ /**
1438
+ * Avatar menu — user account dropdown in the portal header.
1439
+ * Ported from fxs-avatarmenu CSS structure in v0.2.346.
1440
+ *
1441
+ * Shows the user's name and email. Clicking toggles a dropdown
1442
+ * with optional actions (sign out, etc.).
1443
+ *
1444
+ * Usage:
1445
+ * ```html
1446
+ * <apa-avatar-menu>
1447
+ * <a href="/Account/SignOut">Abmelden</a>
1448
+ * </apa-avatar-menu>
1449
+ * ```
1450
+ */
1451
+ class AvatarMenuComponent {
1452
+ portal = inject(PortalService);
1453
+ userAccount() {
1454
+ return this.portal.avatarMenu().userAccount;
1455
+ }
1456
+ displayName() {
1457
+ return getUserDisplayName(this.userAccount());
1458
+ }
1459
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AvatarMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1460
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AvatarMenuComponent, isStandalone: true, selector: "apa-avatar-menu", ngImport: i0, template: `
1461
+ <div class="fxs-avatarmenu">
1462
+ <a class="fxs-has-hover" (click)="portal.toggleAvatarMenu()">
1463
+ <div class="fxs-avatarmenu-header">
1464
+ <span class="fxs-avatarmenu-username">{{ displayName() }}</span>
1465
+ <span class="fxs-avatarmenu-emailaddress">{{ userAccount().emailAddress }}</span>
1466
+ </div>
1467
+ </a>
1468
+ @if (portal.avatarMenu().isOpen) {
1469
+ <div class="fxs-avatarmenu-dropdown" style="display:block;">
1470
+ <ul>
1471
+ <ng-content />
1472
+ </ul>
1473
+ </div>
1474
+ }
1475
+ </div>
1476
+ `, isInline: true });
1477
+ }
1478
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AvatarMenuComponent, decorators: [{
1479
+ type: Component,
1480
+ args: [{
1481
+ selector: 'apa-avatar-menu',
1482
+ standalone: true,
1483
+ template: `
1484
+ <div class="fxs-avatarmenu">
1485
+ <a class="fxs-has-hover" (click)="portal.toggleAvatarMenu()">
1486
+ <div class="fxs-avatarmenu-header">
1487
+ <span class="fxs-avatarmenu-username">{{ displayName() }}</span>
1488
+ <span class="fxs-avatarmenu-emailaddress">{{ userAccount().emailAddress }}</span>
1489
+ </div>
1490
+ </a>
1491
+ @if (portal.avatarMenu().isOpen) {
1492
+ <div class="fxs-avatarmenu-dropdown" style="display:block;">
1493
+ <ul>
1494
+ <ng-content />
1495
+ </ul>
1496
+ </div>
1497
+ }
1498
+ </div>
1499
+ `,
1500
+ }]
1501
+ }] });
1502
+
1503
+ /**
1504
+ * Sidebar navigation — optional vertical navigation panel.
1505
+ * New in v0.3.0 (no equivalent in v0.2.346).
1506
+ *
1507
+ * Provides a collapsible sidebar with navigation items that
1508
+ * open blades when clicked.
1509
+ *
1510
+ * Usage:
1511
+ * ```html
1512
+ * <apa-sidebar [items]="sidebarItems" [collapsed]="false" />
1513
+ * ```
1514
+ */
1515
+ class SidebarComponent {
1516
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
1517
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
1518
+ bladeService = inject(BladeService);
1519
+ visibleItems() {
1520
+ return this.items().filter((item) => item.isVisible);
1521
+ }
1522
+ onItemClick(item) {
1523
+ if (item.bladePath) {
1524
+ this.bladeService.setFirstBlade(item.bladePath, item.title);
1525
+ }
1526
+ item.callback?.();
1527
+ }
1528
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1529
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: SidebarComponent, isStandalone: true, selector: "apa-sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1530
+ <nav class="apa-sidebar" [class.apa-sidebar-collapsed]="collapsed()">
1531
+ @for (item of visibleItems(); track item.bladePath) {
1532
+ <a class="apa-sidebar-item"
1533
+ (click)="onItemClick(item)"
1534
+ style="cursor:pointer;">
1535
+ @if (item.cssClass) {
1536
+ <i [class]="item.cssClass" class="apa-sidebar-icon"></i>
1537
+ }
1538
+ @if (!collapsed()) {
1539
+ <span class="apa-sidebar-label">{{ item.title }}</span>
1540
+ }
1541
+ </a>
1542
+ }
1543
+ </nav>
1544
+ `, isInline: true, styles: [".apa-sidebar{display:flex;flex-direction:column;background-color:var(--apa-chrome);width:200px;height:100%;padding-top:10px;transition:width .2s ease}.apa-sidebar-collapsed{width:50px}.apa-sidebar-item{display:flex;align-items:center;padding:10px 15px;color:var(--apa-chrome-text);text-decoration:none;font-size:13px;transition:background-color .15s ease}.apa-sidebar-item:hover{background-color:var(--apa-chrome-hover)}.apa-sidebar-icon{margin-right:10px;width:20px;text-align:center}.apa-sidebar-collapsed .apa-sidebar-icon{margin-right:0}\n"] });
1545
+ }
1546
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: SidebarComponent, decorators: [{
1547
+ type: Component,
1548
+ args: [{ selector: 'apa-sidebar', standalone: true, template: `
1549
+ <nav class="apa-sidebar" [class.apa-sidebar-collapsed]="collapsed()">
1550
+ @for (item of visibleItems(); track item.bladePath) {
1551
+ <a class="apa-sidebar-item"
1552
+ (click)="onItemClick(item)"
1553
+ style="cursor:pointer;">
1554
+ @if (item.cssClass) {
1555
+ <i [class]="item.cssClass" class="apa-sidebar-icon"></i>
1556
+ }
1557
+ @if (!collapsed()) {
1558
+ <span class="apa-sidebar-label">{{ item.title }}</span>
1559
+ }
1560
+ </a>
1561
+ }
1562
+ </nav>
1563
+ `, styles: [".apa-sidebar{display:flex;flex-direction:column;background-color:var(--apa-chrome);width:200px;height:100%;padding-top:10px;transition:width .2s ease}.apa-sidebar-collapsed{width:50px}.apa-sidebar-item{display:flex;align-items:center;padding:10px 15px;color:var(--apa-chrome-text);text-decoration:none;font-size:13px;transition:background-color .15s ease}.apa-sidebar-item:hover{background-color:var(--apa-chrome-hover)}.apa-sidebar-icon{margin-right:10px;width:20px;text-align:center}.apa-sidebar-collapsed .apa-sidebar-icon{margin-right:0}\n"] }]
1564
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }] } });
1565
+
1566
+ /*
1567
+ * Public API Surface of @ardimedia/angular-portal-azure
1568
+ */
1569
+ // Models
1570
+
1571
+ /**
1572
+ * Generated bundle index. Do not edit.
1573
+ */
1574
+
1575
+ export { AvatarMenuComponent, BladeComponent, BladeDetailComponent, BladeGridComponent, BladeHostComponent, BladeNavComponent, BladeService, CommandBarComponent, NotificationPanelComponent, PanoramaComponent, PortalLayoutComponent, PortalService, SidebarComponent, TILE_DIMENSIONS, TileComponent, TileSize, clearStatusBar, createAvatarMenu, createBlade, createCommand, createDataBlade, createDetailCommands, createNavItem, createNotificationPanel, createPanorama, createTile, executeDeleteItem, executeLoadItem, executeLoadItems, executeSaveItem, filterItems, getUserDisplayName, layoutTiles, providePortalAzure, statusBarError, statusBarInfo, statusBarSuccess };
1576
+ //# sourceMappingURL=ardimedia-angular-portal-azure.mjs.map