@makigamestudio/ui-ionic 0.7.0 → 0.8.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.
@@ -1,10 +1,599 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, signal, input, output, computed, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { inject, ElementRef, Renderer2, ApplicationRef, DestroyRef, input, effect, isSignal, TemplateRef, HostListener, Directive, signal, output, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { PopoverController, IonList, IonItem, IonIcon, IonLabel, IonSpinner, IonButton } from '@ionic/angular/standalone';
4
- import { ActionButtonType, ActionButtonListService, ButtonStateService, ButtonDisplayService, ButtonHandlerService } from '@makigamestudio/ui-core';
4
+ import { TooltipService, TooltipSchedulerService, DeviceDetectionService, ActionButtonType, ActionButtonListService, ButtonStateService, ButtonDisplayService, ButtonHandlerService } from '@makigamestudio/ui-core';
5
5
  import { addIcons } from 'ionicons';
6
6
  import { chevronDownOutline } from 'ionicons/icons';
7
7
 
8
+ /**
9
+ * @file Maki Tooltip Directive
10
+ * @description Ionic implementation of tooltip directive using TooltipService.
11
+ *
12
+ * This directive provides accessible tooltips that adapt to device type and viewport constraints.
13
+ * On desktop, tooltips appear on hover with a delay. On mobile, they appear on click (except for
14
+ * interactive elements like buttons where they're suppressed to avoid interfering with click handlers).
15
+ *
16
+ * All positioning and visibility logic is delegated to TooltipService from ui-core,
17
+ * while this directive handles Ionic-specific DOM manipulation and event handling.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Simple string tooltip
22
+ * <button makiTooltip="Save changes">Save</button>
23
+ *
24
+ * // With custom color
25
+ * <ion-button makiTooltip="Delete item" makiTooltipColor="danger">
26
+ * <ion-icon name="trash" />
27
+ * </ion-button>
28
+ *
29
+ * // Template tooltip
30
+ * <ng-template #customTooltip>
31
+ * <strong>Custom</strong> content
32
+ * </ng-template>
33
+ * <div [makiTooltip]="customTooltip">Hover me</div>
34
+ * ```
35
+ */
36
+ /**
37
+ * Attribute name added to elements with tooltip directive.
38
+ * Useful for testing and debugging.
39
+ */
40
+ const TOOLTIP_ATTRIBUTE = 'maki-tooltip';
41
+ /**
42
+ * Directive for displaying contextual tooltips with device-aware behavior.
43
+ *
44
+ * The directive automatically adapts its behavior based on device type:
45
+ * - **Desktop**: Shows tooltip on hover after 500ms delay, hides on mouse leave
46
+ * - **Mobile**: Shows/hides tooltip on click (suppressed for interactive elements)
47
+ *
48
+ * Tooltips are positioned intelligently to avoid viewport overflow, appearing
49
+ * above or to the left of the element when necessary.
50
+ *
51
+ * @usageNotes
52
+ *
53
+ * ### Basic String Tooltip
54
+ * ```html
55
+ * <button makiTooltip="Click to save">Save</button>
56
+ * ```
57
+ *
58
+ * ### Colored Tooltip
59
+ * ```html
60
+ * <ion-button makiTooltip="Permanently delete" makiTooltipColor="danger">
61
+ * Delete
62
+ * </ion-button>
63
+ * ```
64
+ *
65
+ * ### Template Tooltip
66
+ * ```html
67
+ * <ng-template #richTooltip>
68
+ * <div class="custom-tooltip">
69
+ * <h4>Title</h4>
70
+ * <p>Description</p>
71
+ * </div>
72
+ * </ng-template>
73
+ * <div [makiTooltip]="richTooltip">Hover for details</div>
74
+ * ```
75
+ *
76
+ * ### Live Updates with Signals
77
+ * ```html
78
+ * <span [makiTooltip]="statusTooltip()">Hover</span>
79
+ * ```
80
+ * ```typescript
81
+ * readonly statusTooltip = signal('Status: Ready');
82
+ *
83
+ * // Tooltip updates live while open
84
+ * this.statusTooltip.set('Status: Processing');
85
+ * ```
86
+ *
87
+ * ### Mobile Behavior
88
+ * On mobile devices, tooltips are suppressed for interactive elements
89
+ * (button, ion-button, ion-select, a) to avoid interfering with their
90
+ * primary click handlers.
91
+ */
92
+ class MakiTooltipDirective {
93
+ // ============================================================================
94
+ // Dependencies
95
+ // ============================================================================
96
+ el = inject(ElementRef);
97
+ renderer = inject(Renderer2);
98
+ appRef = inject(ApplicationRef);
99
+ destroyRef = inject(DestroyRef);
100
+ tooltipService = inject(TooltipService);
101
+ scheduler = inject(TooltipSchedulerService);
102
+ deviceDetection = inject(DeviceDetectionService);
103
+ // ============================================================================
104
+ // Inputs
105
+ // ============================================================================
106
+ /**
107
+ * Tooltip content - can be a string or a template reference.
108
+ *
109
+ * If the input is signal-driven, an open tooltip will update live
110
+ * when the signal value changes.
111
+ *
112
+ * @example
113
+ * ```html
114
+ * <!-- String content -->
115
+ * <button makiTooltip="Save changes">Save</button>
116
+ *
117
+ * <!-- Template content -->
118
+ * <ng-template #tooltip>Rich content</ng-template>
119
+ * <div [makiTooltip]="tooltip">Hover</div>
120
+ * ```
121
+ */
122
+ content = input.required({ ...(ngDevMode ? { debugName: "content" } : {}), alias: 'makiTooltip' });
123
+ /**
124
+ * Optional color for the tooltip background.
125
+ * Accepts Ionic color names or null for default styling.
126
+ *
127
+ * @example
128
+ * ```html
129
+ * <button makiTooltip="Delete" makiTooltipColor="danger">Delete</button>
130
+ * ```
131
+ */
132
+ color = input(null, { ...(ngDevMode ? { debugName: "color" } : {}), alias: 'makiTooltipColor' });
133
+ /**
134
+ * Preferred tooltip placement. If provided, the directive will attempt to
135
+ * position the tooltip on the specified side before falling back.
136
+ */
137
+ position = input(undefined, { ...(ngDevMode ? { debugName: "position" } : {}), alias: 'makiTooltipPosition' });
138
+ /**
139
+ * Optional context object for TemplateRef tooltips.
140
+ * Supports signal inputs so templates can react to changes.
141
+ * Open tooltips update live as the context signal changes.
142
+ *
143
+ * The context is exposed as both named properties and `$implicit`.
144
+ *
145
+ * @example
146
+ * ```html
147
+ * <ng-template #tooltip let-title="title" let-data>
148
+ * <strong>{{ title }}</strong>
149
+ * <span>{{ data?.title }}</span>
150
+ * </ng-template>
151
+ * <span [makiTooltip]="tooltip" [makiTooltipContext]="{ title: 'Info' }">Hover</span>
152
+ * ```
153
+ */
154
+ context = input(undefined, { ...(ngDevMode ? { debugName: "context" } : {}), alias: 'makiTooltipContext' });
155
+ // ============================================================================
156
+ // Private State
157
+ // ============================================================================
158
+ /**
159
+ * Reference to the tooltip DOM element.
160
+ */
161
+ tooltip = null;
162
+ /**
163
+ * Timeout handle for hover delay.
164
+ */
165
+ /**
166
+ * Reference to the embedded view when using template content.
167
+ */
168
+ viewRef = null;
169
+ /**
170
+ * Tracks the current template reference to detect swaps.
171
+ */
172
+ currentTemplateRef = null;
173
+ /**
174
+ * Cleanup function for document touch listener.
175
+ */
176
+ touchListenerCleanup = null;
177
+ /**
178
+ * Animation frame ID for tooltip follow loop.
179
+ */
180
+ positionAnimationFrame = null;
181
+ /**
182
+ * Timeout handle for fade-out removal.
183
+ */
184
+ fadeTimeout = null;
185
+ /**
186
+ * Scheduler handle for this directive instance (isolated timers).
187
+ * */
188
+ schedulerHandle = null;
189
+ /**
190
+ * Indicates whether the directive has been destroyed.
191
+ */
192
+ isDestroyed = false;
193
+ /**
194
+ * Latest resolved TemplateRef context.
195
+ */
196
+ latestTemplateContext = { $implicit: undefined };
197
+ // ============================================================================
198
+ // Constructor
199
+ // ============================================================================
200
+ constructor() {
201
+ // Add custom attribute for identification
202
+ this.el.nativeElement.setAttribute(TOOLTIP_ATTRIBUTE, 'true');
203
+ effect(() => {
204
+ this.latestTemplateContext = this.resolveTemplateContext();
205
+ if (this.viewRef) {
206
+ Object.assign(this.viewRef.context, this.latestTemplateContext);
207
+ this.viewRef.detectChanges();
208
+ this.positionTooltip();
209
+ }
210
+ });
211
+ effect(() => {
212
+ const contentValue = this.content();
213
+ if (!this.tooltip)
214
+ return;
215
+ this.updateTooltipContent(contentValue);
216
+ this.positionTooltip();
217
+ });
218
+ // Create isolated scheduler handle for this directive instance
219
+ this.schedulerHandle = this.scheduler.createHandle(() => this.showTooltip(), () => this.hideTooltip());
220
+ // Cleanup on destroy
221
+ this.destroyRef.onDestroy(() => {
222
+ this.isDestroyed = true;
223
+ this.schedulerHandle?.destroy();
224
+ this.hideTooltip();
225
+ });
226
+ }
227
+ // ============================================================================
228
+ // Event Handlers
229
+ // ============================================================================
230
+ /**
231
+ * Shows tooltip on mouse enter (desktop only).
232
+ * Adds a 500ms delay to avoid showing tooltips during quick mouse movements.
233
+ */
234
+ onMouseEnter() {
235
+ if (!this.canShowTooltip())
236
+ return;
237
+ this.schedulerHandle?.onTriggerEnter();
238
+ }
239
+ onMouseLeave() {
240
+ this.schedulerHandle?.onTriggerLeave();
241
+ }
242
+ /**
243
+ * Toggles tooltip on click (all devices).
244
+ * On mobile, skips interactive elements. On desktop, acts as a toggle.
245
+ */
246
+ onClick() {
247
+ if (!this.canShowTooltip()) {
248
+ this.hideTooltip();
249
+ return;
250
+ }
251
+ // En mobile, activar touch state
252
+ this.schedulerHandle?.onClick(this.deviceDetection.isMobile());
253
+ }
254
+ // ============================================================================
255
+ // Private Methods
256
+ // ============================================================================
257
+ /**
258
+ * Checks if tooltip can be shown using TooltipService logic.
259
+ *
260
+ * @returns `true` if tooltip can be shown, `false` otherwise
261
+ */
262
+ canShowTooltip() {
263
+ const elementTag = this.el.nativeElement.tagName.toLowerCase();
264
+ const hasContent = !!this.content();
265
+ return this.tooltipService.shouldShowTooltip(elementTag, this.deviceDetection.isMobile(), hasContent);
266
+ }
267
+ resolveTemplateContext() {
268
+ const rawContext = this.context();
269
+ const resolvedContext = this.unwrapSignal(rawContext);
270
+ if (resolvedContext && typeof resolvedContext === 'object') {
271
+ return {
272
+ ...resolvedContext,
273
+ $implicit: resolvedContext
274
+ };
275
+ }
276
+ return { $implicit: resolvedContext };
277
+ }
278
+ unwrapSignal(value) {
279
+ if (isSignal(value)) {
280
+ return value();
281
+ }
282
+ return value;
283
+ }
284
+ /**
285
+ * Clears the hover timeout if it exists.
286
+ */
287
+ /**
288
+ * Creates and displays the tooltip element.
289
+ *
290
+ * The tooltip is created as a fixed-position element appended to the document body.
291
+ * Content can be either plain text or a rendered template.
292
+ */
293
+ showTooltip() {
294
+ if (this.tooltip || !this.content())
295
+ return;
296
+ // Create tooltip element
297
+ this.tooltip = this.renderer.createElement('div');
298
+ this.renderer.addClass(this.tooltip, 'maki-tooltip');
299
+ if (!this.tooltip)
300
+ return;
301
+ // Ensure tooltip is not visible or interactive while we measure and position it
302
+ this.tooltip.style.position = 'fixed';
303
+ this.tooltip.style.top = '-9999px';
304
+ this.tooltip.style.left = '-9999px';
305
+ this.tooltip.style.visibility = 'hidden';
306
+ this.tooltip.style.pointerEvents = 'none';
307
+ this.tooltip.style.opacity = '0';
308
+ this.tooltip.style.transition = `opacity ${this.tooltipService.getFadeDurationMs()}ms ease`;
309
+ // Apply color attribute if specified
310
+ const colorValue = this.color();
311
+ if (colorValue) {
312
+ this.renderer.setAttribute(this.tooltip, 'data-color', colorValue);
313
+ }
314
+ // Set content (template or string)
315
+ const contentValue = this.content();
316
+ this.updateTooltipContent(contentValue);
317
+ // Append hidden tooltip to body so we can measure it without visual jump
318
+ this.renderer.appendChild(document.body, this.tooltip);
319
+ // If there was a pending fade removal, cancel it (we're re-showing)
320
+ this.clearFadeTimeout();
321
+ // Immediately position the tooltip while hidden to avoid a visual jump
322
+ this.positionTooltip();
323
+ // Reveal and make interactive with fade-in
324
+ // Make visible before starting opacity animation so it's measured properly
325
+ this.tooltip.style.visibility = 'visible';
326
+ // Allow pointer events after showing
327
+ this.tooltip.style.pointerEvents = 'auto';
328
+ // Trigger fade-in
329
+ // Force a reflow to ensure transition runs
330
+ this.tooltip.getBoundingClientRect();
331
+ this.tooltip.style.opacity = '1';
332
+ // Añadir listeners para interacción (desktop y mobile)
333
+ this.addTooltipInteractionListeners();
334
+ // Start animation frame loop to follow trigger
335
+ this.startTooltipFollowLoop();
336
+ // Add document listeners on mobile
337
+ if (this.deviceDetection.isMobile()) {
338
+ this.addDocumentTouchListener();
339
+ }
340
+ }
341
+ /**
342
+ * Positions the tooltip using TooltipService calculations.
343
+ */
344
+ positionTooltip() {
345
+ if (!this.tooltip)
346
+ return;
347
+ const triggerRect = this.el.nativeElement.getBoundingClientRect();
348
+ // Hide tooltip if element is not visible
349
+ if (!this.tooltipService.isElementVisible(triggerRect)) {
350
+ this.hideTooltip();
351
+ return;
352
+ }
353
+ const tooltipRect = this.tooltip.getBoundingClientRect();
354
+ // Calculate position using service
355
+ const position = this.tooltipService.calculatePosition(triggerRect, tooltipRect, window.innerWidth, window.innerHeight, this.position());
356
+ // Apply position
357
+ this.tooltip.style.position = 'fixed';
358
+ this.tooltip.style.zIndex = '10000';
359
+ this.tooltip.style.top = `${position.top}px`;
360
+ this.tooltip.style.left = `${position.left}px`;
361
+ }
362
+ /**
363
+ * Adds a document-level touch listener to hide tooltip on any touch outside.
364
+ * Only used on mobile devices.
365
+ */
366
+ addDocumentTouchListener() {
367
+ this.removeDocumentTouchListener();
368
+ this.touchListenerCleanup = this.renderer.listen('document', 'touchstart', (event) => {
369
+ if (!this.tooltip)
370
+ return;
371
+ // Allow touches inside the tooltip or the trigger element to keep it open
372
+ const target = event.target;
373
+ if (target && (this.tooltip.contains(target) || this.el.nativeElement.contains(target))) {
374
+ return;
375
+ }
376
+ // Any touch outside should hide the tooltip
377
+ this.hideTooltip();
378
+ });
379
+ }
380
+ /**
381
+ * Removes the document-level touch listener.
382
+ */
383
+ removeDocumentTouchListener() {
384
+ if (this.touchListenerCleanup) {
385
+ this.touchListenerCleanup();
386
+ this.touchListenerCleanup = null;
387
+ }
388
+ }
389
+ /**
390
+ * Removes the tooltip from the DOM and cleans up resources.
391
+ */
392
+ hideTooltip() {
393
+ this.removeDocumentTouchListener();
394
+ this.removeTooltipInteractionListeners();
395
+ this.stopTooltipFollowLoop();
396
+ // If there's no tooltip (already removed), nothing to do
397
+ if (!this.tooltip)
398
+ return;
399
+ // Begin fade-out and removal
400
+ this.tooltip.style.opacity = '0';
401
+ this.tooltip.style.pointerEvents = 'none';
402
+ // Schedule removal after fade duration.
403
+ this.clearFadeTimeout();
404
+ this.fadeTimeout = setTimeout(() => {
405
+ if (this.tooltip) {
406
+ // Avoid leaving the tooltip empty during the fade animation.
407
+ if (this.viewRef) {
408
+ if (!this.isDestroyed) {
409
+ try {
410
+ this.appRef.detachView(this.viewRef);
411
+ }
412
+ catch {
413
+ // ignore detach errors
414
+ }
415
+ }
416
+ try {
417
+ this.viewRef.destroy();
418
+ }
419
+ catch {
420
+ // ignore destroy errors
421
+ }
422
+ this.viewRef = null;
423
+ this.currentTemplateRef = null;
424
+ }
425
+ this.renderer.removeChild(document.body, this.tooltip);
426
+ this.tooltip = null;
427
+ }
428
+ this.fadeTimeout = null;
429
+ }, this.tooltipService.getFadeDurationMs());
430
+ }
431
+ clearFadeTimeout() {
432
+ if (this.fadeTimeout) {
433
+ clearTimeout(this.fadeTimeout);
434
+ this.fadeTimeout = null;
435
+ }
436
+ }
437
+ updateTooltipContent(contentValue) {
438
+ if (!this.tooltip)
439
+ return;
440
+ if (contentValue instanceof TemplateRef) {
441
+ this.updateTemplateContent(contentValue);
442
+ return;
443
+ }
444
+ this.updateStringContent(contentValue);
445
+ }
446
+ updateTemplateContent(template) {
447
+ if (!this.tooltip)
448
+ return;
449
+ const shouldRecreate = this.currentTemplateRef !== template || !this.viewRef;
450
+ if (shouldRecreate) {
451
+ this.clearTooltipContent();
452
+ this.viewRef = template.createEmbeddedView(this.latestTemplateContext);
453
+ // Avoid calling attachView after destroy; it can log NG0406 warnings.
454
+ if (!this.isDestroyed) {
455
+ this.appRef.attachView(this.viewRef);
456
+ }
457
+ this.viewRef.rootNodes.forEach(node => this.renderer.appendChild(this.tooltip, node));
458
+ this.currentTemplateRef = template;
459
+ }
460
+ }
461
+ updateStringContent(contentValue) {
462
+ if (!this.tooltip)
463
+ return;
464
+ if (this.viewRef || this.currentTemplateRef) {
465
+ this.clearTooltipContent();
466
+ }
467
+ this.tooltip.textContent = contentValue;
468
+ this.currentTemplateRef = null;
469
+ }
470
+ clearTooltipContent() {
471
+ if (!this.tooltip)
472
+ return;
473
+ if (this.viewRef) {
474
+ if (!this.isDestroyed) {
475
+ try {
476
+ this.appRef.detachView(this.viewRef);
477
+ }
478
+ catch {
479
+ // ignore detach errors
480
+ }
481
+ }
482
+ try {
483
+ this.viewRef.destroy();
484
+ }
485
+ catch {
486
+ // ignore destroy errors
487
+ }
488
+ this.viewRef = null;
489
+ }
490
+ while (this.tooltip.firstChild) {
491
+ this.renderer.removeChild(this.tooltip, this.tooltip.firstChild);
492
+ }
493
+ }
494
+ /**
495
+ * Añade listeners de interacción al tooltip para mantenerlo abierto mientras se interactúa.
496
+ */
497
+ addTooltipInteractionListeners() {
498
+ if (!this.tooltip)
499
+ return;
500
+ // Siempre elimina antes por si acaso (defensivo)
501
+ this.removeTooltipInteractionListeners();
502
+ // Desktop: mouseenter/mouseleave
503
+ this.tooltip.addEventListener('mouseenter', this.onTooltipMouseEnter);
504
+ this.tooltip.addEventListener('mouseleave', this.onTooltipMouseLeave);
505
+ // Mobile: touchstart/touchend
506
+ this.tooltip.addEventListener('touchstart', this.onTooltipTouchStart, { passive: true });
507
+ this.tooltip.addEventListener('touchend', this.onTooltipTouchEnd, { passive: true });
508
+ }
509
+ /**
510
+ * Elimina listeners de interacción del tooltip.
511
+ */
512
+ removeTooltipInteractionListeners() {
513
+ if (!this.tooltip)
514
+ return;
515
+ this.tooltip.removeEventListener('mouseenter', this.onTooltipMouseEnter);
516
+ this.tooltip.removeEventListener('mouseleave', this.onTooltipMouseLeave);
517
+ this.tooltip.removeEventListener('touchstart', this.onTooltipTouchStart);
518
+ this.tooltip.removeEventListener('touchend', this.onTooltipTouchEnd);
519
+ }
520
+ /**
521
+ * Handler: mouse entra en el tooltip (desktop).
522
+ */
523
+ onTooltipMouseEnter = () => {
524
+ this.schedulerHandle?.onTooltipEnter();
525
+ };
526
+ /**
527
+ * Handler: mouse sale del tooltip (desktop).
528
+ */
529
+ onTooltipMouseLeave = () => {
530
+ this.schedulerHandle?.onTooltipLeave();
531
+ };
532
+ /**
533
+ * Handler: touchstart en el tooltip (mobile).
534
+ */
535
+ onTooltipTouchStart = () => {
536
+ this.schedulerHandle?.onTooltipTouchStart();
537
+ };
538
+ /**
539
+ * Handler: touchend en el tooltip (mobile).
540
+ */
541
+ onTooltipTouchEnd = () => {
542
+ this.schedulerHandle?.onTooltipTouchEnd();
543
+ };
544
+ /**
545
+ * Starts a high-performance animation frame loop to keep the tooltip positioned with its trigger.
546
+ */
547
+ startTooltipFollowLoop() {
548
+ this.stopTooltipFollowLoop();
549
+ const loop = () => {
550
+ if (!this.tooltip)
551
+ return;
552
+ this.positionTooltip();
553
+ this.positionAnimationFrame = requestAnimationFrame(loop);
554
+ };
555
+ this.positionAnimationFrame = requestAnimationFrame(loop);
556
+ }
557
+ /**
558
+ * Stops the animation frame loop for tooltip positioning.
559
+ */
560
+ stopTooltipFollowLoop() {
561
+ if (this.positionAnimationFrame !== null) {
562
+ cancelAnimationFrame(this.positionAnimationFrame);
563
+ this.positionAnimationFrame = null;
564
+ }
565
+ }
566
+ /**
567
+ * Cleanup on directive destruction.
568
+ */
569
+ ngOnDestroy() {
570
+ this.hideTooltip();
571
+ }
572
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MakiTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
573
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: MakiTooltipDirective, isStandalone: true, selector: "[makiTooltip]", inputs: { content: { classPropertyName: "content", publicName: "makiTooltip", isSignal: true, isRequired: true, transformFunction: null }, color: { classPropertyName: "color", publicName: "makiTooltipColor", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "makiTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "makiTooltipContext", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()", "click": "onClick()" } }, ngImport: i0 });
574
+ }
575
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MakiTooltipDirective, decorators: [{
576
+ type: Directive,
577
+ args: [{
578
+ selector: '[makiTooltip]',
579
+ standalone: true
580
+ }]
581
+ }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "makiTooltip", required: true }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "makiTooltipColor", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "makiTooltipPosition", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "makiTooltipContext", required: false }] }], onMouseEnter: [{
582
+ type: HostListener,
583
+ args: ['mouseenter']
584
+ }], onMouseLeave: [{
585
+ type: HostListener,
586
+ args: ['mouseleave']
587
+ }], onClick: [{
588
+ type: HostListener,
589
+ args: ['click']
590
+ }] } });
591
+
592
+ /**
593
+ * @file Directives Barrel Export
594
+ * @description Exports all directives from the ui-ionic library.
595
+ */
596
+
8
597
  /**
9
598
  * @file Action Button List Component
10
599
  * @description Ionic implementation of action button list for popovers/dropdowns.
@@ -44,10 +633,10 @@ import { chevronDownOutline } from 'ionicons/icons';
44
633
  *
45
634
  * @usageNotes
46
635
  * ### Inputs
47
- * - `buttons` (required): Array of `ActionButton` objects to display
636
+ * - `buttons` (required): Array of `IonicActionButton` objects to display
48
637
  *
49
638
  * ### Outputs
50
- * - `buttonSelect`: Emits the selected `ActionButton` when clicked
639
+ * - `buttonSelect`: Emits the selected `IonicActionButton` when clicked
51
640
  */
