wally-ui 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/cli.js +8 -5
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/playground/showcase/src/app/app.config.ts +9 -2
  5. package/playground/showcase/src/app/app.routes.server.ts +4 -0
  6. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +164 -31
  7. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +25 -3
  8. package/playground/showcase/src/app/components/ai/ai-prompt-input/ai-prompt-input.html +1 -1
  9. package/playground/showcase/src/app/components/badge/badge.css +0 -0
  10. package/playground/showcase/src/app/components/badge/badge.html +3 -0
  11. package/playground/showcase/src/app/components/badge/badge.ts +24 -0
  12. package/playground/showcase/src/app/components/button/button.html +1 -3
  13. package/playground/showcase/src/app/components/button/button.ts +4 -4
  14. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.css +0 -0
  15. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.html +9 -0
  16. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.spec.ts +23 -0
  17. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.ts +167 -0
  18. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.css +0 -0
  19. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.html +5 -0
  20. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.spec.ts +23 -0
  21. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.ts +10 -0
  22. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.css +0 -0
  23. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.html +6 -0
  24. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.spec.ts +23 -0
  25. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.ts +37 -0
  26. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.css +0 -0
  27. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.html +3 -0
  28. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.spec.ts +23 -0
  29. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.ts +11 -0
  30. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.css +0 -0
  31. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.html +1 -0
  32. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.spec.ts +23 -0
  33. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.ts +11 -0
  34. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.css +0 -0
  35. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.html +1 -0
  36. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.spec.ts +23 -0
  37. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.ts +11 -0
  38. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.css +0 -0
  39. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.html +3 -0
  40. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.spec.ts +23 -0
  41. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.ts +16 -0
  42. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.css +0 -0
  43. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.html +9 -0
  44. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.spec.ts +23 -0
  45. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.ts +31 -0
  46. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.css +0 -0
  47. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.html +13 -0
  48. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.spec.ts +23 -0
  49. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.ts +40 -0
  50. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.spec.ts +16 -0
  51. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.ts +23 -0
  52. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.css +0 -0
  53. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.html +8 -0
  54. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.spec.ts +23 -0
  55. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.ts +55 -0
  56. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.css +0 -0
  57. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.html +3 -0
  58. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.spec.ts +16 -0
  59. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.ts +31 -0
  60. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.ts +69 -0
  61. package/playground/showcase/src/app/components/tooltip/tooltip.ts +195 -80
  62. package/playground/showcase/src/app/pages/documentation/components/components.html +110 -51
  63. package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
  64. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.css +1 -0
  65. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.examples.ts +404 -0
  66. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.html +612 -0
  67. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.ts +127 -0
  68. package/playground/showcase/src/app/pages/documentation/components/tooltip-docs/tooltip-docs.html +0 -4
  69. package/playground/showcase/src/app/pages/home/home.html +10 -6
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuGroup } from './dropdown-menu-group';
4
+
5
+ describe('DropdownMenuGroup', () => {
6
+ let component: DropdownMenuGroup;
7
+ let fixture: ComponentFixture<DropdownMenuGroup>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuGroup]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuGroup);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,10 @@
1
+ import { Component, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-group',
5
+ templateUrl: './dropdown-menu-group.html',
6
+ styleUrl: './dropdown-menu-group.css'
7
+ })
8
+ export class DropdownMenuGroup {
9
+ ariaLabel = input<string>('');
10
+ }
@@ -0,0 +1,6 @@
1
+ <div (click)="handleClick($event)" class="p-1" role="menuitem" tabindex="0">
2
+ <div class="text-base text-[#0a0a0a] hover:bg-neutral-100 dark:hover:bg-neutral-700/50 dark:text-white py-2 px-4 rounded-lg"
3
+ [ngClass]="{'text-neutral-400 dark:!text-white/50 pointer-events-none': disabled(), 'cursor-pointer': !disabled()}">
4
+ <ng-content></ng-content>
5
+ </div>
6
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuItem } from './dropdown-menu-item';
4
+
5
+ describe('DropdownMenuItem', () => {
6
+ let component: DropdownMenuItem;
7
+ let fixture: ComponentFixture<DropdownMenuItem>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuItem]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuItem);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,37 @@
1
+ import { Component, input, InputSignal, output, OutputEmitterRef } from '@angular/core';
2
+
3
+ import { DropdownMenuService } from '../dropdown-menu.service';
4
+ import { CommonModule } from '@angular/common';
5
+
6
+ @Component({
7
+ selector: 'wally-dropdown-menu-item',
8
+ imports: [
9
+ CommonModule
10
+ ],
11
+ templateUrl: './dropdown-menu-item.html',
12
+ styleUrl: './dropdown-menu-item.css'
13
+ })
14
+ export class DropdownMenuItem {
15
+ disabled: InputSignal<boolean> = input<boolean>(false);
16
+
17
+ click: OutputEmitterRef<void> = output<void>();
18
+
19
+ constructor(
20
+ private dropdownMenuService: DropdownMenuService
21
+ ) {}
22
+
23
+ /**
24
+ * Handles click event on menu item.
25
+ * Stops event propagation to prevent double-firing, emits click event, and closes dropdown.
26
+ * Does nothing if item is disabled.
27
+ */
28
+ handleClick(event: MouseEvent): void {
29
+ if (this.disabled()) {
30
+ return;
31
+ }
32
+
33
+ event?.stopPropagation();
34
+ this.click.emit();
35
+ this.dropdownMenuService.close();
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ <div class="px-5 py-4 text-base font-medium text-[#0a0a0a] dark:text-white">
2
+ <ng-content></ng-content>
3
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuLabel } from './dropdown-menu-label';
4
+
5
+ describe('DropdownMenuLabel', () => {
6
+ let component: DropdownMenuLabel;
7
+ let fixture: ComponentFixture<DropdownMenuLabel>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuLabel]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuLabel);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,11 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-label',
5
+ imports: [],
6
+ templateUrl: './dropdown-menu-label.html',
7
+ styleUrl: './dropdown-menu-label.css'
8
+ })
9
+ export class DropdownMenuLabel {
10
+
11
+ }
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuPortal } from './dropdown-menu-portal';
4
+
5
+ describe('DropdownMenuPortal', () => {
6
+ let component: DropdownMenuPortal;
7
+ let fixture: ComponentFixture<DropdownMenuPortal>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuPortal]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuPortal);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,11 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-portal',
5
+ imports: [],
6
+ templateUrl: './dropdown-menu-portal.html',
7
+ styleUrl: './dropdown-menu-portal.css'
8
+ })
9
+ export class DropdownMenuPortal {
10
+
11
+ }
@@ -0,0 +1 @@
1
+ <div class="h-px bg-neutral-300 dark:bg-neutral-700"></div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSeparator } from './dropdown-menu-separator';
4
+
5
+ describe('DropdownMenuSeparator', () => {
6
+ let component: DropdownMenuSeparator;
7
+ let fixture: ComponentFixture<DropdownMenuSeparator>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSeparator]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSeparator);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,11 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-separator',
5
+ imports: [],
6
+ templateUrl: './dropdown-menu-separator.html',
7
+ styleUrl: './dropdown-menu-separator.css'
8
+ })
9
+ export class DropdownMenuSeparator {
10
+
11
+ }
@@ -0,0 +1,3 @@
1
+ <div class="relative">
2
+ <ng-content></ng-content>
3
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSub } from './dropdown-menu-sub';
4
+
5
+ describe('DropdownMenuSub', () => {
6
+ let component: DropdownMenuSub;
7
+ let fixture: ComponentFixture<DropdownMenuSub>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSub]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSub);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,16 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
4
+
5
+ @Component({
6
+ selector: 'wally-dropdown-menu-sub',
7
+ imports: [],
8
+ providers: [
9
+ DropdownMenuSubService
10
+ ],
11
+ templateUrl: './dropdown-menu-sub.html',
12
+ styleUrl: './dropdown-menu-sub.css'
13
+ })
14
+ export class DropdownMenuSub {
15
+
16
+ }
@@ -0,0 +1,9 @@
1
+ @if (subService.isOpen()) {
2
+ <div
3
+ (mouseenter)="onMouseEnter()"
4
+ (mouseleave)="onMouseLeave()"
5
+ [class]="'absolute bg-white dark:bg-[#1b1b1b] rounded-xl shadow-2xl border border-neutral-300 dark:border-neutral-700 min-w-56 z-50 transition-all duration-200 ease-out ' + positionClasses()"
6
+ role="menu">
7
+ <ng-content></ng-content>
8
+ </div>
9
+ }
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSubContent } from './dropdown-menu-sub-content';
4
+
5
+ describe('DropdownMenuSubContent', () => {
6
+ let component: DropdownMenuSubContent;
7
+ let fixture: ComponentFixture<DropdownMenuSubContent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSubContent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSubContent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,31 @@
1
+ import { Component, computed } from '@angular/core';
2
+ import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
3
+
4
+ @Component({
5
+ selector: 'wally-dropdown-menu-sub-content',
6
+ imports: [],
7
+ templateUrl: './dropdown-menu-sub-content.html',
8
+ styleUrl: './dropdown-menu-sub-content.css'
9
+ })
10
+ export class DropdownMenuSubContent {
11
+ constructor(public subService: DropdownMenuSubService) {}
12
+
13
+ positionClasses = computed(() => {
14
+ return 'top-0 left-full';
15
+ });
16
+
17
+ onMouseEnter(): void {
18
+ this.subService.setHoveringContent(true);
19
+ this.subService.open();
20
+ }
21
+
22
+ onMouseLeave(): void {
23
+ this.subService.setHoveringContent(false);
24
+
25
+ setTimeout(() => {
26
+ if (!this.subService.isHoveringContent()) {
27
+ this.subService.close();
28
+ }
29
+ }, 100);
30
+ }
31
+ }
@@ -0,0 +1,13 @@
1
+ <div (mouseenter)="onMouseEnter()" (mouseleave)="onMouseLeave()" class="p-1" role="menuitem" [attr.aria-haspopup]="true"
2
+ [attr.aria-expanded]="subService.isOpen()">
3
+
4
+ <div
5
+ class="flex justify-center items-center text-base text-[#0a0a0a] hover:bg-neutral-100 dark:hover:bg-neutral-700/50 dark:text-white py-2 px-4 rounded-lg cursor-pointer">
6
+ <ng-content></ng-content>
7
+
8
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
9
+ class="size-4 ml-auto text-neutral-500 dark:text-neutral-400">
10
+ <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
11
+ </svg>
12
+ </div>
13
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSubTrigger } from './dropdown-menu-sub-trigger';
4
+
5
+ describe('DropdownMenuSubTrigger', () => {
6
+ let component: DropdownMenuSubTrigger;
7
+ let fixture: ComponentFixture<DropdownMenuSubTrigger>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSubTrigger]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSubTrigger);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,40 @@
1
+ import { Component } from '@angular/core';
2
+ import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
3
+
4
+ @Component({
5
+ selector: 'wally-dropdown-menu-sub-trigger',
6
+ imports: [],
7
+ templateUrl: './dropdown-menu-sub-trigger.html',
8
+ styleUrl: './dropdown-menu-sub-trigger.css'
9
+ })
10
+ export class DropdownMenuSubTrigger {
11
+ private hoverTimeout: any = null;
12
+
13
+ constructor(public subService: DropdownMenuSubService) {}
14
+
15
+ /**
16
+ * Opens submenu after 150ms delay to prevent accidental triggers.
17
+ */
18
+ onMouseEnter(): void {
19
+ this.hoverTimeout = setTimeout(() => {
20
+ this.subService.open();
21
+ }, 150);
22
+ }
23
+
24
+ /**
25
+ * Closes submenu with 300ms delay, allowing user to move mouse to submenu content.
26
+ * Cancels pending open timeout if mouse leaves quickly.
27
+ */
28
+ onMouseLeave(): void {
29
+ if (this.hoverTimeout) {
30
+ clearTimeout(this.hoverTimeout);
31
+ this.hoverTimeout = null;
32
+ }
33
+
34
+ setTimeout(() => {
35
+ if (!this.subService.isHoveringContent()) {
36
+ this.subService.close();
37
+ }
38
+ }, 300);
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSubService } from './dropdown-menu-sub.service';
4
+
5
+ describe('DropdownMenuSubService', () => {
6
+ let service: DropdownMenuSubService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(DropdownMenuSubService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
@@ -0,0 +1,23 @@
1
+ import { Injectable, signal } from '@angular/core';
2
+
3
+ @Injectable()
4
+ export class DropdownMenuSubService {
5
+ isOpen = signal(false);
6
+ private hoveringContent = signal(false);
7
+
8
+ open(): void {
9
+ this.isOpen.set(true);
10
+ }
11
+
12
+ close(): void {
13
+ this.isOpen.set(false);
14
+ }
15
+
16
+ setHoveringContent(value: boolean): void {
17
+ this.hoveringContent.set(value);
18
+ }
19
+
20
+ isHoveringContent(): boolean {
21
+ return this.hoveringContent();
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ <div
2
+ (click)="toggle()"
3
+ (mouseenter)="onMouseEnter()"
4
+ (mouseleave)="onMouseLeave()"
5
+ class="cursor-pointer"
6
+ role="button">
7
+ <ng-content></ng-content>
8
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuTrigger } from './dropdown-menu-trigger';
4
+
5
+ describe('DropdownMenuTrigger', () => {
6
+ let component: DropdownMenuTrigger;
7
+ let fixture: ComponentFixture<DropdownMenuTrigger>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuTrigger]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuTrigger);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,55 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ import { DropdownMenuService } from '../dropdown-menu.service';
4
+
5
+ @Component({
6
+ selector: 'wally-dropdown-menu-trigger',
7
+ imports: [],
8
+ templateUrl: './dropdown-menu-trigger.html',
9
+ styleUrl: './dropdown-menu-trigger.css'
10
+ })
11
+ export class DropdownMenuTrigger {
12
+ private hoverTimeout: any = null;
13
+
14
+ constructor(
15
+ public dropdownMenuService: DropdownMenuService
16
+ ) {}
17
+
18
+ toggle(): void {
19
+ if (this.dropdownMenuService.triggerMode() === 'click') {
20
+ this.dropdownMenuService.toggle();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Handles mouse enter event for hover mode.
26
+ * Opens the dropdown after a 200ms delay to prevent accidental triggers.
27
+ */
28
+ onMouseEnter(): void {
29
+ if (this.dropdownMenuService.triggerMode() === 'hover') {
30
+ this.hoverTimeout = setTimeout(() => {
31
+ this.dropdownMenuService.open();
32
+ }, 200);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Handles mouse leave event for hover mode.
38
+ * Cancels pending open timeout and closes dropdown after 100ms delay,
39
+ * allowing user to move mouse to dropdown content without it closing.
40
+ */
41
+ onMouseLeave(): void {
42
+ if (this.dropdownMenuService.triggerMode() === 'hover') {
43
+ if (this.hoverTimeout) {
44
+ clearTimeout(this.hoverTimeout);
45
+ this.hoverTimeout = null;
46
+ }
47
+
48
+ setTimeout(() => {
49
+ if (!this.dropdownMenuService.isHoveringContent()) {
50
+ this.dropdownMenuService.close();
51
+ }
52
+ }, 100);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,3 @@
1
+ <div class="relative inline-block">
2
+ <ng-content></ng-content>
3
+ </div>
@@ -0,0 +1,16 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuService } from './dropdown-menu.service';
4
+
5
+ describe('DropdownMenuService', () => {
6
+ let service: DropdownMenuService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(DropdownMenuService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
@@ -0,0 +1,31 @@
1
+ import { Injectable, signal, WritableSignal } from '@angular/core';
2
+
3
+ export type TriggerMode = 'click' | 'hover';
4
+
5
+ @Injectable()
6
+ export class DropdownMenuService {
7
+ isOpen: WritableSignal<boolean> = signal(false);
8
+ triggerMode = signal<TriggerMode>('click');
9
+
10
+ private hoveringContent = signal(false);
11
+
12
+ toggle(): void {
13
+ this.isOpen.update(value => !value);
14
+ }
15
+
16
+ open(): void {
17
+ this.isOpen.set(true);
18
+ }
19
+
20
+ close(): void {
21
+ this.isOpen.set(false);
22
+ }
23
+
24
+ setHoveringContent(value: boolean) {
25
+ this.hoveringContent.set(value);
26
+ }
27
+
28
+ isHoveringContent(): boolean {
29
+ return this.hoveringContent();
30
+ }
31
+ }