wally-ui 1.12.1 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) 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.routes.server.ts +4 -0
  5. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +164 -31
  6. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +25 -3
  7. package/playground/showcase/src/app/components/ai/ai-prompt-input/ai-prompt-input.html +1 -1
  8. package/playground/showcase/src/app/components/badge/badge.css +0 -0
  9. package/playground/showcase/src/app/components/badge/badge.html +3 -0
  10. package/playground/showcase/src/app/components/badge/badge.ts +24 -0
  11. package/playground/showcase/src/app/components/button/button.html +1 -3
  12. package/playground/showcase/src/app/components/button/button.ts +4 -4
  13. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.css +0 -0
  14. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.html +9 -0
  15. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.spec.ts +23 -0
  16. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.ts +167 -0
  17. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.css +0 -0
  18. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.html +5 -0
  19. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.spec.ts +23 -0
  20. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.ts +10 -0
  21. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.css +0 -0
  22. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.html +6 -0
  23. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.spec.ts +23 -0
  24. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.ts +37 -0
  25. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.css +0 -0
  26. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.html +3 -0
  27. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.spec.ts +23 -0
  28. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.ts +11 -0
  29. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.css +0 -0
  30. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.html +1 -0
  31. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.spec.ts +23 -0
  32. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.ts +11 -0
  33. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.css +0 -0
  34. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.html +1 -0
  35. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.spec.ts +23 -0
  36. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.ts +11 -0
  37. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.css +0 -0
  38. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.html +3 -0
  39. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.spec.ts +23 -0
  40. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.ts +16 -0
  41. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.css +0 -0
  42. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.html +9 -0
  43. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.spec.ts +23 -0
  44. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.ts +140 -0
  45. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.css +0 -0
  46. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.html +13 -0
  47. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.spec.ts +23 -0
  48. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.ts +40 -0
  49. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.spec.ts +16 -0
  50. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.ts +23 -0
  51. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.css +0 -0
  52. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.html +8 -0
  53. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.spec.ts +23 -0
  54. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.ts +55 -0
  55. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.css +0 -0
  56. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.html +3 -0
  57. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.spec.ts +16 -0
  58. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.ts +31 -0
  59. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.ts +69 -0
  60. package/playground/showcase/src/app/components/tooltip/tooltip.ts +195 -80
  61. package/playground/showcase/src/app/pages/documentation/components/components.html +110 -51
  62. package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
  63. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.css +1 -0
  64. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.examples.ts +404 -0
  65. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.html +612 -0
  66. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.ts +127 -0
  67. package/playground/showcase/src/app/pages/home/home.html +10 -6
