@ojiepermana/angular 21.1.4 → 21.1.5

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.
@@ -1,8 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { provideEnvironmentInitializer, inject, makeEnvironmentProviders, ChangeDetectionStrategy, Component, input, computed } from '@angular/core';
2
+ import { provideEnvironmentInitializer, inject, makeEnvironmentProviders, viewChild, input, output, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { provideMaterialLayout, LayoutService } from '@ojiepermana/angular/layout';
4
- import { NavigationService, DEFAULT_NAVIGATION_ID, TopbarComponent, SidebarComponent } from '@ojiepermana/angular/navigation';
5
- import { provideMaterialTheme, withMaterialDefaults } from '@ojiepermana/angular/theme';
4
+ import { NavigationService, DEFAULT_NAVIGATION_ID, UiNavIconComponent, TopbarComponent, SidebarComponent } from '@ojiepermana/angular/navigation';
5
+ import { provideMaterialTheme, withMaterialDefaults, ThemeService } from '@ojiepermana/angular/theme';
6
+ import { cn, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, ButtonComponent, PopoverContentDirective, PopoverTriggerDirective } from '@ojiepermana/angular/component';
6
7
  import { RouterOutlet } from '@angular/router';
7
8
 
8
9
  const ETOS_BRAND_NAME = 'etos';
@@ -43,6 +44,524 @@ function provideEtosBrand(options = {}) {
43
44
  return makeEnvironmentProviders(providers);
44
45
  }
45
46
 
47
+ const THEME_SCHEME_OPTIONS = [
48
+ { value: 'light', label: 'Light', icon: 'light_mode' },
49
+ { value: 'dark', label: 'Dark', icon: 'dark_mode' },
50
+ { value: 'system', label: 'System', icon: 'computer' },
51
+ ];
52
+ const LAYOUT_MODE_OPTIONS = [
53
+ { value: 'horizontal', label: 'Horizontal', icon: 'view_column' },
54
+ { value: 'vertical', label: 'Vertical', icon: 'view_sidebar' },
55
+ ];
56
+ const LAYOUT_WIDTH_OPTIONS = [
57
+ { value: 'full', label: 'Full', icon: 'fit_screen' },
58
+ { value: 'fixed', label: 'Fixed', icon: 'center_focus_strong' },
59
+ ];
60
+ class EtosThemeSwitcherComponent {
61
+ theme = inject(ThemeService);
62
+ layout = inject(LayoutService);
63
+ popoverTrigger = viewChild.required('trigger');
64
+ class = input('', ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
65
+ userInfo = input(null, ...(ngDevMode ? [{ debugName: "userInfo" }] : /* istanbul ignore next */ []));
66
+ userName = input('User', ...(ngDevMode ? [{ debugName: "userName" }] : /* istanbul ignore next */ []));
67
+ userSubtitle = input('Theme and layout preferences', ...(ngDevMode ? [{ debugName: "userSubtitle" }] : /* istanbul ignore next */ []));
68
+ avatarSrc = input(null, ...(ngDevMode ? [{ debugName: "avatarSrc" }] : /* istanbul ignore next */ []));
69
+ avatarAlt = input('', ...(ngDevMode ? [{ debugName: "avatarAlt" }] : /* istanbul ignore next */ []));
70
+ quickActions = input.required(...(ngDevMode ? [{ debugName: "quickActions" }] : /* istanbul ignore next */ []));
71
+ notificationShortcut = input(null, ...(ngDevMode ? [{ debugName: "notificationShortcut" }] : /* istanbul ignore next */ []));
72
+ showNotificationShortcut = input(false, ...(ngDevMode ? [{ debugName: "showNotificationShortcut" }] : /* istanbul ignore next */ []));
73
+ popoverSide = input('bottom', ...(ngDevMode ? [{ debugName: "popoverSide" }] : /* istanbul ignore next */ []));
74
+ popoverAlign = input('end', ...(ngDevMode ? [{ debugName: "popoverAlign" }] : /* istanbul ignore next */ []));
75
+ actionSelected = output();
76
+ themeMode = this.theme.mode;
77
+ themeScheme = this.theme.scheme;
78
+ layoutMode = this.layout.mode;
79
+ layoutWidth = this.layout.width;
80
+ themeSchemeOptions = THEME_SCHEME_OPTIONS;
81
+ layoutModeOptions = LAYOUT_MODE_OPTIONS;
82
+ layoutWidthOptions = LAYOUT_WIDTH_OPTIONS;
83
+ notificationShortcutConfig = computed(() => {
84
+ const shortcut = this.notificationShortcut();
85
+ if (shortcut) {
86
+ return {
87
+ value: shortcut.value ?? 'notifications',
88
+ icon: shortcut.icon ?? 'notifications',
89
+ ariaLabel: shortcut.ariaLabel ?? `Open notifications for ${this.resolvedUserName()}`,
90
+ };
91
+ }
92
+ if (!this.showNotificationShortcut()) {
93
+ return null;
94
+ }
95
+ return {
96
+ value: 'notifications',
97
+ icon: 'notifications',
98
+ ariaLabel: `Open notifications for ${this.resolvedUserName()}`,
99
+ };
100
+ }, ...(ngDevMode ? [{ debugName: "notificationShortcutConfig" }] : /* istanbul ignore next */ []));
101
+ actionOptions = computed(() => {
102
+ const shortcutValue = this.notificationShortcutConfig()?.value;
103
+ if (!shortcutValue) {
104
+ return this.quickActions();
105
+ }
106
+ return this.quickActions().filter((action) => action.value !== shortcutValue);
107
+ }, ...(ngDevMode ? [{ debugName: "actionOptions" }] : /* istanbul ignore next */ []));
108
+ hostClasses = computed(() => cn('inline-flex shrink-0 items-center gap-2', this.class()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
109
+ resolvedUserName = computed(() => this.userInfo()?.name?.trim() || this.userName(), ...(ngDevMode ? [{ debugName: "resolvedUserName" }] : /* istanbul ignore next */ []));
110
+ resolvedUserSubtitle = computed(() => this.userInfo()?.subtitle ?? this.userSubtitle(), ...(ngDevMode ? [{ debugName: "resolvedUserSubtitle" }] : /* istanbul ignore next */ []));
111
+ resolvedAvatarSrc = computed(() => this.userInfo()?.avatarSrc ?? this.avatarSrc(), ...(ngDevMode ? [{ debugName: "resolvedAvatarSrc" }] : /* istanbul ignore next */ []));
112
+ resolvedAvatarAlt = computed(() => this.userInfo()?.avatarAlt ?? this.avatarAlt(), ...(ngDevMode ? [{ debugName: "resolvedAvatarAlt" }] : /* istanbul ignore next */ []));
113
+ hasAvatar = computed(() => !!this.resolvedAvatarSrc(), ...(ngDevMode ? [{ debugName: "hasAvatar" }] : /* istanbul ignore next */ []));
114
+ avatarImageSrc = computed(() => this.resolvedAvatarSrc() ?? '', ...(ngDevMode ? [{ debugName: "avatarImageSrc" }] : /* istanbul ignore next */ []));
115
+ avatarAltText = computed(() => this.resolvedAvatarAlt() || `${this.resolvedUserName()} avatar`, ...(ngDevMode ? [{ debugName: "avatarAltText" }] : /* istanbul ignore next */ []));
116
+ initials = computed(() => this.toInitials(this.resolvedUserName()), ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
117
+ triggerLabel = computed(() => `Open user info for ${this.resolvedUserName()}`, ...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
118
+ triggerButtonClasses(open) {
119
+ return cn('relative h-8 w-8 rounded-full p-0 transition-colors duration-150 hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background', open && 'bg-muted/50');
120
+ }
121
+ notificationButtonClasses() {
122
+ return cn('h-8 w-8 rounded-[var(--etos-layout-frame-radius)] p-0 text-muted-foreground transition-colors duration-150 hover:bg-muted/50 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background');
123
+ }
124
+ segmentedOptionClasses(active) {
125
+ return cn('h-10 rounded-[var(--etos-layout-frame-radius)] border border-transparent px-[0.3125rem] py-[0.1875rem] text-foreground transition-colors', active ? 'bg-background shadow-sm' : 'text-muted-foreground hover:bg-background/70');
126
+ }
127
+ themeOptionClasses(active) {
128
+ return this.segmentedOptionClasses(active);
129
+ }
130
+ themeIconClasses(active) {
131
+ return active ? 'text-foreground' : 'text-muted-foreground';
132
+ }
133
+ actionButtonClasses(tone = 'default') {
134
+ return cn('h-12 w-full justify-start gap-2.5 rounded-[var(--etos-layout-frame-radius)] border border-transparent px-2 py-1.5 text-left transition-colors hover:bg-muted/50', tone === 'destructive' && 'hover:bg-destructive/8 focus-visible:ring-destructive/30');
135
+ }
136
+ actionIconClasses(tone = 'default') {
137
+ return tone === 'destructive' ? 'text-destructive' : 'text-foreground';
138
+ }
139
+ actionLabelClasses(tone = 'default') {
140
+ return tone === 'destructive' ? 'text-sm font-medium text-destructive' : 'text-sm font-medium text-foreground';
141
+ }
142
+ setThemeScheme(mode) {
143
+ this.theme.setScheme(mode);
144
+ }
145
+ setLayoutMode(mode) {
146
+ this.layout.setMode(mode);
147
+ }
148
+ setLayoutWidth(width) {
149
+ this.layout.setWidth(width);
150
+ }
151
+ triggerAction(action) {
152
+ this.actionSelected.emit(action);
153
+ this.popoverTrigger().close();
154
+ }
155
+ triggerNotificationAction(event) {
156
+ event.stopPropagation();
157
+ const shortcut = this.notificationShortcutConfig();
158
+ if (!shortcut) {
159
+ return;
160
+ }
161
+ this.actionSelected.emit(shortcut.value);
162
+ }
163
+ labelForLayoutMode(mode) {
164
+ return mode === 'horizontal' ? 'Horizontal' : 'Vertical';
165
+ }
166
+ labelForLayoutWidth(width) {
167
+ return width === 'full' ? 'Full' : 'Fixed';
168
+ }
169
+ toInitials(name) {
170
+ const segments = name.trim().split(/\s+/).filter(Boolean);
171
+ if (segments.length === 0) {
172
+ return 'UI';
173
+ }
174
+ if (segments.length === 1) {
175
+ return segments[0].slice(0, 2).toUpperCase();
176
+ }
177
+ return `${segments[0][0] ?? ''}${segments[1][0] ?? ''}`.toUpperCase();
178
+ }
179
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EtosThemeSwitcherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
180
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: EtosThemeSwitcherComponent, isStandalone: true, selector: "etos-theme-switcher", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, userInfo: { classPropertyName: "userInfo", publicName: "userInfo", isSignal: true, isRequired: false, transformFunction: null }, userName: { classPropertyName: "userName", publicName: "userName", isSignal: true, isRequired: false, transformFunction: null }, userSubtitle: { classPropertyName: "userSubtitle", publicName: "userSubtitle", isSignal: true, isRequired: false, transformFunction: null }, avatarSrc: { classPropertyName: "avatarSrc", publicName: "avatarSrc", isSignal: true, isRequired: false, transformFunction: null }, avatarAlt: { classPropertyName: "avatarAlt", publicName: "avatarAlt", isSignal: true, isRequired: false, transformFunction: null }, quickActions: { classPropertyName: "quickActions", publicName: "quickActions", isSignal: true, isRequired: true, transformFunction: null }, notificationShortcut: { classPropertyName: "notificationShortcut", publicName: "notificationShortcut", isSignal: true, isRequired: false, transformFunction: null }, showNotificationShortcut: { classPropertyName: "showNotificationShortcut", publicName: "showNotificationShortcut", isSignal: true, isRequired: false, transformFunction: null }, popoverSide: { classPropertyName: "popoverSide", publicName: "popoverSide", isSignal: true, isRequired: false, transformFunction: null }, popoverAlign: { classPropertyName: "popoverAlign", publicName: "popoverAlign", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { actionSelected: "actionSelected" }, host: { properties: { "class": "hostClasses()" } }, viewQueries: [{ propertyName: "popoverTrigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
181
+ @if (notificationShortcutConfig(); as shortcut) {
182
+ <button
183
+ type="button"
184
+ ui-button
185
+ variant="ghost"
186
+ size="icon"
187
+ data-trigger-action="notifications"
188
+ [attr.data-value]="shortcut.value"
189
+ [attr.aria-label]="shortcut.ariaLabel"
190
+ [class]="notificationButtonClasses()"
191
+ (click)="triggerNotificationAction($event)">
192
+ <ui-nav-icon [name]="shortcut.icon" [size]="18" class="text-current" />
193
+ </button>
194
+ }
195
+
196
+ <button
197
+ #trigger="uiPopoverTrigger"
198
+ type="button"
199
+ ui-button
200
+ variant="ghost"
201
+ size="icon"
202
+ [uiPopoverTrigger]="preferencesPanel"
203
+ [side]="popoverSide()"
204
+ [align]="popoverAlign()"
205
+ [attr.aria-label]="triggerLabel()"
206
+ [class]="triggerButtonClasses(trigger.isOpen())">
207
+ <ui-avatar class="h-8 w-8 border border-border/60 shadow-[inset_0_1px_0_rgba(255,255,255,0.18)]">
208
+ @if (hasAvatar()) {
209
+ <ui-avatar-image [src]="avatarImageSrc()" [alt]="avatarAltText()" />
210
+ }
211
+ <ui-avatar-fallback class="bg-primary text-xs font-semibold tracking-[0.24em] text-primary-foreground">
212
+ {{ initials() }}
213
+ </ui-avatar-fallback>
214
+ </ui-avatar>
215
+ </button>
216
+
217
+ <ng-template uiPopoverContent #preferencesPanel="uiPopoverContent">
218
+ <section
219
+ data-etos-theme-switcher-panel
220
+ role="dialog"
221
+ aria-label="User Info"
222
+ class="w-[min(21rem,calc(100vw-1.5rem))] overflow-hidden rounded-[var(--etos-layout-frame-radius)] border border-border/70 bg-background text-foreground shadow-[0_18px_48px_rgba(15,23,42,0.12)]">
223
+ <header class="p-5 pb-4">
224
+ <div class="flex items-center gap-4">
225
+ <ui-avatar class="h-14 w-14 border border-border/60 shadow-sm">
226
+ @if (hasAvatar()) {
227
+ <ui-avatar-image [src]="avatarImageSrc()" [alt]="avatarAltText()" />
228
+ }
229
+ <ui-avatar-fallback class="bg-primary text-sm font-semibold tracking-[0.24em] text-primary-foreground">
230
+ {{ initials() }}
231
+ </ui-avatar-fallback>
232
+ </ui-avatar>
233
+
234
+ <div class="min-w-0 flex min-h-14 flex-1 flex-col justify-center self-center">
235
+ <div class="space-y-px">
236
+ <h2 class="truncate text-[1.1rem] font-semibold leading-none tracking-tight text-foreground">
237
+ {{ resolvedUserName() }}
238
+ </h2>
239
+ <p class="text-sm leading-[0.95rem] text-muted-foreground">
240
+ {{ resolvedUserSubtitle() }}
241
+ </p>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </header>
246
+
247
+ <div class="space-y-4 px-5 pb-5">
248
+ <section
249
+ data-setting="theme-scheme"
250
+ [attr.data-current]="themeScheme()"
251
+ class="rounded-[var(--etos-layout-frame-radius)] bg-muted/65 p-0.5">
252
+ <div class="grid grid-cols-3 gap-1">
253
+ @for (option of themeSchemeOptions; track option.value) {
254
+ <button
255
+ type="button"
256
+ ui-button
257
+ size="sm"
258
+ variant="ghost"
259
+ [class]="themeOptionClasses(themeScheme() === option.value)"
260
+ data-setting-option="theme-scheme"
261
+ [attr.data-value]="option.value"
262
+ (click)="setThemeScheme(option.value)">
263
+ <span class="inline-flex items-center gap-2.5">
264
+ <ui-nav-icon
265
+ [name]="option.icon"
266
+ [size]="18"
267
+ [class]="themeIconClasses(themeScheme() === option.value)" />
268
+ <span class="text-sm font-semibold leading-none">{{ option.label }}</span>
269
+ </span>
270
+ </button>
271
+ }
272
+ </div>
273
+ </section>
274
+
275
+ <section data-setting="layout-mode" [attr.data-current]="layoutMode()" class="space-y-2">
276
+ <div class="px-1">
277
+ <p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-muted-foreground">Layout</p>
278
+ </div>
279
+ <div class="rounded-[var(--etos-layout-frame-radius)] bg-muted/65 p-0.5">
280
+ <div class="grid grid-cols-2 gap-1">
281
+ @for (option of layoutModeOptions; track option.value) {
282
+ <button
283
+ type="button"
284
+ ui-button
285
+ size="sm"
286
+ variant="ghost"
287
+ [class]="segmentedOptionClasses(layoutMode() === option.value)"
288
+ data-setting-option="layout-mode"
289
+ [attr.data-value]="option.value"
290
+ (click)="setLayoutMode(option.value)">
291
+ <span class="inline-flex items-center gap-2.5">
292
+ <ui-nav-icon
293
+ [name]="option.icon"
294
+ [size]="18"
295
+ [class]="themeIconClasses(layoutMode() === option.value)" />
296
+ <span class="text-sm font-semibold leading-none">{{ option.label }}</span>
297
+ </span>
298
+ </button>
299
+ }
300
+ </div>
301
+ </div>
302
+ </section>
303
+
304
+ <section data-setting="layout-width" [attr.data-current]="layoutWidth()" class="space-y-2">
305
+ <div class="px-1">
306
+ <p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-muted-foreground">Width</p>
307
+ </div>
308
+ <div class="rounded-[var(--etos-layout-frame-radius)] bg-muted/65 p-0.5">
309
+ <div class="grid grid-cols-2 gap-1">
310
+ @for (option of layoutWidthOptions; track option.value) {
311
+ <button
312
+ type="button"
313
+ ui-button
314
+ size="sm"
315
+ variant="ghost"
316
+ [class]="segmentedOptionClasses(layoutWidth() === option.value)"
317
+ data-setting-option="layout-width"
318
+ [attr.data-value]="option.value"
319
+ (click)="setLayoutWidth(option.value)">
320
+ <span class="inline-flex items-center gap-2.5">
321
+ <ui-nav-icon
322
+ [name]="option.icon"
323
+ [size]="18"
324
+ [class]="themeIconClasses(layoutWidth() === option.value)" />
325
+ <span class="text-sm font-semibold leading-none">{{ option.label }}</span>
326
+ </span>
327
+ </button>
328
+ }
329
+ </div>
330
+ </div>
331
+ </section>
332
+
333
+ <section class="space-y-1 border-t border-border/70 pt-2">
334
+ <h3 class="sr-only">Quick Actions</h3>
335
+ <div class="grid gap-1">
336
+ @for (action of actionOptions(); track action.value) {
337
+ <button
338
+ type="button"
339
+ ui-button
340
+ variant="ghost"
341
+ [class]="actionButtonClasses(action.tone ?? 'default')"
342
+ data-action-option
343
+ [attr.data-value]="action.value"
344
+ (click)="triggerAction(action.value)">
345
+ <span class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted/65">
346
+ <ui-nav-icon
347
+ [name]="action.icon"
348
+ [size]="19"
349
+ [class]="actionIconClasses(action.tone ?? 'default')" />
350
+ </span>
351
+ <span [class]="actionLabelClasses(action.tone ?? 'default')">
352
+ {{ action.label }}
353
+ </span>
354
+ </button>
355
+ }
356
+ </div>
357
+ </section>
358
+ </div>
359
+ </section>
360
+ </ng-template>
361
+ `, isInline: true, dependencies: [{ kind: "component", type: AvatarComponent, selector: "ui-avatar", inputs: ["class"] }, { kind: "component", type: AvatarFallbackComponent, selector: "ui-avatar-fallback", inputs: ["class"] }, { kind: "component", type: AvatarImageComponent, selector: "ui-avatar-image", inputs: ["src", "alt", "class"] }, { kind: "component", type: ButtonComponent, selector: "button[ui-button], a[ui-button]", inputs: ["variant", "size", "class"] }, { kind: "directive", type: PopoverContentDirective, selector: "ng-template[uiPopoverContent]", exportAs: ["uiPopoverContent"] }, { kind: "directive", type: PopoverTriggerDirective, selector: "[uiPopoverTrigger]", inputs: ["uiPopoverTrigger", "side", "align", "disabled"], outputs: ["openedChange"], exportAs: ["uiPopoverTrigger"] }, { kind: "component", type: UiNavIconComponent, selector: "ui-nav-icon", inputs: ["name", "class", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
362
+ }
363
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EtosThemeSwitcherComponent, decorators: [{
364
+ type: Component,
365
+ args: [{
366
+ selector: 'etos-theme-switcher',
367
+ changeDetection: ChangeDetectionStrategy.OnPush,
368
+ imports: [
369
+ AvatarComponent,
370
+ AvatarFallbackComponent,
371
+ AvatarImageComponent,
372
+ ButtonComponent,
373
+ PopoverContentDirective,
374
+ PopoverTriggerDirective,
375
+ UiNavIconComponent,
376
+ ],
377
+ host: {
378
+ '[class]': 'hostClasses()',
379
+ },
380
+ template: `
381
+ @if (notificationShortcutConfig(); as shortcut) {
382
+ <button
383
+ type="button"
384
+ ui-button
385
+ variant="ghost"
386
+ size="icon"
387
+ data-trigger-action="notifications"
388
+ [attr.data-value]="shortcut.value"
389
+ [attr.aria-label]="shortcut.ariaLabel"
390
+ [class]="notificationButtonClasses()"
391
+ (click)="triggerNotificationAction($event)">
392
+ <ui-nav-icon [name]="shortcut.icon" [size]="18" class="text-current" />
393
+ </button>
394
+ }
395
+
396
+ <button
397
+ #trigger="uiPopoverTrigger"
398
+ type="button"
399
+ ui-button
400
+ variant="ghost"
401
+ size="icon"
402
+ [uiPopoverTrigger]="preferencesPanel"
403
+ [side]="popoverSide()"
404
+ [align]="popoverAlign()"
405
+ [attr.aria-label]="triggerLabel()"
406
+ [class]="triggerButtonClasses(trigger.isOpen())">
407
+ <ui-avatar class="h-8 w-8 border border-border/60 shadow-[inset_0_1px_0_rgba(255,255,255,0.18)]">
408
+ @if (hasAvatar()) {
409
+ <ui-avatar-image [src]="avatarImageSrc()" [alt]="avatarAltText()" />
410
+ }
411
+ <ui-avatar-fallback class="bg-primary text-xs font-semibold tracking-[0.24em] text-primary-foreground">
412
+ {{ initials() }}
413
+ </ui-avatar-fallback>
414
+ </ui-avatar>
415
+ </button>
416
+
417
+ <ng-template uiPopoverContent #preferencesPanel="uiPopoverContent">
418
+ <section
419
+ data-etos-theme-switcher-panel
420
+ role="dialog"
421
+ aria-label="User Info"
422
+ class="w-[min(21rem,calc(100vw-1.5rem))] overflow-hidden rounded-[var(--etos-layout-frame-radius)] border border-border/70 bg-background text-foreground shadow-[0_18px_48px_rgba(15,23,42,0.12)]">
423
+ <header class="p-5 pb-4">
424
+ <div class="flex items-center gap-4">
425
+ <ui-avatar class="h-14 w-14 border border-border/60 shadow-sm">
426
+ @if (hasAvatar()) {
427
+ <ui-avatar-image [src]="avatarImageSrc()" [alt]="avatarAltText()" />
428
+ }
429
+ <ui-avatar-fallback class="bg-primary text-sm font-semibold tracking-[0.24em] text-primary-foreground">
430
+ {{ initials() }}
431
+ </ui-avatar-fallback>
432
+ </ui-avatar>
433
+
434
+ <div class="min-w-0 flex min-h-14 flex-1 flex-col justify-center self-center">
435
+ <div class="space-y-px">
436
+ <h2 class="truncate text-[1.1rem] font-semibold leading-none tracking-tight text-foreground">
437
+ {{ resolvedUserName() }}
438
+ </h2>
439
+ <p class="text-sm leading-[0.95rem] text-muted-foreground">
440
+ {{ resolvedUserSubtitle() }}
441
+ </p>
442
+ </div>
443
+ </div>
444
+ </div>
445
+ </header>
446
+
447
+ <div class="space-y-4 px-5 pb-5">
448
+ <section
449
+ data-setting="theme-scheme"
450
+ [attr.data-current]="themeScheme()"
451
+ class="rounded-[var(--etos-layout-frame-radius)] bg-muted/65 p-0.5">
452
+ <div class="grid grid-cols-3 gap-1">
453
+ @for (option of themeSchemeOptions; track option.value) {
454
+ <button
455
+ type="button"
456
+ ui-button
457
+ size="sm"
458
+ variant="ghost"
459
+ [class]="themeOptionClasses(themeScheme() === option.value)"
460
+ data-setting-option="theme-scheme"
461
+ [attr.data-value]="option.value"
462
+ (click)="setThemeScheme(option.value)">
463
+ <span class="inline-flex items-center gap-2.5">
464
+ <ui-nav-icon
465
+ [name]="option.icon"
466
+ [size]="18"
467
+ [class]="themeIconClasses(themeScheme() === option.value)" />
468
+ <span class="text-sm font-semibold leading-none">{{ option.label }}</span>
469
+ </span>
470
+ </button>
471
+ }
472
+ </div>
473
+ </section>
474
+
475
+ <section data-setting="layout-mode" [attr.data-current]="layoutMode()" class="space-y-2">
476
+ <div class="px-1">
477
+ <p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-muted-foreground">Layout</p>
478
+ </div>
479
+ <div class="rounded-[var(--etos-layout-frame-radius)] bg-muted/65 p-0.5">
480
+ <div class="grid grid-cols-2 gap-1">
481
+ @for (option of layoutModeOptions; track option.value) {
482
+ <button
483
+ type="button"
484
+ ui-button
485
+ size="sm"
486
+ variant="ghost"
487
+ [class]="segmentedOptionClasses(layoutMode() === option.value)"
488
+ data-setting-option="layout-mode"
489
+ [attr.data-value]="option.value"
490
+ (click)="setLayoutMode(option.value)">
491
+ <span class="inline-flex items-center gap-2.5">
492
+ <ui-nav-icon
493
+ [name]="option.icon"
494
+ [size]="18"
495
+ [class]="themeIconClasses(layoutMode() === option.value)" />
496
+ <span class="text-sm font-semibold leading-none">{{ option.label }}</span>
497
+ </span>
498
+ </button>
499
+ }
500
+ </div>
501
+ </div>
502
+ </section>
503
+
504
+ <section data-setting="layout-width" [attr.data-current]="layoutWidth()" class="space-y-2">
505
+ <div class="px-1">
506
+ <p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-muted-foreground">Width</p>
507
+ </div>
508
+ <div class="rounded-[var(--etos-layout-frame-radius)] bg-muted/65 p-0.5">
509
+ <div class="grid grid-cols-2 gap-1">
510
+ @for (option of layoutWidthOptions; track option.value) {
511
+ <button
512
+ type="button"
513
+ ui-button
514
+ size="sm"
515
+ variant="ghost"
516
+ [class]="segmentedOptionClasses(layoutWidth() === option.value)"
517
+ data-setting-option="layout-width"
518
+ [attr.data-value]="option.value"
519
+ (click)="setLayoutWidth(option.value)">
520
+ <span class="inline-flex items-center gap-2.5">
521
+ <ui-nav-icon
522
+ [name]="option.icon"
523
+ [size]="18"
524
+ [class]="themeIconClasses(layoutWidth() === option.value)" />
525
+ <span class="text-sm font-semibold leading-none">{{ option.label }}</span>
526
+ </span>
527
+ </button>
528
+ }
529
+ </div>
530
+ </div>
531
+ </section>
532
+
533
+ <section class="space-y-1 border-t border-border/70 pt-2">
534
+ <h3 class="sr-only">Quick Actions</h3>
535
+ <div class="grid gap-1">
536
+ @for (action of actionOptions(); track action.value) {
537
+ <button
538
+ type="button"
539
+ ui-button
540
+ variant="ghost"
541
+ [class]="actionButtonClasses(action.tone ?? 'default')"
542
+ data-action-option
543
+ [attr.data-value]="action.value"
544
+ (click)="triggerAction(action.value)">
545
+ <span class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted/65">
546
+ <ui-nav-icon
547
+ [name]="action.icon"
548
+ [size]="19"
549
+ [class]="actionIconClasses(action.tone ?? 'default')" />
550
+ </span>
551
+ <span [class]="actionLabelClasses(action.tone ?? 'default')">
552
+ {{ action.label }}
553
+ </span>
554
+ </button>
555
+ }
556
+ </div>
557
+ </section>
558
+ </div>
559
+ </section>
560
+ </ng-template>
561
+ `,
562
+ }]
563
+ }], propDecorators: { popoverTrigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], userInfo: [{ type: i0.Input, args: [{ isSignal: true, alias: "userInfo", required: false }] }], userName: [{ type: i0.Input, args: [{ isSignal: true, alias: "userName", required: false }] }], userSubtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "userSubtitle", required: false }] }], avatarSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarSrc", required: false }] }], avatarAlt: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarAlt", required: false }] }], quickActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickActions", required: true }] }], notificationShortcut: [{ type: i0.Input, args: [{ isSignal: true, alias: "notificationShortcut", required: false }] }], showNotificationShortcut: [{ type: i0.Input, args: [{ isSignal: true, alias: "showNotificationShortcut", required: false }] }], popoverSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "popoverSide", required: false }] }], popoverAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "popoverAlign", required: false }] }], actionSelected: [{ type: i0.Output, args: ["actionSelected"] }] } });
564
+
46
565
  class EtosEmptyLayoutComponent {
47
566
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EtosEmptyLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
48
567
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: EtosEmptyLayoutComponent, isStandalone: true, selector: "etos-empty-layout", host: { attributes: { "data-brand-layout": "etos-empty" }, classAttribute: "etos-layout-empty-host" }, ngImport: i0, template: `
@@ -189,7 +708,15 @@ class EtosVerticalLayoutComponent {
189
708
  [position]="sidebarPosition()"
190
709
  [ariaLabel]="ariaLabel()"
191
710
  [style.border-left-width]="dividerBorderWidth()"
192
- [style.border-right-width]="dividerBorderWidth()" />
711
+ [style.border-right-width]="dividerBorderWidth()">
712
+ <div ui-sidebar-header class="flex h-full w-full min-w-0 items-center">
713
+ <ng-content select="[ui-layout-brand],[ui-topbar-start],[ui-sidebar-header]" />
714
+ </div>
715
+
716
+ <div ui-sidebar-footer class="flex h-full w-full min-w-0 items-center justify-between gap-3 px-3">
717
+ <ng-content select="[ui-layout-profile],[ui-topbar-end],[ui-sidebar-footer]" />
718
+ </div>
719
+ </ui-sidebar>
193
720
  <main [class]="mainClasses()">
194
721
  <router-outlet />
195
722
  </main>
@@ -215,7 +742,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
215
742
  [position]="sidebarPosition()"
216
743
  [ariaLabel]="ariaLabel()"
217
744
  [style.border-left-width]="dividerBorderWidth()"
218
- [style.border-right-width]="dividerBorderWidth()" />
745
+ [style.border-right-width]="dividerBorderWidth()">
746
+ <div ui-sidebar-header class="flex h-full w-full min-w-0 items-center">
747
+ <ng-content select="[ui-layout-brand],[ui-topbar-start],[ui-sidebar-header]" />
748
+ </div>
749
+
750
+ <div ui-sidebar-footer class="flex h-full w-full min-w-0 items-center justify-between gap-3 px-3">
751
+ <ng-content select="[ui-layout-profile],[ui-topbar-end],[ui-sidebar-footer]" />
752
+ </div>
753
+ </ui-sidebar>
219
754
  <main [class]="mainClasses()">
220
755
  <router-outlet />
221
756
  </main>
@@ -279,5 +814,5 @@ const ETOS_BRAND_VERSION = '0.0.1';
279
814
  * Generated bundle index. Do not edit.
280
815
  */
281
816
 
282
- export { ETOS_BRAND_NAME, ETOS_BRAND_VERSION, ETOS_LAYOUT_CONFIG, ETOS_THEME_CONFIG, EtosEmptyLayoutComponent, EtosHorizontalLayoutComponent, EtosLayoutComponent, EtosVerticalLayoutComponent, provideEtosBrand, provideEtosLayout, provideEtosTheme };
817
+ export { ETOS_BRAND_NAME, ETOS_BRAND_VERSION, ETOS_LAYOUT_CONFIG, ETOS_THEME_CONFIG, EtosEmptyLayoutComponent, EtosHorizontalLayoutComponent, EtosLayoutComponent, EtosThemeSwitcherComponent, EtosVerticalLayoutComponent, provideEtosBrand, provideEtosLayout, provideEtosTheme };
283
818
  //# sourceMappingURL=ojiepermana-angular-etos.mjs.map