@ojiepermana/angular 21.1.9 → 21.1.12

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,9 +1,9 @@
1
+ import { DOCUMENT, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
1
2
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, signal, computed, Injectable, PLATFORM_ID, input, ChangeDetectionStrategy, Component, ViewContainerRef, viewChild, effect, ElementRef } from '@angular/core';
3
+ import { inject, DestroyRef, signal, computed, effect, Injectable, PLATFORM_ID, input, ChangeDetectionStrategy, Component, ViewContainerRef, viewChild, ElementRef } from '@angular/core';
3
4
  import { Router, NavigationEnd, RouterLink, RouterLinkActive } from '@angular/router';
4
5
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
5
6
  import { filter, map } from 'rxjs/operators';
6
- import { DOCUMENT, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
7
7
  import { MatTooltip } from '@angular/material/tooltip';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
@@ -15,26 +15,42 @@ import { merge } from 'rxjs';
15
15
 
16
16
  /** Default registry key used when no id is specified. */
17
17
  const DEFAULT_NAVIGATION_ID = 'main';
18
+ const SIDEBAR_COLLAPSE_STORAGE_KEY = 'sidebar-collapse';
19
+ const LEGACY_SIDEBAR_APPEARANCE_STORAGE_KEY = 'sidebar-appearance';
20
+ function parseSidebarCollapsed(value) {
21
+ if (value === 'true')
22
+ return true;
23
+ if (value === 'false')
24
+ return false;
25
+ return null;
26
+ }
18
27
  /**
19
28
  * Signal-based global state untuk navigation (sidebar/topbar).
20
29
  *
21
30
  * Items disimpan dalam registry ber-key. Key default adalah `'main'`.
22
- * Komponen `ui-sidebar` / `ui-topbar` memilih registry via input `navigationId`.
31
+ * Komponen `sidebar` / `topbar` memilih registry via input `navigationId`.
23
32
  */
24
33
  class NavigationService {
34
+ doc = inject(DOCUMENT);
25
35
  router = inject(Router);
26
36
  destroyRef = inject(DestroyRef);
37
+ persistedSidebarCollapsed = this.readPersistedSidebarCollapsed();
27
38
  /** Internal version counter — incremented on every registry mutation. */
28
39
  _version = signal(0, ...(ngDevMode ? [{ debugName: "_version" }] : /* istanbul ignore next */ []));
29
40
  /** Internal map of registered navigation trees. */
30
41
  _registry = new Map();
42
+ _collapsed = signal(this.persistedSidebarCollapsed ?? false, ...(ngDevMode ? [{ debugName: "_collapsed" }] : /* istanbul ignore next */ []));
43
+ _hasStoredSidebarCollapse = signal(this.persistedSidebarCollapsed !== null, ...(ngDevMode ? [{ debugName: "_hasStoredSidebarCollapse" }] : /* istanbul ignore next */ []));
31
44
  /**
32
45
  * Backward-compatible accessor — returns items for the default (`'main'`) key.
33
46
  * Prefer `getItems(id)` when working with named registries.
34
47
  */
35
48
  items = computed(() => this.getItems(DEFAULT_NAVIGATION_ID)(), ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
49
+ /** Sidebar appearance preference (`default` or `thin`). */
50
+ sidebarAppearance = computed(() => (this._collapsed() ? 'thin' : 'default'), ...(ngDevMode ? [{ debugName: "sidebarAppearance" }] : /* istanbul ignore next */ []));
51
+ hasStoredSidebarCollapse = this._hasStoredSidebarCollapse.asReadonly();
36
52
  /** Sidebar collapsed (default ↔ thin) toggle untuk desktop. */
37
- collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
53
+ collapsed = this._collapsed.asReadonly();
38
54
  /** Sheet drawer terbuka di mobile. */
39
55
  mobileOpen = signal(false, ...(ngDevMode ? [{ debugName: "mobileOpen" }] : /* istanbul ignore next */ []));
40
56
  /** Set id grup / collapsable yang sedang terbuka. */
@@ -74,6 +90,12 @@ class NavigationService {
74
90
  return trail;
75
91
  }, ...(ngDevMode ? [{ debugName: "activeTrail" }] : /* istanbul ignore next */ []));
76
92
  constructor() {
93
+ effect(() => {
94
+ if (!this._hasStoredSidebarCollapse()) {
95
+ return;
96
+ }
97
+ this.persistSidebarCollapsed(this._collapsed());
98
+ });
77
99
  this.router.events
78
100
  .pipe(filter((e) => e instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
79
101
  .subscribe((e) => this.activeUrl.set(e.urlAfterRedirects));
@@ -98,12 +120,19 @@ class NavigationService {
98
120
  return this._registry.get(id) ?? [];
99
121
  });
100
122
  }
123
+ setSidebarAppearance(value) {
124
+ this.setCollapsed(value === 'thin');
125
+ }
126
+ toggleSidebarAppearance(currentAppearance = this.sidebarAppearance()) {
127
+ this.setCollapsed(currentAppearance !== 'thin');
128
+ }
101
129
  /** Toggle sidebar collapsed (default ↔ thin). */
102
130
  toggleCollapsed() {
103
- this.collapsed.update((v) => !v);
131
+ this.setCollapsed(!this._collapsed());
104
132
  }
105
133
  setCollapsed(value) {
106
- this.collapsed.set(value);
134
+ this._collapsed.set(value);
135
+ this._hasStoredSidebarCollapse.set(true);
107
136
  }
108
137
  openMobile() {
109
138
  this.mobileOpen.set(true);
@@ -137,10 +166,40 @@ class NavigationService {
137
166
  isActive(id) {
138
167
  return !!id && this.activeTrail().has(id);
139
168
  }
140
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
141
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NavigationService, providedIn: 'root' });
169
+ readPersistedSidebarCollapsed() {
170
+ try {
171
+ const storage = this.doc.defaultView?.localStorage;
172
+ const collapsed = parseSidebarCollapsed(storage?.getItem(SIDEBAR_COLLAPSE_STORAGE_KEY));
173
+ if (collapsed !== null) {
174
+ return collapsed;
175
+ }
176
+ const legacyAppearance = storage?.getItem(LEGACY_SIDEBAR_APPEARANCE_STORAGE_KEY);
177
+ if (legacyAppearance === 'thin') {
178
+ return true;
179
+ }
180
+ if (legacyAppearance === 'default') {
181
+ return false;
182
+ }
183
+ return null;
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ persistSidebarCollapsed(value) {
190
+ try {
191
+ const storage = this.doc.defaultView?.localStorage;
192
+ storage?.setItem(SIDEBAR_COLLAPSE_STORAGE_KEY, String(value));
193
+ storage?.removeItem(LEGACY_SIDEBAR_APPEARANCE_STORAGE_KEY);
194
+ }
195
+ catch {
196
+ /* ignore */
197
+ }
198
+ }
199
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
200
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: NavigationService, providedIn: 'root' });
142
201
  }
143
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NavigationService, decorators: [{
202
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: NavigationService, decorators: [{
144
203
  type: Injectable,
145
204
  args: [{ providedIn: 'root' }]
146
205
  }], ctorParameters: () => [] });
@@ -180,10 +239,10 @@ class UiNavIconComponent {
180
239
  link.setAttribute(MATERIAL_SYMBOLS_FONT_ATTR, MATERIAL_SYMBOLS_FONT_ID);
181
240
  this.doc.head.appendChild(link);
182
241
  }
183
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UiNavIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
184
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: UiNavIconComponent, isStandalone: true, selector: "ui-nav-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true", "translate": "no" }, properties: { "class": "classes()", "style.font-size.px": "size()", "style.font-variation-settings": "fontVariationSettings" } }, ngImport: i0, template: `{{ name() }}`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
242
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UiNavIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
243
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.11", type: UiNavIconComponent, isStandalone: true, selector: "ui-nav-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true", "translate": "no" }, properties: { "class": "classes()", "style.font-size.px": "size()", "style.font-variation-settings": "fontVariationSettings" } }, ngImport: i0, template: `{{ name() }}`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
185
244
  }