@@ -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
+ }
@@ -0,0 +1,69 @@
1
+ import { Component, effect, ElementRef, input, OnDestroy } from '@angular/core';
2
+
3
+ import { DropdownMenuService } from './dropdown-menu.service';
4
+
5
+ export type TriggerMode = 'click' | 'hover';
6
+
7
+ @Component({
8
+ selector: 'wally-dropdown-menu',
9
+ imports: [],
10
+ providers: [DropdownMenuService],
11
+ templateUrl: './dropdown-menu.html',
12
+ styleUrl: './dropdown-menu.css'
13
+ })
14
+ export class DropdownMenu implements OnDestroy {
15
+ triggerMode = input<TriggerMode>('click');
16
+
17
+ private clickOutsideListener: ((event: MouseEvent) => void) | null = null;
18
+
19
+ constructor(
20
+ public dropdownMenuService: DropdownMenuService,
21
+ private elementRef: ElementRef
22
+ ) {
23
+ effect(() => {
24
+ const mode = this.triggerMode();
25
+ this.dropdownMenuService.triggerMode.set(mode);
26
+ });
27
+
28
+ effect(() => {
29
+ if (this.dropdownMenuService.isOpen()) {
30
+ this.addClickOutsideListener();
31
+ } else {
32
+ this.removeClickOutsideListener();
33
+ }
34
+ });
35
+ }
36
+
37
+ ngOnDestroy(): void {
38
+ this.removeClickOutsideListener();
39
+ }
40
+
41
+ /**
42
+ * Adds a click listener to the document to detect clicks outside the dropdown.
43
+ * Uses setTimeout to prevent the same click that opens the dropdown from immediately closing it.
44
+ */
45
+ private addClickOutsideListener(): void {
46
+ setTimeout(() => {
47
+ this.clickOutsideListener = (event: MouseEvent) => {
48
+ const clickedElement = event.target as HTMLElement;
49
+ const dropdownElement = this.elementRef.nativeElement as HTMLElement;
50
+
51
+ if (!dropdownElement.contains(clickedElement)) {
52
+ this.dropdownMenuService.close();
53
+ }
54
+ };
55
+
56
+ document.addEventListener('click', this.clickOutsideListener);
57
+ }, 0);
58
+ }
59
+
60
+ /**
61
+ * Removes the click outside listener from the document and cleans up the reference.
62
+ */
63
+ private removeClickOutsideListener(): void {
64
+ if (this.clickOutsideListener) {
65
+ document.removeEventListener('click', this.clickOutsideListener);
66
+ this.clickOutsideListener = null;
67
+ }
68
+ }
69
+ }
@@ -1,4 +1,4 @@
1
- import { Component, computed, effect, ElementRef, input, InputSignal, OnDestroy, signal, ViewChild, WritableSignal } from '@angular/core';
1
+ import { Component, computed, ElementRef, input, InputSignal, OnDestroy, signal, ViewChild, WritableSignal } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
 
4
4
  type TooltipPosition = 'top' | 'bottom' | 'left' | 'right' | 'auto';
@@ -19,40 +19,34 @@ export class Tooltip implements OnDestroy {
19
19
  offset: InputSignal<number> = input<number>(2);
20
20
 
21
21
  visible: WritableSignal<boolean> = signal<boolean>(false);
22
- actualPosition = signal<Exclude<TooltipPosition, 'auto'>>('top');
23
- tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
22
+ actualPosition: WritableSignal<"top" | "bottom" | "left" | "right"> = signal<Exclude<TooltipPosition, 'auto'>>('top');
23
+ tooltipId: string = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
24
+ isPositioned: WritableSignal<boolean> = signal<boolean>(false);
24
25
 
25
26
  private showTimeout?: number;
26
27
  private hideTimeout?: number;
27
28
 
28
29
  tooltipClasses = computed(() => {
29
- const base = 'absolute z-50 py-1.5 px-3 text-sm text-white bg-[#121212] shadow-lg whitespace-nowrap pointer-events-none rounded-xl';
30
+ const base = 'absolute z-10 py-1.5 px-3 text-sm text-white font-medium bg-[#121212] shadow-lg whitespace-nowrap pointer-events-none rounded-xl';
30
31
  const darkMode = 'dark:bg-white dark:text-[#121212]';
31
32
 
32
33
  const positions = {
33
- top: `bottom-full left-1/2 -translate-x-1/2 mb-${this.offset()}`,
34
- bottom: `top-full left-1/2 -translate-x-1/2 mt-${this.offset()}`,
35
- left: `right-full top-1/2 -translate-y-1/2 mr-${this.offset()}`,
36
- right: `left-full top-1/2 -translate-y-1/2 ml-${this.offset()}`
34
+ top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
35
+ bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
36
+ left: 'right-full top-1/2 -translate-y-1/2 mr-2',
37
+ right: 'left-full top-1/2 -translate-y-1/2 ml-2'
37
38
  };
38
39
 
39
- // Animação super suave - apenas fade rápido
40
- const animation = this.visible()
40
+ const animation = this.visible() && this.isPositioned()
41
41
  ? 'transition-opacity duration-150 opacity-100'
42
- : 'transition-opacity duration-100 opacity-0';
42
+ : 'opacity-0';
43
43
 
44
44
  return `${base} ${darkMode} ${positions[this.actualPosition()]} ${animation}`;
45
45
  });
46
46
 
47
47
  constructor(
48
48
  private elementRef: ElementRef
49
- ) {
50
- effect(() => {
51
- if (this.visible() && this.position() === 'auto') {
52
- setTimeout(() => this.calculateBestPosition(), 10);
53
- }
54
- });
55
- }
49
+ ) { }
56
50
 