52
641
  class ActionButtonListComponent {
53
642
  /** Reference to ActionButtonType.Dropdown for template comparison. */
@@ -110,6 +699,10 @@ class ActionButtonListComponent {
110
699
  <ion-item
111
700
  [button]="true"
112
701
  [disabled]="!canSelectButton(button)"
702
+ [attr.aria-label]="button.ariaLabel"
703
+ [makiTooltip]="button.tooltip || ''"
704
+ [makiTooltipColor]="button.tooltipColor ?? null"
705
+ makiTooltipPosition="left"
113
706
  [detail]="button.type === ActionButtonType.Dropdown"
114
707
  (click)="onButtonClick(button)"
115
708
  >
@@ -128,16 +721,20 @@ class ActionButtonListComponent {
128
721
  </ion-item>
129
722
  }
130
723
  </ion-list>
131
- `, isInline: true, styles: [":host{display:block}::ng-deep ion-popover::part(content){width:fit-content}ion-list{padding:0}ion-item{--padding-start: var(--maki-action-button-list-padding, 16px);--padding-end: var(--maki-action-button-list-padding, 16px);--min-height: var(--maki-action-button-list-item-height, 44px);cursor:pointer;width:100%}ion-item[disabled]{cursor:not-allowed}ion-label{white-space:nowrap}ion-icon{font-size:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}ion-spinner{width:var(--maki-action-button-list-icon-size, 20px);height:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}\n"], dependencies: [{ kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
724
+ `, isInline: true, styles: [":host{display:block}::ng-deep ion-popover::part(content){width:fit-content}ion-list{padding:0}ion-item{--padding-start: var(--maki-action-button-list-padding, 16px);--padding-end: var(--maki-action-button-list-padding, 16px);--min-height: var(--maki-action-button-list-item-height, 44px);cursor:pointer;width:100%}ion-item[disabled]{cursor:not-allowed}ion-label{white-space:nowrap}ion-icon{font-size:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}ion-spinner{width:var(--maki-action-button-list-icon-size, 20px);height:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}\n"], dependencies: [{ kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "directive", type: MakiTooltipDirective, selector: "[makiTooltip]", inputs: ["makiTooltip", "makiTooltipColor", "makiTooltipPosition", "makiTooltipContext"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
132
725
  }
133
726
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListComponent, decorators: [{
134
727
  type: Component,
135
- args: [{ selector: 'maki-action-button-list', standalone: true, imports: [IonList, IonItem, IonIcon, IonLabel, IonSpinner], providers: [ActionButtonListService], changeDetection: ChangeDetectionStrategy.OnPush, template: `
728
+ args: [{ selector: 'maki-action-button-list', standalone: true, imports: [IonList, IonItem, IonIcon, IonLabel, IonSpinner, MakiTooltipDirective], providers: [ActionButtonListService], changeDetection: ChangeDetectionStrategy.OnPush, template: `
136
729
  <ion-list lines="none">
137
730
  @for (button of buttonList(); track button.id) {
138
731
  <ion-item
139
732
  [button]="true"
140
733
  [disabled]="!canSelectButton(button)"
734
+ [attr.aria-label]="button.ariaLabel"
735
+ [makiTooltip]="button.tooltip || ''"
736
+ [makiTooltipColor]="button.tooltipColor ?? null"
737
+ makiTooltipPosition="left"
141
738
  [detail]="button.type === ActionButtonType.Dropdown"
142
739
  (click)="onButtonClick(button)"
143
740
  >
@@ -174,7 +771,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
174
771
  */
175
772
  /**
176
773
  * A configurable button component that renders an `ion-button` based on
177
- * an `ActionButton` configuration object.
774
+ * an `IonicActionButton` configuration object.
178
775
  *
179
776
  * Features:
180
777
  * - Two button types: Default and Dropdown
@@ -197,26 +794,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
197
794
  *
198
795
  * <!-- Dropdown button -->
199
796
  * <maki-button [button]="menuButton" />
797
+ *
798
+ * <!-- Button with tooltip -->
799
+ * <maki-button [button]="buttonWithTooltip" />
200
800
  * ```
201
801
  *
202
802
  * @example
203
803
  * ```typescript
204
804
  * // In your component
205
- * import { ActionButton, ActionButtonType } from '@makigamestudio/ui-core';
805
+ * import { ActionButtonType } from '@makigamestudio/ui-core';
206
806
  * import { IonicActionButton } from '@makigamestudio/ui-ionic';
807
+ * import { TemplateRef, ViewChild } from '@angular/core';
207
808
  *
208
- * // Generic ActionButton (loose typing)
209
- * saveButton: ActionButton = {
809
+ * // Button with string tooltip
810
+ * saveButton: IonicActionButton = {
210
811
  * id: 'save',
211
812
  * label: 'Save',
212
813
  * icon: 'save-outline',
213
814
  * type: ActionButtonType.Default,
815
+ * tooltip: 'Save your changes',
214
816
  * config: { fill: 'solid', color: 'primary' },
215
817
  * handler: async () => {
216
818
  * await this.saveData();
217
819
  * }
218
820
  * };
219
821
  *
822
+ * // Button with colored tooltip
823
+ * deleteButton: IonicActionButton = {
824
+ * id: 'delete',
825
+ * icon: 'trash-outline',
826
+ * type: ActionButtonType.Default,
827
+ * tooltip: 'Permanently delete this item',
828
+ * tooltipColor: 'danger',
829
+ * config: { fill: 'clear', color: 'danger' },
830
+ * handler: () => this.deleteItem()
831
+ * };
832
+ *
833
+ * // Button with template tooltip
834
+ * @ViewChild('richTooltip') richTooltipTemplate!: TemplateRef<unknown>;
835
+ *
836
+ * infoButton: IonicActionButton = {
837
+ * id: 'info',
838
+ * icon: 'information-circle-outline',
839
+ * type: ActionButtonType.Default,
840
+ * tooltip: this.richTooltipTemplate,
841
+ * tooltipColor: 'primary',
842
+ * handler: () => {}
843
+ * };
844
+ *
220
845
  * // IonicActionButton (strict typing for Ionic-specific config)
221
846
  * ionicButton: IonicActionButton = {
222
847
  * id: 'ionic',
@@ -229,7 +854,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
229
854
  * }
230
855
  * };
231
856
  *
232
- * menuButton: ActionButton = {
857
+ * menuButton: IonicActionButton = {
233
858
  * id: 'menu',
234
859
  * label: 'Actions',
235
860
  * type: ActionButtonType.Dropdown,
@@ -243,7 +868,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
243
868
  *
244
869
  * @usageNotes
245
870
  * ### Inputs
246
- * - `button` (required): The `ActionButton` configuration object
871
+ * - `button` (required): The `IonicActionButton` configuration object
247
872
  *
248
873
  * ### Outputs
249
874
  * - `buttonClick`: Emits the button configuration when clicked (for non-dropdown buttons)
@@ -318,9 +943,6 @@ class ButtonComponent {
318
943
  async openDropdown(event) {
319
944
  const btn = this.button();
320
945
  const children = btn.children ?? [];
321
- if (children.length === 0) {
322
- return;
323
- }
324
946
  const alignment = (btn.config?.dropdownAlignment ?? 'end');
325
947
  const popover = await this.popoverCtrl.create({
326
948
  component: ActionButtonListComponent,
@@ -360,7 +982,8 @@ class ButtonComponent {
360
982
  [strong]="button().config?.strong"
361
983
  [disabled]="isDisabled()"
362
984
  [attr.aria-label]="button().ariaLabel"
363
- [title]="button().tooltip ?? ''"
985
+ [makiTooltip]="button().tooltip || ''"
986
+ [makiTooltipColor]="button().tooltipColor ?? null"
364
987
  (click)="onClick($event)"
365
988
  >
366
989
  @if (showLoadingSpinner()) {
@@ -375,11 +998,11 @@ class ButtonComponent {
375
998
  <ion-icon name="chevron-down-outline" slot="end" class="dropdown-icon" />
376
999
  }
377
1000
  </ion-button>
378
- `, isInline: true, styles: ["ion-button{--padding-start: var(--maki-button-padding-start);--padding-end: var(--maki-button-padding-end);text-transform:none}ion-button.button-has-icon-only::part(native){padding:0}ion-spinner{width:var(--maki-button-spinner-size, 16px);height:var(--maki-button-spinner-size, 16px);margin:0 4px 0 0}.button-icon{font-size:var(--maki-button-icon-size, 16px);margin:0 4px 0 0}.button-icon[slot=icon-only]{margin:0}.dropdown-icon{font-size:var(--maki-button-dropdown-icon-size, 16px);margin-inline-start:var(--maki-button-dropdown-icon-gap, 4px)}\n"], dependencies: [{ kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1001
+ `, isInline: true, styles: ["ion-button{--padding-start: var(--maki-button-padding-start);--padding-end: var(--maki-button-padding-end);text-transform:none}ion-button.button-has-icon-only::part(native){padding:0}ion-spinner{width:var(--maki-button-spinner-size, 16px);height:var(--maki-button-spinner-size, 16px);margin:0 4px 0 0}.button-icon{font-size:var(--maki-button-icon-size, 16px);margin:0 4px 0 0}.button-icon[slot=icon-only]{margin:0}.dropdown-icon{font-size:var(--maki-button-dropdown-icon-size, 16px);margin-inline-start:var(--maki-button-dropdown-icon-gap, 4px)}\n"], dependencies: [{ kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "directive", type: MakiTooltipDirective, selector: "[makiTooltip]", inputs: ["makiTooltip", "makiTooltipColor", "makiTooltipPosition", "makiTooltipContext"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
379
1002
  }
380
1003
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonComponent, decorators: [{
381
1004
  type: Component,
382
- args: [{ selector: 'maki-button', standalone: true, imports: [IonButton, IonIcon, IonSpinner], providers: [ButtonStateService, ButtonDisplayService, ButtonHandlerService], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1005
+ args: [{ selector: 'maki-button', standalone: true, imports: [IonButton, IonIcon, IonSpinner, MakiTooltipDirective], providers: [ButtonStateService, ButtonDisplayService, ButtonHandlerService], changeDetection: ChangeDetectionStrategy.OnPush, template: `
383
1006
  <ion-button
384
1007
  [fill]="button().config?.fill"
385
1008
  [size]="button().config?.size"
@@ -389,7 +1012,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
389
1012
  [strong]="button().config?.strong"
390
1013
  [disabled]="isDisabled()"
391
1014
  [attr.aria-label]="button().ariaLabel"
392
- [title]="button().tooltip ?? ''"
1015
+ [makiTooltip]="button().tooltip || ''"
1016
+ [makiTooltipColor]="button().tooltipColor ?? null"
393
1017
  (click)="onClick($event)"
394
1018
  >
395
1019
  @if (showLoadingSpinner()) {
@@ -426,5 +1050,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
426
1050
  * Generated bundle index. Do not edit.
427
1051
  */
428
1052
 
429
- export { ActionButtonListComponent, ButtonComponent };
1053
+ export { ActionButtonListComponent, ButtonComponent, MakiTooltipDirective };
430
1054
  //# sourceMappingURL=makigamestudio-ui-ionic.mjs.map