186
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UiNavIconComponent, decorators: [{
245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UiNavIconComponent, decorators: [{
187
246
  type: Component,
188
247
  args: [{
189
248
  selector: 'ui-nav-icon',
@@ -263,8 +322,8 @@ class UiNavItemComponent {
263
322
  item.action(item);
264
323
  }
265
324
  }
266
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UiNavItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
267
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: UiNavItemComponent, isStandalone: true, selector: "ui-nav-item", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
325
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UiNavItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
326
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: UiNavItemComponent, isStandalone: true, selector: "ui-nav-item", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
268
327
  @switch (type()) {
269
328
  @case ('divider') {
270
329
  <hr class="my-2 border-t border-border" role="separator" />
@@ -273,9 +332,9 @@ class UiNavItemComponent {
273
332
  <div class="flex-1"></div>
274
333
  }
275
334
  @case ('group') {
276
- <div class="p-3" role="group" [attr.aria-labelledby]="headingId()">
335
+ <div role="group" [attr.aria-labelledby]="headingId()">
277
336
  @if (!compact()) {
278
- <div class="sticky top-0 z-10 bg-background py-3 text-muted-foreground">
337
+ <div class="sticky top-0 z-10 p-3 text-muted-foreground backdrop-blur-3xl bg-transparent">
279
338
  <div [id]="headingId()" [class]="cn('ui-nav-heading text-muted-foreground', item().classes?.title)">
280
339
  {{ item().title }}
281
340
  </div>
@@ -295,7 +354,7 @@ class UiNavItemComponent {
295
354
  type="button"
296
355
  [class]="
297
356
  cn(
298
- 'ui-nav-text group/ni flex w-full items-center gap-3 rounded-md px-3 py-2 text-foreground/80 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
357
+ 'ui-nav-text group/ni flex w-full items-center gap-3 rounded-full p-3 text-foreground/80 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
299
358
  isTrailActive() && 'text-primary',
300
359
  item().classes?.wrapper
301
360
  )
@@ -308,7 +367,15 @@ class UiNavItemComponent {
308
367
  [matTooltipDisabled]="!compact()"
309
368
  (click)="toggleGroup()">
310
369
  @if (collapsableItem().icon) {
311
- <ui-nav-icon [name]="collapsableItem().icon!" [size]="18" [class]="item().classes?.icon ?? ''" />
370
+ <span
371
+ [class]="
372
+ cn(
373
+ 'inline-flex shrink-0 items-center justify-center',
374
+ open && 'h-7 w-7 rounded-full border border-brand'
375
+ )
376
+ ">
377
+ <ui-nav-icon [name]="collapsableItem().icon!" [size]="18" [class]="item().classes?.icon ?? ''" />
378
+ </span>
312
379
  }
313
380
  @if (!compact()) {
314
381
  <span [class]="cn('flex-1 truncate text-left', item().classes?.title)">
@@ -324,7 +391,10 @@ class UiNavItemComponent {
324
391
  }
325
392
  </button>
326
393
  @if (!compact() && open) {
327
- <div [id]="id + '-panel'" role="region" class="ml-3 mt-0.5 flex flex-col gap-0.5 border-l border-border pl-3">
394
+ <div
395
+ [id]="id + '-panel'"
396
+ role="region"
397
+ class="relative ml-6.5 mt-0.5 flex flex-col gap-0.5 pl-3 before:absolute before:bottom-0 before:left-0 before:-top-3.5 before:w-px before:bg-border before:content-['']">
328
398
  @for (child of collapsableItem().children; track child.id) {
329
399
  <ui-nav-item [item]="child" [level]="level() + 1" [compact]="false" />
330
400
  }
@@ -456,7 +526,7 @@ class UiNavItemComponent {
456
526
  }
457
527
  `, isInline: true, dependencies: [{ kind: "component", type: UiNavItemComponent, selector: "ui-nav-item", inputs: ["item", "level", "compact"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: UiNavIconComponent, selector: "ui-nav-icon", inputs: ["name", "class", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
458
528
  }
459
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UiNavItemComponent, decorators: [{
529
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UiNavItemComponent, decorators: [{
460
530
  type: Component,
461
531
  args: [{
462
532
  selector: 'ui-nav-item',
@@ -471,9 +541,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
471
541
  <div class="flex-1"></div>
472
542
  }
473
543
  @case ('group') {
474
- <div class="p-3" role="group" [attr.aria-labelledby]="headingId()">
544
+ <div role="group" [attr.aria-labelledby]="headingId()">
475
545
  @if (!compact()) {
476
- <div class="sticky top-0 z-10 bg-background py-3 text-muted-foreground">
546
+ <div class="sticky top-0 z-10 p-3 text-muted-foreground backdrop-blur-3xl bg-transparent">
477
547
  <div [id]="headingId()" [class]="cn('ui-nav-heading text-muted-foreground', item().classes?.title)">
478
548
  {{ item().title }}
479
549
  </div>
@@ -493,7 +563,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
493
563
  type="button"
494
564
  [class]="
495
565
  cn(
496
- 'ui-nav-text group/ni flex w-full items-center gap-3 rounded-md px-3 py-2 text-foreground/80 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
566
+ 'ui-nav-text group/ni flex w-full items-center gap-3 rounded-full p-3 text-foreground/80 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
497
567
  isTrailActive() && 'text-primary',
498
568
  item().classes?.wrapper
499
569
  )
@@ -506,7 +576,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
506
576
  [matTooltipDisabled]="!compact()"
507
577
  (click)="toggleGroup()">
508
578
  @if (collapsableItem().icon) {
509
- <ui-nav-icon [name]="collapsableItem().icon!" [size]="18" [class]="item().classes?.icon ?? ''" />
579
+ <span
580
+ [class]="
581
+ cn(
582
+ 'inline-flex shrink-0 items-center justify-center',
583
+ open && 'h-7 w-7 rounded-full border border-brand'
584
+ )
585
+ ">
586
+ <ui-nav-icon [name]="collapsableItem().icon!" [size]="18" [class]="item().classes?.icon ?? ''" />
587
+ </span>
510
588
  }
511
589
  @if (!compact()) {
512
590
  <span [class]="cn('flex-1 truncate text-left', item().classes?.title)">
@@ -522,7 +600,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
522
600
  }
523
601
  </button>
524
602
  @if (!compact() && open) {
525
- <div [id]="id + '-panel'" role="region" class="ml-3 mt-0.5 flex flex-col gap-0.5 border-l border-border pl-3">
603
+ <div
604
+ [id]="id + '-panel'"
605
+ role="region"
606
+ class="relative ml-6.5 mt-0.5 flex flex-col gap-0.5 pl-3 before:absolute before:bottom-0 before:left-0 before:-top-3.5 before:w-px before:bg-border before:content-['']">
526
607
  @for (child of collapsableItem().children; track child.id) {
527
608
  <ui-nav-item [item]="child" [level]="level() + 1" [compact]="false" />
528
609
  }
@@ -693,6 +774,7 @@ class SidebarComponent {
693
774
  return explicit.length > 0 ? explicit : this.nav.getItems(this.navigationId())();
694
775
  }, ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
695
776
  hovered = signal(false, ...(ngDevMode ? [{ debugName: "hovered" }] : /* istanbul ignore next */ []));
777
+ suppressHoverUntilLeave = signal(false, ...(ngDevMode ? [{ debugName: "suppressHoverUntilLeave" }] : /* istanbul ignore next */ []));
696
778
  drawerTpl = viewChild.required('drawerTpl');
697
779
  drawerRef = null;
698
780
  focusTrap = null;
@@ -702,8 +784,19 @@ class SidebarComponent {
702
784
  initialValue: false,
703
785
  });
704
786
  isMobile = computed(() => this.autoMobile() && this.isMobileMedia(), ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
705
- isExpanded = computed(() => this.appearance() === 'default' || this.hovered(), ...(ngDevMode ? [{ debugName: "isExpanded" }] : /* istanbul ignore next */ []));
706
- isCompact = computed(() => !this.isMobile() && this.appearance() === 'thin' && !this.hovered(), ...(ngDevMode ? [{ debugName: "isCompact" }] : /* istanbul ignore next */ []));
787
+ resolvedCollapsed = computed(() => {
788
+ if (this.nav.hasStoredSidebarCollapse()) {
789
+ return this.nav.collapsed();
790
+ }
791
+ return this.appearance() === 'thin';
792
+ }, ...(ngDevMode ? [{ debugName: "resolvedCollapsed" }] : /* istanbul ignore next */ []));
793
+ resolvedAppearance = computed(() => {
794
+ return this.resolvedCollapsed() ? 'thin' : 'default';
795
+ }, ...(ngDevMode ? [{ debugName: "resolvedAppearance" }] : /* istanbul ignore next */ []));
796
+ hoverActive = computed(() => this.hovered() && !this.suppressHoverUntilLeave(), ...(ngDevMode ? [{ debugName: "hoverActive" }] : /* istanbul ignore next */ []));
797
+ isExpanded = computed(() => !this.resolvedCollapsed() || this.hoverActive(), ...(ngDevMode ? [{ debugName: "isExpanded" }] : /* istanbul ignore next */ []));
798
+ isCompact = computed(() => !this.isMobile() && this.resolvedCollapsed() && !this.hoverActive(), ...(ngDevMode ? [{ debugName: "isCompact" }] : /* istanbul ignore next */ []));
799
+ toggleButtonLabel = computed(() => this.resolvedCollapsed() ? 'Expand sidebar' : 'Collapse sidebar', ...(ngDevMode ? [{ debugName: "toggleButtonLabel" }] : /* istanbul ignore next */ []));
707
800
  constructor() {
708
801
  // Auto-register items ke service untuk active trail (hanya jika input non-kosong).
709
802
  effect(() => {
@@ -723,24 +816,64 @@ class SidebarComponent {
723
816
  this.destroyRef.onDestroy(() => this.closeDrawer());
724
817
  }
725
818
  hostClasses = computed(() => {
726
- const base = ['relative flex shrink-0 bg-background text-foreground', 'transition-[width] duration-200 ease-out'];
727
- const appearance = this.appearance();
728
- if (appearance === 'thin')
819
+ const base = ['relative flex shrink-0 text-foreground', 'transition-[width] duration-200 ease-out'];
820
+ if (this.resolvedCollapsed())
729
821
  base.push('w-16');
730
822
  else
731
823
  base.push('[width:17.5rem]');
732
824
  if (this.position() === 'right')
733
- base.push('border-l');
734
- else
735
- base.push('border-r');
736
- base.push('border-border');
825
+ base.push('border-l', 'border-brand');
737
826
  return [...base, this.class()].join(' ');
738
827
  }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
828
+ headerClasses = computed(() => {
829
+ const base = ['flex h-12 items-center gap-2 border-b border-brand'];
830
+ if (this.isCompact()) {
831
+ base.push('justify-center px-2');
832
+ }
833
+ return base.join(' ');
834
+ }, ...(ngDevMode ? [{ debugName: "headerClasses" }] : /* istanbul ignore next */ []));
835
+ headerSlotClasses = computed(() => {
836
+ const base = ['min-w-0'];
837
+ if (this.isCompact()) {
838
+ base.push('flex flex-1 items-center justify-center overflow-hidden', '[&>[sidebar-header]>*]:mx-auto', '[&>[sidebar-header]>*]:max-w-full', '[&>[sidebar-header]>*]:shrink-0');
839
+ }
840
+ else {
841
+ base.push('flex min-w-0 flex-1 items-center');
842
+ }
843
+ return base.join(' ');
844
+ }, ...(ngDevMode ? [{ debugName: "headerSlotClasses" }] : /* istanbul ignore next */ []));
845
+ navClasses = computed(() => {
846
+ const base = ['flex-1 overflow-y-auto overflow-x-hidden'];
847
+ if (this.isCompact()) {
848
+ base.push('px-2', '[&_.ui-nav-text]:mx-auto', '[&_.ui-nav-text]:w-10', '[&_.ui-nav-text]:justify-center', '[&_.ui-nav-text]:px-0');
849
+ }
850
+ return base.join(' ');
851
+ }, ...(ngDevMode ? [{ debugName: "navClasses" }] : /* istanbul ignore next */ []));
852
+ footerClasses = computed(() => {
853
+ const base = ['h-12 border-t border-brand'];
854
+ if (this.isCompact()) {
855
+ base.push('flex items-center justify-center px-2');
856
+ }
857
+ return base.join(' ');
858
+ }, ...(ngDevMode ? [{ debugName: "footerClasses" }] : /* istanbul ignore next */ []));
859
+ footerSlotClasses = computed(() => {
860
+ const base = ['h-full'];
861
+ if (this.isCompact()) {
862
+ base.push('flex h-full items-center justify-center overflow-hidden', '[&>[sidebar-footer]>*]:mx-auto', '[&>[sidebar-footer]>*]:w-auto', '[&>[sidebar-footer]>*]:max-w-full', '[&>[sidebar-footer]>*]:justify-center', '[&>[sidebar-footer]>*]:gap-0', '[&>[sidebar-footer]>*]:px-0', '[&>[sidebar-footer]>*>*:first-child]:mx-auto', '[&>[sidebar-footer]>*>*:nth-child(n+2)]:hidden');
863
+ }
864
+ else {
865
+ base.push('w-full');
866
+ }
867
+ return base.join(' ');
868
+ }, ...(ngDevMode ? [{ debugName: "footerSlotClasses" }] : /* istanbul ignore next */ []));
739
869
  innerClasses = computed(() => {
740
- const overlayActive = this.appearance() === 'thin' && this.hovered();
870
+ const overlayActive = this.resolvedCollapsed() && this.hoverActive();
741
871
  const base = ['flex h-full flex-col transition-[width] duration-200 ease-out'];
872
+ if (this.resolvedCollapsed()) {
873
+ base.push('bg-background');
874
+ }
742
875
  if (overlayActive) {
743
- base.push('absolute inset-y-0 z-30 bg-background shadow-xl [width:17.5rem]', this.position() === 'right' ? 'right-0 border-l border-border' : 'left-0 border-r border-border');
876
+ base.push('absolute inset-y-0 z-30 shadow-xl [width:17.5rem]', this.position() === 'right' ? 'right-0 border-l border-brand' : 'left-0 border-r border-brand');
744
877
  }
745
878
  else {
746
879
  base.push('w-full');
@@ -748,16 +881,25 @@ class SidebarComponent {
748
881
  return base.join(' ');
749
882
  }, ...(ngDevMode ? [{ debugName: "innerClasses" }] : /* istanbul ignore next */ []));
750
883
  onHoverEnter() {
751
- if (this.appearance() === 'thin' && !this.isMobile())
752
- this.hovered.set(true);
884
+ if (!this.resolvedCollapsed() || this.isMobile() || this.suppressHoverUntilLeave())
885
+ return;
886
+ this.hovered.set(true);
753
887
  }
754
888
  onHoverLeave() {
755
- if (this.appearance() === 'thin')
889
+ this.suppressHoverUntilLeave.set(false);
890
+ if (this.resolvedCollapsed())
756
891
  this.hovered.set(false);
757
892
  }
893
+ toggleAppearance(event) {
894
+ event.stopPropagation();
895
+ const nextCollapsed = !this.resolvedCollapsed();
896
+ this.nav.setCollapsed(nextCollapsed);
897
+ this.hovered.set(false);
898
+ this.suppressHoverUntilLeave.set(nextCollapsed);
899
+ }
758
900
  /** Touch fallback: tap pada strip thin (ketika belum expanded) untuk expand. */
759
901
  onHostClick(event) {
760
- if (this.appearance() !== 'thin' || this.isMobile())
902
+ if (!this.resolvedCollapsed() || this.isMobile())
761
903
  return;
762
904
  if (this.hovered())
763
905
  return;
@@ -782,7 +924,7 @@ class SidebarComponent {
782
924
  hasBackdrop: true,
783
925
  backdropClass: 'cdk-overlay-dark-backdrop',
784
926
  scrollStrategy: this.overlay.scrollStrategies.block(),
785
- panelClass: ['ui-sidebar-drawer'],
927
+ panelClass: ['sidebar-drawer'],
786
928
  });
787
929
  const portal = new TemplatePortal(this.drawerTpl(), this.vcr);
788
930
  const viewRef = this.drawerRef.attach(portal);
@@ -810,25 +952,41 @@ class SidebarComponent {
810
952
  this.previouslyFocused?.focus?.();
811
953
  this.previouslyFocused = null;
812
954
  }
813
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
814
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: SidebarComponent, isStandalone: true, selector: "ui-sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, navigationId: { classPropertyName: "navigationId", publicName: "navigationId", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, autoMobile: { classPropertyName: "autoMobile", publicName: "autoMobile", isSignal: true, isRequired: false, transformFunction: null }, autoRegister: { classPropertyName: "autoRegister", publicName: "autoRegister", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, listeners: { "mouseenter": "onHoverEnter()", "mouseleave": "onHoverLeave()", "focusin": "onHoverEnter()", "click": "onHostClick($event)" }, properties: { "attr.aria-label": "ariaLabel()", "class": "hostClasses()", "attr.data-appearance": "appearance()", "attr.data-position": "position()", "attr.data-expanded": "isExpanded()", "hidden": "isMobile()" } }, viewQueries: [{ propertyName: "drawerTpl", first: true, predicate: ["drawerTpl"], descendants: true, isSignal: true }], ngImport: i0, template: `
955
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
956
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: SidebarComponent, isStandalone: true, selector: "sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, navigationId: { classPropertyName: "navigationId", publicName: "navigationId", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, autoMobile: { classPropertyName: "autoMobile", publicName: "autoMobile", isSignal: true, isRequired: false, transformFunction: null }, autoRegister: { classPropertyName: "autoRegister", publicName: "autoRegister", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, listeners: { "mouseenter": "onHoverEnter()", "mouseleave": "onHoverLeave()", "focusin": "onHoverEnter()", "click": "onHostClick($event)" }, properties: { "attr.aria-label": "ariaLabel()", "class": "hostClasses()", "attr.data-appearance": "resolvedAppearance()", "attr.data-position": "position()", "attr.data-expanded": "isExpanded()", "hidden": "isMobile()" } }, viewQueries: [{ propertyName: "drawerTpl", first: true, predicate: ["drawerTpl"], descendants: true, isSignal: true }], ngImport: i0, template: `
815
957
  <div [class]="innerClasses()">
816
958
  <ng-container [ngTemplateOutlet]="body" />
817
959
  </div>
818
960
 
819
961
  <ng-template #body>
820
962
  @if (header()) {
821
- <div class="flex h-12 items-center gap-2 border-b border-border">
822
- <ng-content select="[ui-sidebar-header]" />
963
+ <div [class]="headerClasses()">
964
+ <div [class]="headerSlotClasses()">
965
+ <ng-content select="[sidebar-header]" />
966
+ </div>
967
+ @if (!isMobile() && !isCompact()) {
968
+ <button
969
+ type="button"
970
+ data-sidebar-toggle
971
+ class="ml-auto mr-2 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
972
+ [attr.aria-label]="toggleButtonLabel()"
973
+ [attr.title]="toggleButtonLabel()"
974
+ [attr.aria-pressed]="resolvedAppearance() === 'thin'"
975
+ (click)="toggleAppearance($event)">
976
+ <ui-nav-icon name="view_sidebar" [size]="18" class="text-current" />
977
+ </button>
978
+ }
823
979
  </div>
824
980
  }
825
- <nav class="flex-1 overflow-y-auto overflow-x-hidden">
981
+ <nav [class]="navClasses()">
826
982
  @for (item of resolvedItems(); track item.id) {
827
983
  <ui-nav-item [item]="item" [compact]="isCompact()" />
828
984
  }
829
985
  </nav>
830
- <div class="border-t border-border h-12">
831
- <ng-content select="[ui-sidebar-footer]" />
986
+ <div [class]="footerClasses()">
987
+ <div [class]="footerSlotClasses()">
988
+ <ng-content select="[sidebar-footer]" />
989
+ </div>
832
990
  </div>
833
991
  </ng-template>
834
992
 
@@ -837,25 +995,25 @@ class SidebarComponent {
837
995
  role="dialog"
838
996
  aria-modal="true"
839
997
  [attr.aria-label]="ariaLabel()"
840
- class="flex h-full w-72 max-w-[85vw] flex-col border-border bg-background text-foreground shadow-xl"
998
+ class="flex h-full w-72 max-w-[85vw] flex-col text-foreground shadow-xl"
841
999
  [class.border-l]="position() === 'right'"
842
- [class.border-r]="position() !== 'right'">
1000
+ [class.border-brand]="position() === 'right'">
843
1001
  <ng-container [ngTemplateOutlet]="body" />
844
1002
  </div>
845
1003
  </ng-template>
846
- `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UiNavItemComponent, selector: "ui-nav-item", inputs: ["item", "level", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1004
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UiNavIconComponent, selector: "ui-nav-icon", inputs: ["name", "class", "size"] }, { kind: "component", type: UiNavItemComponent, selector: "ui-nav-item", inputs: ["item", "level", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
847
1005
  }
848
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SidebarComponent, decorators: [{
1006
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SidebarComponent, decorators: [{
849
1007
  type: Component,
850
1008
  args: [{
851
- selector: 'ui-sidebar',
1009
+ selector: 'sidebar',
852
1010
  changeDetection: ChangeDetectionStrategy.OnPush,
853
- imports: [NgTemplateOutlet, UiNavItemComponent],
1011
+ imports: [NgTemplateOutlet, UiNavIconComponent, UiNavItemComponent],
854
1012
  host: {
855
1013
  role: 'navigation',
856
1014
  '[attr.aria-label]': 'ariaLabel()',
857
1015
  '[class]': 'hostClasses()',
858
- '[attr.data-appearance]': 'appearance()',
1016
+ '[attr.data-appearance]': 'resolvedAppearance()',
859
1017
  '[attr.data-position]': 'position()',
860
1018
  '[attr.data-expanded]': 'isExpanded()',
861
1019
  '[hidden]': 'isMobile()',
@@ -871,17 +1029,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
871
1029
 
872
1030
  <ng-template #body>
873
1031
  @if (header()) {
874
- <div class="flex h-12 items-center gap-2 border-b border-border">
875
- <ng-content select="[ui-sidebar-header]" />
1032
+ <div [class]="headerClasses()">
1033
+ <div [class]="headerSlotClasses()">
1034
+ <ng-content select="[sidebar-header]" />
1035
+ </div>
1036
+ @if (!isMobile() && !isCompact()) {
1037
+ <button
1038
+ type="button"
1039
+ data-sidebar-toggle
1040
+ class="ml-auto mr-2 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
1041
+ [attr.aria-label]="toggleButtonLabel()"
1042
+ [attr.title]="toggleButtonLabel()"
1043
+ [attr.aria-pressed]="resolvedAppearance() === 'thin'"
1044
+ (click)="toggleAppearance($event)">
1045
+ <ui-nav-icon name="view_sidebar" [size]="18" class="text-current" />
1046
+ </button>
1047
+ }
876
1048
  </div>
877
1049
  }
878
- <nav class="flex-1 overflow-y-auto overflow-x-hidden">
1050
+ <nav [class]="navClasses()">
879
1051
  @for (item of resolvedItems(); track item.id) {
880
1052
  <ui-nav-item [item]="item" [compact]="isCompact()" />
881
1053
  }
882
1054
  </nav>
883
- <div class="border-t border-border h-12">
884
- <ng-content select="[ui-sidebar-footer]" />
1055
+ <div [class]="footerClasses()">
1056
+ <div [class]="footerSlotClasses()">
1057
+ <ng-content select="[sidebar-footer]" />
1058
+ </div>
885
1059
  </div>
886
1060
  </ng-template>
887
1061
 
@@ -890,9 +1064,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
890
1064
  role="dialog"
891
1065
  aria-modal="true"
892
1066
  [attr.aria-label]="ariaLabel()"
893
- class="flex h-full w-72 max-w-[85vw] flex-col border-border bg-background text-foreground shadow-xl"
1067
+ class="flex h-full w-72 max-w-[85vw] flex-col text-foreground shadow-xl"
894
1068
  [class.border-l]="position() === 'right'"
895
- [class.border-r]="position() !== 'right'">
1069
+ [class.border-brand]="position() === 'right'">
896
1070
  <ng-container [ngTemplateOutlet]="body" />
897
1071
  </div>
898
1072
  </ng-template>
@@ -902,10 +1076,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
902
1076
 
903
1077
  /**
904
1078
  * Horizontal navigation (topbar) — shadcn-styled.
905
- *
906
- * Variants:
907
- * - `default`: horizontal list; item `collapsable` buka dropdown
908
- * - `megamenu`: item `mega` buka panel full-width multi-kolom
909
1079
  */
910
1080
  class TopbarComponent {
911
1081
  nav = inject(NavigationService);
@@ -924,7 +1094,6 @@ class TopbarComponent {
924
1094
  /** Tampilkan hamburger di `< md` yang men-toggle mobile drawer sidebar. */
925
1095
  showHamburger = input(true, ...(ngDevMode ? [{ debugName: "showHamburger" }] : /* istanbul ignore next */ []));
926
1096
  hamburgerLabel = input('Open navigation', ...(ngDevMode ? [{ debugName: "hamburgerLabel" }] : /* istanbul ignore next */ []));
927
- topbarHeight = 'var(--layout-topbar-height)';
928
1097
  /** Resolved items: input jika disediakan, fallback ke registry NavigationService. */
929
1098
  resolvedItems = computed(() => {
930
1099
  const explicit = this.items();
@@ -933,7 +1102,6 @@ class TopbarComponent {
933
1102
  openId = signal(null, ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
934
1103
  active = null;
935
1104
  dropdownTpl = viewChild.required('dropdownTpl');
936
- megaTpl = viewChild.required('megaTpl');
937
1105
  constructor() {
938
1106
  effect(() => {
939
1107
  const explicit = this.items();
@@ -943,11 +1111,13 @@ class TopbarComponent {
943
1111
  this.destroyRef.onDestroy(() => this.closeAll());
944
1112
  }
945
1113
  hostClasses = computed(() => {
946
- return [
947
- 'sticky top-0 z-20 flex w-full items-center border-b border-border bg-background text-foreground',
948
- this.class(),
949
- ].join(' ');
1114
+ return ['sticky top-0 z-20 flex h-11 w-full items-center', this.class()].join(' ');
950
1115
  }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
1116
+ endSlotClasses = computed(() => {
1117
+ const base = ['flex h-full shrink-0 items-center justify-end gap-2 overflow-hidden'];
1118
+ base.push('[&>[topbar-end]]:flex', '[&>[topbar-end]]:min-w-0', '[&>[topbar-end]]:max-w-full', '[&>[topbar-end]]:items-center', '[&>[topbar-end]]:justify-end', '[&>[topbar-end]]:gap-2', '[&>[topbar-end]>*]:w-auto', '[&>[topbar-end]>*]:max-w-full', '[&>[topbar-end]>*]:justify-end', '[&>[topbar-end]>*]:gap-0', '[&>[topbar-end]>*]:px-2', '[&>[topbar-end]>*>*:first-child]:ml-auto', '[&>[topbar-end]>*>*:nth-child(n+2)]:hidden');
1119
+ return base.join(' ');
1120
+ }, ...(ngDevMode ? [{ debugName: "endSlotClasses" }] : /* istanbul ignore next */ []));
951
1121
  asBasic(i) {
952
1122
  return i;
953
1123
  }
@@ -957,36 +1127,9 @@ class TopbarComponent {
957
1127
  asGroup(i) {
958
1128
  return i;
959
1129
  }
960
- asMega(i) {
961
- return i;
962
- }
963
1130
  isItemActive(id) {
964
1131
  return this.nav.isActive(id);
965
1132
  }
966
- megaGridClasses(columns) {
967
- const c = Math.min(Math.max(columns ?? 4, 1), 6);
968
- const classes = ['grid', 'gap-6'];
969
- switch (c) {
970
- case 1:
971
- classes.push('grid-cols-1');
972
- break;
973
- case 2:
974
- classes.push('md:grid-cols-2');
975
- break;
976
- case 3:
977
- classes.push('md:grid-cols-3');
978
- break;
979
- case 5:
980
- classes.push('md:grid-cols-5');
981
- break;
982
- case 6:
983
- classes.push('md:grid-cols-6');
984
- break;
985
- default:
986
- classes.push('md:grid-cols-4');
987
- }
988
- return classes.join(' ');
989
- }
990
1133
  toggleDropdown(trigger, item) {
991
1134
  if (this.openId() === item.id)
992
1135
  this.closeAll();
@@ -997,67 +1140,36 @@ class TopbarComponent {
997
1140
  if (this.openId() === item.id)
998
1141
  return;
999
1142
  this.closeAll();
1000
- this.attach(trigger, item, this.dropdownTpl(), /*fullWidth*/ false);
1143
+ this.attach(trigger, item, this.dropdownTpl());
1001
1144
  }
1002
- toggleMega(trigger, item) {
1003
- if (this.openId() === item.id)
1004
- this.closeAll();
1005
- else
1006
- this.openMega(trigger, item);
1007
- }
1008
- openMega(trigger, item) {
1009
- if (this.openId() === item.id)
1010
- return;
1011
- this.closeAll();
1012
- this.attach(trigger, item, this.megaTpl(), /*fullWidth*/ true);
1013
- }
1014
- attach(trigger, item, tpl, fullWidth) {
1145
+ attach(trigger, item, tpl) {
1015
1146
  const strategy = this.overlay
1016
1147
  .position()
1017
1148
  .flexibleConnectedTo(trigger)
1018
1149
  .withFlexibleDimensions(false)
1019
1150
  .withPush(false)
1020
- .withPositions(fullWidth
1021
- ? [
1022
- {
1023
- originX: 'center',
1024
- originY: 'bottom',
1025
- overlayX: 'center',
1026
- overlayY: 'top',
1027
- offsetY: 4,
1028
- },
1029
- {
1030
- originX: 'center',
1031
- originY: 'top',
1032
- overlayX: 'center',
1033
- overlayY: 'bottom',
1034
- offsetY: -4,
1035
- },
1036
- ]
1037
- : [
1038
- {
1039
- originX: 'start',
1040
- originY: 'bottom',
1041
- overlayX: 'start',
1042
- overlayY: 'top',
1043
- offsetY: 4,
1044
- },
1045
- {
1046
- originX: 'start',
1047
- originY: 'top',
1048
- overlayX: 'start',
1049
- overlayY: 'bottom',
1050
- offsetY: -4,
1051
- },
1052
- ]);
1151
+ .withPositions([
1152
+ {
1153
+ originX: 'start',
1154
+ originY: 'bottom',
1155
+ overlayX: 'start',
1156
+ overlayY: 'top',
1157
+ offsetY: 4,
1158
+ },
1159
+ {
1160
+ originX: 'start',
1161
+ originY: 'top',
1162
+ overlayX: 'start',
1163
+ overlayY: 'bottom',
1164
+ offsetY: -4,
1165
+ },
1166
+ ]);
1053
1167
  const ref = this.overlay.create({
1054
1168
  positionStrategy: strategy,
1055
1169
  scrollStrategy: this.overlay.scrollStrategies.reposition(),
1056
1170
  hasBackdrop: true,
1057
1171
  backdropClass: 'cdk-overlay-transparent-backdrop',
1058
- panelClass: fullWidth ? ['ui-mega-panel'] : ['ui-dropdown-panel'],
1059
- width: fullWidth ? '100vw' : undefined,
1060
- maxWidth: fullWidth ? '100vw' : undefined,
1172
+ panelClass: ['ui-dropdown-panel'],
1061
1173
  });
1062
1174
  const portal = new TemplatePortal(tpl, this.vcr, { $implicit: item });
1063
1175
  ref.attach(portal);
@@ -1107,7 +1219,7 @@ class TopbarComponent {
1107
1219
  triggers[nextIndex].focus();
1108
1220
  }
1109
1221
  }
1110
- /** Arrow-key navigation dalam dropdown/mega panel. */
1222
+ /** Arrow-key navigation dalam dropdown panel. */
1111
1223
  onPanelKeydown(event) {
1112
1224
  const key = event.key;
1113
1225
  if (key !== 'ArrowDown' &&
@@ -1148,10 +1260,10 @@ class TopbarComponent {
1148
1260
  const items = this.collectPanelFocusables(panel);
1149
1261
  items[0]?.focus();
1150
1262
  }
1151
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TopbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1152
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: TopbarComponent, isStandalone: true, selector: "ui-topbar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, navigationId: { classPropertyName: "navigationId", publicName: "navigationId", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, autoRegister: { classPropertyName: "autoRegister", publicName: "autoRegister", isSignal: true, isRequired: false, transformFunction: null }, showHamburger: { classPropertyName: "showHamburger", publicName: "showHamburger", isSignal: true, isRequired: false, transformFunction: null }, hamburgerLabel: { classPropertyName: "hamburgerLabel", publicName: "hamburgerLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "ariaLabel()", "class": "hostClasses()", "attr.data-appearance": "appearance()", "style.height": "topbarHeight" } }, viewQueries: [{ propertyName: "dropdownTpl", first: true, predicate: ["dropdownTpl"], descendants: true, isSignal: true }, { propertyName: "megaTpl", first: true, predicate: ["megaTpl"], descendants: true, isSignal: true }], ngImport: i0, template: `
1263
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: TopbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1264
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: TopbarComponent, isStandalone: true, selector: "topbar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, navigationId: { classPropertyName: "navigationId", publicName: "navigationId", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, autoRegister: { classPropertyName: "autoRegister", publicName: "autoRegister", isSignal: true, isRequired: false, transformFunction: null }, showHamburger: { classPropertyName: "showHamburger", publicName: "showHamburger", isSignal: true, isRequired: false, transformFunction: null }, hamburgerLabel: { classPropertyName: "hamburgerLabel", publicName: "hamburgerLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "ariaLabel()", "class": "hostClasses()", "attr.data-appearance": "appearance()" } }, viewQueries: [{ propertyName: "dropdownTpl", first: true, predicate: ["dropdownTpl"], descendants: true, isSignal: true }], ngImport: i0, template: `
1153
1265
  <div class="flex h-full w-full items-center gap-3 px-1">
1154
- <div data-ui-topbar-slot="start" class="flex shrink-0 items-center gap-2">
1266
+ <div data-topbar-slot="start" class="flex shrink-0 items-center gap-2">
1155
1267
  @if (showHamburger()) {
1156
1268
  <button
1157
1269
  type="button"
@@ -1162,10 +1274,10 @@ class TopbarComponent {
1162
1274
  <ui-nav-icon name="menu" [size]="18" />
1163
1275
  </button>
1164
1276
  }
1165
- <ng-content select="[ui-topbar-start]" />
1277
+ <ng-content select="[topbar-start]" />
1166
1278
  </div>
1167
1279
 
1168
- <div data-ui-topbar-slot="nav" class="flex min-w-0 flex-1 items-center justify-center">
1280
+ <div data-topbar-slot="nav" class="flex min-w-0 flex-1 items-center justify-center">
1169
1281
  <ul
1170
1282
  class="flex min-w-0 flex-1 items-center justify-center gap-1"
1171
1283
  role="menubar"
@@ -1228,25 +1340,6 @@ class TopbarComponent {
1228
1340
  <ui-nav-icon name="expand_more" [size]="18" />
1229
1341
  </button>
1230
1342
  }
1231
- @case ('mega') {
1232
- @let mega = asMega(item);
1233
- <button
1234
- #trigger
1235
- type="button"
1236
- role="menuitem"
1237
- class="ui-nav-text inline-flex items-center gap-2 rounded-md px-3 py-2 text-foreground/80 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1238
- [class.text-primary]="isItemActive(mega.id)"
1239
- [attr.aria-expanded]="openId() === mega.id"
1240
- [attr.aria-haspopup]="'menu'"
1241
- (click)="toggleMega(trigger, item)"
1242
- (mouseenter)="openMega(trigger, item)">
1243
- @if (mega.icon) {
1244
- <ui-nav-icon [name]="mega.icon" [size]="18" />
1245
- }
1246
- <span>{{ mega.title }}</span>
1247
- <ui-nav-icon name="expand_more" [size]="18" />
1248
- </button>
1249
- }
1250
1343
  @default {
1251
1344
  <span class="ui-nav-heading px-3 py-2 text-muted-foreground">
1252
1345
  {{ item.title }}
@@ -1258,51 +1351,25 @@ class TopbarComponent {
1258
1351
  </ul>
1259
1352
  </div>
1260
1353
 
1261
- <div data-ui-topbar-slot="end" class="flex shrink-0 items-center justify-end gap-2">
1262
- <ng-content select="[ui-topbar-end]" />
1354
+ <div data-topbar-slot="end" [class]="endSlotClasses()">
1355
+ <ng-content select="[topbar-end]" />
1263
1356
  </div>
1264
1357
  </div>
1265
1358
 
1266
1359
  <!-- Dropdown template -->
1267
1360
  <ng-template #dropdownTpl let-item>
1268
- <div
1269
- role="menu"
1270
- class="min-w-56 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
1271
- (keydown)="onPanelKeydown($event)">
1361
+ <div role="menu" class="min-w-56 border border-brand bg-background/95 p-1" (keydown)="onPanelKeydown($event)">
1272
1362
  @for (child of item.children; track child.id) {
1273
1363
  <ui-nav-item [item]="child" />
1274
1364
  }
1275
1365
  </div>
1276
1366
  </ng-template>
1277
-
1278
- <!-- Mega panel template -->
1279
- <ng-template #megaTpl let-item>
1280
- <div
1281
- role="menu"
1282
- class="w-screen max-w-[min(90vw,72rem)] rounded-md border border-border bg-popover p-6 text-popover-foreground shadow-lg"
1283
- (keydown)="onPanelKeydown($event)">
1284
- <div [class]="megaGridClasses(item.columns)">
1285
- @for (col of item.children; track col.id) {
1286
- <div>
1287
- <div class="ui-nav-heading mb-2 text-muted-foreground">
1288
- {{ col.title }}
1289
- </div>
1290
- <div class="flex flex-col gap-0.5">
1291
- @for (leaf of col.children ?? []; track leaf.id) {
1292
- <ui-nav-item [item]="leaf" />
1293
- }
1294
- </div>
1295
- </div>
1296
- }
1297
- </div>
1298
- </div>
1299
- </ng-template>
1300
1367
  `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: UiNavIconComponent, selector: "ui-nav-icon", inputs: ["name", "class", "size"] }, { kind: "component", type: UiNavItemComponent, selector: "ui-nav-item", inputs: ["item", "level", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1301
1368
  }
1302
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TopbarComponent, decorators: [{
1369
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: TopbarComponent, decorators: [{
1303
1370
  type: Component,
1304
1371
  args: [{
1305
- selector: 'ui-topbar',
1372
+ selector: 'topbar',
1306
1373
  changeDetection: ChangeDetectionStrategy.OnPush,
1307
1374
  imports: [RouterLink, RouterLinkActive, UiNavIconComponent, UiNavItemComponent],
1308
1375
  host: {
@@ -1310,11 +1377,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1310
1377
  '[attr.aria-label]': 'ariaLabel()',
1311
1378
  '[class]': 'hostClasses()',
1312
1379
  '[attr.data-appearance]': 'appearance()',
1313
- '[style.height]': 'topbarHeight',
1314
1380
  },
1315
1381
  template: `
1316
1382
  <div class="flex h-full w-full items-center gap-3 px-1">
1317
- <div data-ui-topbar-slot="start" class="flex shrink-0 items-center gap-2">
1383
+ <div data-topbar-slot="start" class="flex shrink-0 items-center gap-2">
1318
1384
  @if (showHamburger()) {
1319
1385
  <button
1320
1386
  type="button"
@@ -1325,10 +1391,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1325
1391
  <ui-nav-icon name="menu" [size]="18" />
1326
1392
  </button>
1327
1393
  }
1328
- <ng-content select="[ui-topbar-start]" />
1394
+ <ng-content select="[topbar-start]" />
1329
1395
  </div>
1330
1396
 
1331
- <div data-ui-topbar-slot="nav" class="flex min-w-0 flex-1 items-center justify-center">
1397
+ <div data-topbar-slot="nav" class="flex min-w-0 flex-1 items-center justify-center">
1332
1398
  <ul
1333
1399
  class="flex min-w-0 flex-1 items-center justify-center gap-1"
1334
1400
  role="menubar"
@@ -1391,25 +1457,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1391
1457
  <ui-nav-icon name="expand_more" [size]="18" />
1392
1458
  </button>
1393
1459
  }
1394
- @case ('mega') {
1395
- @let mega = asMega(item);
1396
- <button
1397
- #trigger
1398
- type="button"
1399
- role="menuitem"
1400
- class="ui-nav-text inline-flex items-center gap-2 rounded-md px-3 py-2 text-foreground/80 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1401
- [class.text-primary]="isItemActive(mega.id)"
1402
- [attr.aria-expanded]="openId() === mega.id"
1403
- [attr.aria-haspopup]="'menu'"
1404
- (click)="toggleMega(trigger, item)"
1405
- (mouseenter)="openMega(trigger, item)">
1406
- @if (mega.icon) {
1407
- <ui-nav-icon [name]="mega.icon" [size]="18" />
1408
- }
1409
- <span>{{ mega.title }}</span>
1410
- <ui-nav-icon name="expand_more" [size]="18" />
1411
- </button>
1412
- }
1413
1460
  @default {
1414
1461
  <span class="ui-nav-heading px-3 py-2 text-muted-foreground">
1415
1462
  {{ item.title }}
@@ -1421,48 +1468,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1421
1468
  </ul>
1422
1469
  </div>
1423
1470
 
1424
- <div data-ui-topbar-slot="end" class="flex shrink-0 items-center justify-end gap-2">
1425
- <ng-content select="[ui-topbar-end]" />
1471
+ <div data-topbar-slot="end" [class]="endSlotClasses()">
1472
+ <ng-content select="[topbar-end]" />
1426
1473
  </div>
1427
1474
  </div>
1428
1475
 
1429
1476
  <!-- Dropdown template -->
1430
1477
  <ng-template #dropdownTpl let-item>
1431
- <div
1432
- role="menu"
1433
- class="min-w-56 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
1434
- (keydown)="onPanelKeydown($event)">
1478
+ <div role="menu" class="min-w-56 border border-brand bg-background/95 p-1" (keydown)="onPanelKeydown($event)">
1435
1479
  @for (child of item.children; track child.id) {
1436
1480
  <ui-nav-item [item]="child" />
1437
1481
  }
1438
1482
  </div>
1439
1483
  </ng-template>
1440
-
1441
- <!-- Mega panel template -->
1442
- <ng-template #megaTpl let-item>
1443
- <div
1444
- role="menu"
1445
- class="w-screen max-w-[min(90vw,72rem)] rounded-md border border-border bg-popover p-6 text-popover-foreground shadow-lg"
1446
- (keydown)="onPanelKeydown($event)">
1447
- <div [class]="megaGridClasses(item.columns)">
1448
- @for (col of item.children; track col.id) {
1449
- <div>
1450
- <div class="ui-nav-heading mb-2 text-muted-foreground">
1451
- {{ col.title }}
1452
- </div>
1453
- <div class="flex flex-col gap-0.5">
1454
- @for (leaf of col.children ?? []; track leaf.id) {
1455
- <ui-nav-item [item]="leaf" />
1456
- }
1457
- </div>
1458
- </div>
1459
- }
1460
- </div>
1461
- </div>
1462
- </ng-template>
1463
1484
  `,
1464
1485
  }]
1465
- }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], navigationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "navigationId", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], autoRegister: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoRegister", required: false }] }], showHamburger: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHamburger", required: false }] }], hamburgerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hamburgerLabel", required: false }] }], dropdownTpl: [{ type: i0.ViewChild, args: ['dropdownTpl', { isSignal: true }] }], megaTpl: [{ type: i0.ViewChild, args: ['megaTpl', { isSignal: true }] }] } });
1486
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], navigationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "navigationId", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], autoRegister: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoRegister", required: false }] }], showHamburger: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHamburger", required: false }] }], hamburgerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hamburgerLabel", required: false }] }], dropdownTpl: [{ type: i0.ViewChild, args: ['dropdownTpl', { isSignal: true }] }] } });
1466
1487
 
1467
1488
  const DemoNavigationData = [
1468
1489
  {