57
51
  ngOnDestroy() {
58
52
  if (this.showTimeout) {
@@ -74,11 +68,18 @@ export class Tooltip implements OnDestroy {
74
68
  }
75
69
 
76
70
  this.showTimeout = window.setTimeout(() => {
77
- this.visible.set(true);
78
-
71
+ // Set initial position
79
72
  if (this.position() !== 'auto') {
80
73
  this.actualPosition.set(this.position() as Exclude<TooltipPosition, 'auto'>);
81
74
  }
75
+
76
+ // Make tooltip visible with opacity 0 to measure it
77
+ this.visible.set(true);
78
+
79
+ // Calculate best position before showing (next frame after DOM update)
80
+ requestAnimationFrame(() => {
81
+ this.calculateBestPosition();
82
+ });
82
83
  }, this.delay());
83
84
  }
84
85
 
@@ -90,100 +91,214 @@ export class Tooltip implements OnDestroy {
90
91
 
91
92
  this.hideTimeout = window.setTimeout(() => {
92
93
  this.visible.set(false);
94
+ this.isPositioned.set(false); // Reset positioning flag
93
95
  }, 100);
94
96
  }
95
97
 
98
+ /**
99
+ * Calculates the best position for the tooltip based on available viewport space.
100
+ *
101
+ * This method intelligently positions the tooltip to ensure it remains fully visible
102
+ * within the viewport boundaries. It respects the user's preferred position but will
103
+ * automatically switch to an alternative position if the preferred one doesn't fit.
104
+ *
105
+ * **Algorithm Steps:**
106
+ * 1. Measures available space in all four directions (top, bottom, left, right)
107
+ * 2. Validates each position considering:
108
+ * - Tooltip dimensions (width/height)
109
+ * - Viewport boundaries
110
+ * - Required offset and padding (8px each)
111
+ * 3. Determines priority order:
112
+ * - If `position="auto"`: tries top → bottom → right → left
113
+ * - If specific position: tries that position first, then fallback to others
114
+ * 4. Selects the first valid position from priority order
115
+ * 5. If no position is fully valid, selects the one with most available space
116
+ * 6. Applies fine-tuning adjustments:
117
+ * - For top/bottom: shifts horizontally if edges overflow
118
+ * - For left/right: shifts vertically if edges overflow
119
+ *
120
+ * **Position Validation:**
121
+ * - **Top/Bottom**: Checks if there's enough vertical space AND tooltip fits horizontally
122
+ * - **Left/Right**: Checks if there's enough horizontal space AND tooltip fits vertically
123
+ *
124
+ * **Viewport Adjustment:**
125
+ * - Uses `calc()` to adjust `left` or `top` CSS properties
126
+ * - Preserves Tailwind's `-translate-x-1/2` and `-translate-y-1/2` transforms
127
+ * - Ensures 8px minimum padding from all viewport edges
128
+ *
129
+ * @example
130
+ * // User specifies position="bottom"
131
+ * // If bottom doesn't fit → tries top → right → left
132
+ * // Applies horizontal shift if tooltip overflows left/right edges
133
+ *
134
+ * @returns void
135
+ */
96
136
  private calculateBestPosition() {
97
- const hostElement =
98
- this.elementRef.nativeElement.querySelector('.tooltip-wrapper');
99
- if (!hostElement) return;
100
-
101
- const rect = hostElement.getBoundingClientRect();
102
- const tooltipEl = this.tooltipElement?.nativeElement;
103
- if (!tooltipEl) return;
104
-
105
- const tooltipRect = tooltipEl.getBoundingClientRect();
106
- const offset = this.offset();
107
-
108
- const viewportWidth = window.innerWidth;
109
- const viewportHeight = window.innerHeight;
110
-
111
- const spaceAbove = rect.top;
112
- const spaceBelow = viewportHeight - rect.bottom;
113
- const spaceLeft = rect.left;
114
- const spaceRight = viewportWidth - rect.right;
137
+ const wrapperElement = this.elementRef.nativeElement.querySelector('.tooltip-wrapper');
138
+ if (!wrapperElement) {
139
+ return;
140
+ }
115
141
 
116
- const tooltipWidth = tooltipRect.width || 150;
117
- const tooltipHeight = tooltipRect.height || 40;
142
+ const tooltipElement = this.tooltipElement?.nativeElement;
143
+ if (!tooltipElement) {
144
+ return;
145
+ }
118
146
 
119
- const positions: {
120
- position: Exclude<TooltipPosition, 'auto'>; space: number; isValid: boolean}[] = [
147
+ const wrapperBounds = wrapperElement.getBoundingClientRect();
148
+ const tooltipBounds = tooltipElement.getBoundingClientRect();
149
+
150
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
151
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
152
+
153
+ // Available space in each direction from the wrapper element
154
+ const availableSpaceAbove = wrapperBounds.top;
155
+ const availableSpaceBelow = viewportHeight - wrapperBounds.bottom;
156
+ const availableSpaceLeft = wrapperBounds.left;
157
+ const availableSpaceRight = viewportWidth - wrapperBounds.right;
158
+
159
+ // Actual tooltip dimensions
160
+ const tooltipWidth = tooltipBounds.width;
161
+ const tooltipHeight = tooltipBounds.height;
162
+
163
+ // Fixed spacing values (8px each = 0.5rem in Tailwind)
164
+ const offsetFromWrapper = 8; // Gap between tooltip and wrapper
165
+ const edgePadding = 8; // Minimum distance from viewport edges
166
+
167
+ // Calculate horizontal center position of the wrapper
168
+ const wrapperCenterX = wrapperBounds.left + wrapperBounds.width / 2;
169
+ const wrapperCenterY = wrapperBounds.top + wrapperBounds.height / 2;
170
+
171
+ // Calculate where tooltip edges would be when centered
172
+ const tooltipLeftEdgeWhenCentered = wrapperCenterX - tooltipWidth / 2;
173
+ const tooltipRightEdgeWhenCentered = wrapperCenterX + tooltipWidth / 2;
174
+ const tooltipTopEdgeWhenCentered = wrapperCenterY - tooltipHeight / 2;
175
+ const tooltipBottomEdgeWhenCentered = wrapperCenterY + tooltipHeight / 2;
176
+
177
+ // Check if tooltip fits within viewport for each position
178
+ const positionValidations: {
179
+ position: Exclude<TooltipPosition, 'auto'>;
180
+ availableSpace: number;
181
+ fitsInViewport: boolean;
182
+ fitsHorizontally?: boolean;
183
+ fitsVertically?: boolean;
184
+ }[] = [
121
185
  {
122
186
  position: 'top',
123
- space: spaceAbove,
124
- isValid: spaceAbove >= tooltipHeight + offset
187
+ availableSpace: availableSpaceAbove,
188
+ fitsInViewport: availableSpaceAbove >= tooltipHeight + offsetFromWrapper + edgePadding,
189
+ fitsHorizontally: tooltipLeftEdgeWhenCentered >= edgePadding &&
190
+ tooltipRightEdgeWhenCentered <= viewportWidth - edgePadding
125
191
  },
126
192
  {
127
193
  position: 'bottom',
128
- space: spaceBelow,
129
- isValid: spaceBelow >= tooltipHeight + offset
194
+ availableSpace: availableSpaceBelow,
195
+ fitsInViewport: availableSpaceBelow >= tooltipHeight + offsetFromWrapper + edgePadding,
196
+ fitsHorizontally: tooltipLeftEdgeWhenCentered >= edgePadding &&
197
+ tooltipRightEdgeWhenCentered <= viewportWidth - edgePadding
130
198
  },
131
199
  {
132
200
  position: 'left',
133
- space: spaceLeft,
134
- isValid: spaceLeft >= tooltipWidth + offset
201
+ availableSpace: availableSpaceLeft,
202
+ fitsInViewport: availableSpaceLeft >= tooltipWidth + offsetFromWrapper + edgePadding,
203
+ fitsVertically: tooltipTopEdgeWhenCentered >= edgePadding &&
204
+ tooltipBottomEdgeWhenCentered <= viewportHeight - edgePadding
135
205
  },
136
206
  {
137
207
  position: 'right',
138
- space: spaceRight,
139
- isValid: spaceRight >= tooltipWidth + offset
208
+ availableSpace: availableSpaceRight,
209
+ fitsInViewport: availableSpaceRight >= tooltipWidth + offsetFromWrapper + edgePadding,
210
+ fitsVertically: tooltipTopEdgeWhenCentered >= edgePadding &&
211
+ tooltipBottomEdgeWhenCentered <= viewportHeight - edgePadding
140
212
  }
141
213
  ];
142
214
 
143
- const priorityOrder: Exclude<TooltipPosition, 'auto'>[] = ['top', 'bottom', 'right', 'left'];
215
+ // Determine which positions to try, in order of preference
216
+ let positionsToTryInOrder: Exclude<TooltipPosition, 'auto'>[];
144
217
 
145
- let bestPosition: Exclude<TooltipPosition, 'auto'> = 'top';
218
+ if (this.position() === 'auto') {
219
+ // Auto mode: use default priority order
220
+ positionsToTryInOrder = ['top', 'bottom', 'right', 'left'];
221
+ } else {
222
+ // Specific position: try user's preference first, then fallback to others
223
+ const userPreferredPosition = this.position() as Exclude<TooltipPosition, 'auto'>;
224
+ const allOtherPositions = (['top', 'bottom', 'right', 'left'] as Exclude<TooltipPosition, 'auto'>[])
225
+ .filter(position => position !== userPreferredPosition);
146
226
 
147
- for (const preferred of priorityOrder) {
148
- const pos = positions.find(p => p.position === preferred);
149
- if (pos?.isValid) {
150
- bestPosition = preferred;
227
+ positionsToTryInOrder = [userPreferredPosition, ...allOtherPositions];
228
+ }
229
+
230
+ // Find the first position that fits completely in the viewport
231
+ let finalSelectedPosition: Exclude<TooltipPosition, 'auto'> = positionsToTryInOrder[0];
232
+
233
+ for (const candidatePosition of positionsToTryInOrder) {
234
+ const validation = positionValidations.find(v => v.position === candidatePosition);
235
+ if (validation?.fitsInViewport) {
236
+ finalSelectedPosition = candidatePosition;
151
237
  break;
152
238
  }
153
239
  }
154
240
 
155
- if (!positions.find(p => p.position ===
156
- bestPosition)?.isValid) {
157
- const maxSpace = positions.reduce((max, pos) => pos.space >
158
- max.space ? pos : max, positions[0]);
159
- bestPosition = maxSpace.position;
241
+ // Fallback: if no position fits, use the one with most available space
242
+ const currentPositionValidation = positionValidations.find(v => v.position === finalSelectedPosition);
243
+ if (!currentPositionValidation?.fitsInViewport) {
244
+ const positionWithMostSpace = positionValidations.reduce((best, current) =>
245
+ current.availableSpace > best.availableSpace ? current : best,
246
+ positionValidations[0]
247
+ );
248
+ finalSelectedPosition = positionWithMostSpace.position;
160
249
  }
161
250
 
162
- this.actualPosition.set(bestPosition);
251
+ this.actualPosition.set(finalSelectedPosition);
252
+
253
+ // Apply fine-tuning adjustments after position is set
254
+ requestAnimationFrame(() => {
255
+ // Mark as positioned to trigger fade-in animation
256
+ this.isPositioned.set(true);
163
257
 
164
- setTimeout(() => {
165
- const updatedTooltipRect = tooltipEl.getBoundingClientRect();
166
- const padding = 8;
167
258
 
168
- if (bestPosition === 'top' || bestPosition === 'bottom') {
169
- if (updatedTooltipRect.left < padding) {
170
- const shift = padding - updatedTooltipRect.left;
171
- tooltipEl.style.transform = `translateX(${shift}px)`;
172
- } else if (updatedTooltipRect.right > viewportWidth - padding) {
173
- const shift = (viewportWidth - padding) - updatedTooltipRect.right;
174
- tooltipEl.style.transform = `translateX(${shift}px)`;
259
+ const tooltipBoundsAfterPositioning = tooltipElement.getBoundingClientRect();
260
+
261
+ // Reset any previous adjustments
262
+ tooltipElement.style.left = '';
263
+ tooltipElement.style.top = '';
264
+
265
+ // Adjust horizontal position for top/bottom tooltips that overflow edges
266
+ if (finalSelectedPosition === 'top' || finalSelectedPosition === 'bottom') {
267
+ let horizontalAdjustment = 0;
268
+
269
+ // Check if tooltip overflows left edge
270
+ if (tooltipBoundsAfterPositioning.left < edgePadding) {
271
+ horizontalAdjustment = edgePadding - tooltipBoundsAfterPositioning.left;
272
+ }
273
+ // Check if tooltip overflows right edge
274
+ else if (tooltipBoundsAfterPositioning.right > viewportWidth - edgePadding) {
275
+ horizontalAdjustment = (viewportWidth - edgePadding) - tooltipBoundsAfterPositioning.right;
276
+ }
277
+
278
+ if (horizontalAdjustment !== 0) {
279
+ // Shift tooltip while preserving centered alignment
280
+ tooltipElement.style.left = `calc(50% + ${horizontalAdjustment}px)`;
175
281
  }
176
282
  }
177
283
 
178
- if (bestPosition === 'left' || bestPosition === 'right') {
179
- if (updatedTooltipRect.top < padding) {
180
- const shift = padding - updatedTooltipRect.top;
181
- tooltipEl.style.transform = `translateY(${shift}px)`;
182
- } else if (updatedTooltipRect.bottom > viewportHeight - padding) {
183
- const shift = (viewportHeight - padding) - updatedTooltipRect.bottom;
184
- tooltipEl.style.transform = `translateY(${shift}px)`;
284
+ // Adjust vertical position for left/right tooltips that overflow edges
285
+ if (finalSelectedPosition === 'left' || finalSelectedPosition === 'right') {
286
+ let verticalAdjustment = 0;
287
+
288
+ // Check if tooltip overflows top edge
289
+ if (tooltipBoundsAfterPositioning.top < edgePadding) {
290
+ verticalAdjustment = edgePadding - tooltipBoundsAfterPositioning.top;
291
+ }
292
+ // Check if tooltip overflows bottom edge
293
+ else if (tooltipBoundsAfterPositioning.bottom > viewportHeight - edgePadding) {
294
+ verticalAdjustment = (viewportHeight - edgePadding) - tooltipBoundsAfterPositioning.bottom;
295
+ }
296
+
297
+ if (verticalAdjustment !== 0) {
298
+ // Shift tooltip while preserving centered alignment
299
+ tooltipElement.style.top = `calc(50% + ${verticalAdjustment}px)`;
185
300
  }
186
301
  }
187
- }, 0);
302
+ });
188
303
  }
189
304
  }