@makigamestudio/ui-core 0.7.0 → 0.9.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,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, signal } from '@angular/core';
|
|
2
|
+
import { Injectable, signal, inject, DestroyRef } from '@angular/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @file Action button type enumeration
|
|
@@ -653,6 +653,602 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
653
653
|
type: Injectable
|
|
654
654
|
}] });
|
|
655
655
|
|
|
656
|
+
/**
|
|
657
|
+
* @file Device Detection Service
|
|
658
|
+
* @description Detects and tracks device type (mobile/desktop) based on user-agent and viewport width.
|
|
659
|
+
*
|
|
660
|
+
* This service provides a reactive signal that updates when the viewport is resized,
|
|
661
|
+
* allowing components to respond to device type changes. The service uses OR logic,
|
|
662
|
+
* considering a device mobile if EITHER the user-agent indicates a mobile device
|
|
663
|
+
* OR the viewport width is less than 768px.
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* ```typescript
|
|
667
|
+
* // Component usage
|
|
668
|
+
* @Component({
|
|
669
|
+
* selector: 'app-example',
|
|
670
|
+
* template: `
|
|
671
|
+
* @if (deviceDetection.isMobile()) {
|
|
672
|
+
* <p>Mobile view</p>
|
|
673
|
+
* } @else {
|
|
674
|
+
* <p>Desktop view</p>
|
|
675
|
+
* }
|
|
676
|
+
* `
|
|
677
|
+
* })
|
|
678
|
+
* export class ExampleComponent {
|
|
679
|
+
* readonly deviceDetection = inject(DeviceDetectionService);
|
|
680
|
+
* }
|
|
681
|
+
*
|
|
682
|
+
* // Direct usage in computed signals
|
|
683
|
+
* @Component({
|
|
684
|
+
* selector: 'app-layout'
|
|
685
|
+
* })
|
|
686
|
+
* export class LayoutComponent {
|
|
687
|
+
* private readonly deviceDetection = inject(DeviceDetectionService);
|
|
688
|
+
*
|
|
689
|
+
* readonly showSidebar = computed(() =>
|
|
690
|
+
* !this.deviceDetection.isMobile()
|
|
691
|
+
* );
|
|
692
|
+
* }
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
/**
|
|
696
|
+
* Service for detecting and tracking device type based on user-agent and viewport width.
|
|
697
|
+
*
|
|
698
|
+
* The service initializes on construction and sets up a window resize listener to
|
|
699
|
+
* track viewport changes. It uses OR logic to determine if the device is mobile:
|
|
700
|
+
* - User-agent matches mobile device patterns
|
|
701
|
+
* - OR viewport width is less than 768px
|
|
702
|
+
*
|
|
703
|
+
* The resize listener is automatically cleaned up when the service is destroyed.
|
|
704
|
+
*
|
|
705
|
+
* @usageNotes
|
|
706
|
+
*
|
|
707
|
+
* ### Basic Usage
|
|
708
|
+
* Inject the service and use the `isMobile()` signal in templates or computed signals:
|
|
709
|
+
*
|
|
710
|
+
* ```typescript
|
|
711
|
+
* export class MyComponent {
|
|
712
|
+
* readonly deviceDetection = inject(DeviceDetectionService);
|
|
713
|
+
* }
|
|
714
|
+
* ```
|
|
715
|
+
*
|
|
716
|
+
* ### Template Usage
|
|
717
|
+
* ```typescript
|
|
718
|
+
* @if (deviceDetection.isMobile()) {
|
|
719
|
+
* <ion-icon name="phone-portrait-outline" />
|
|
720
|
+
* } @else {
|
|
721
|
+
* <ion-icon name="desktop-outline" />
|
|
722
|
+
* }
|
|
723
|
+
* ```
|
|
724
|
+
*
|
|
725
|
+
* ### Computed Signals
|
|
726
|
+
* ```typescript
|
|
727
|
+
* readonly layout = computed(() =>
|
|
728
|
+
* this.deviceDetection.isMobile() ? 'mobile' : 'desktop'
|
|
729
|
+
* );
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
class DeviceDetectionService {
|
|
733
|
+
destroyRef = inject(DestroyRef);
|
|
734
|
+
/**
|
|
735
|
+
* Regular expression pattern for detecting mobile devices via user-agent.
|
|
736
|
+
* Matches common mobile device identifiers including:
|
|
737
|
+
* - Android devices
|
|
738
|
+
* - iOS devices (iPhone, iPad, iPod)
|
|
739
|
+
* - Windows Phone (IEMobile)
|
|
740
|
+
* - BlackBerry
|
|
741
|
+
* - Opera Mini mobile browser
|
|
742
|
+
* - webOS devices
|
|
743
|
+
*/
|
|
744
|
+
MOBILE_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
745
|
+
/**
|
|
746
|
+
* Breakpoint width (in pixels) below which viewport is considered mobile.
|
|
747
|
+
* @constant
|
|
748
|
+
*/
|
|
749
|
+
MOBILE_BREAKPOINT = 768;
|
|
750
|
+
/**
|
|
751
|
+
* Private writable signal tracking mobile device state.
|
|
752
|
+
*/
|
|
753
|
+
_isMobile = signal(false, ...(ngDevMode ? [{ debugName: "_isMobile" }] : []));
|
|
754
|
+
/**
|
|
755
|
+
* Public readonly signal indicating whether the device is mobile.
|
|
756
|
+
* Returns `true` if EITHER user-agent indicates mobile OR viewport width < 768px.
|
|
757
|
+
*
|
|
758
|
+
* @returns Readonly signal of mobile device state
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* ```typescript
|
|
762
|
+
* const deviceDetection = inject(DeviceDetectionService);
|
|
763
|
+
* const isMobile = deviceDetection.isMobile();
|
|
764
|
+
* console.log(`Device is ${isMobile ? 'mobile' : 'desktop'}`);
|
|
765
|
+
* ```
|
|
766
|
+
*/
|
|
767
|
+
isMobile = this._isMobile.asReadonly();
|
|
768
|
+
constructor() {
|
|
769
|
+
// Initialize mobile state
|
|
770
|
+
this._isMobile.set(this.checkDevice());
|
|
771
|
+
// Set up resize listener
|
|
772
|
+
const resizeHandler = () => {
|
|
773
|
+
this._isMobile.set(this.checkDevice());
|
|
774
|
+
};
|
|
775
|
+
window.addEventListener('resize', resizeHandler);
|
|
776
|
+
// Cleanup on service destruction
|
|
777
|
+
this.destroyRef.onDestroy(() => {
|
|
778
|
+
window.removeEventListener('resize', resizeHandler);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Checks if the device should be considered mobile based on user-agent and viewport width.
|
|
783
|
+
*
|
|
784
|
+
* Uses OR logic: returns `true` if EITHER condition is met:
|
|
785
|
+
* - User-agent matches mobile device pattern
|
|
786
|
+
* - Viewport width is less than 768px
|
|
787
|
+
*
|
|
788
|
+
* @returns `true` if device is mobile, `false` otherwise
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* ```typescript
|
|
792
|
+
* const deviceDetection = inject(DeviceDetectionService);
|
|
793
|
+
* const isMobile = deviceDetection.checkDevice();
|
|
794
|
+
* // isMobile will be true if:
|
|
795
|
+
* // - User-agent contains "Android", "iPhone", etc.
|
|
796
|
+
* // - OR window.innerWidth < 768
|
|
797
|
+
* ```
|
|
798
|
+
*/
|
|
799
|
+
checkDevice() {
|
|
800
|
+
const isMobileUA = this.MOBILE_REGEX.test(navigator.userAgent);
|
|
801
|
+
const isMobileViewport = window.innerWidth < this.MOBILE_BREAKPOINT;
|
|
802
|
+
return isMobileUA || isMobileViewport;
|
|
803
|
+
}
|
|
804
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DeviceDetectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
805
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DeviceDetectionService, providedIn: 'root' });
|
|
806
|
+
}
|
|
807
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DeviceDetectionService, decorators: [{
|
|
808
|
+
type: Injectable,
|
|
809
|
+
args: [{
|
|
810
|
+
providedIn: 'root'
|
|
811
|
+
}]
|
|
812
|
+
}], ctorParameters: () => [] });
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* @file Tooltip Service
|
|
816
|
+
* @description UI-agnostic service for tooltip positioning and display logic.
|
|
817
|
+
*
|
|
818
|
+
* This service provides pure computational functions for tooltip positioning
|
|
819
|
+
* and visibility rules, making it reusable across different UI library
|
|
820
|
+
* implementations (Ionic, Material, PrimeNG, etc.).
|
|
821
|
+
*
|
|
822
|
+
* @example
|
|
823
|
+
* ```typescript
|
|
824
|
+
* // Ionic implementation (MakiTooltipDirective)
|
|
825
|
+
* @Directive({
|
|
826
|
+
* selector: '[makiTooltip]'
|
|
827
|
+
* })
|
|
828
|
+
* export class MakiTooltipDirective {
|
|
829
|
+
* private readonly tooltipService = inject(TooltipService);
|
|
830
|
+
* private readonly deviceDetection = inject(DeviceDetectionService);
|
|
831
|
+
*
|
|
832
|
+
* private positionTooltip(): void {
|
|
833
|
+
* const triggerRect = this.el.nativeElement.getBoundingClientRect();
|
|
834
|
+
* const tooltipRect = this.tooltip.getBoundingClientRect();
|
|
835
|
+
* const position = this.tooltipService.calculatePosition(
|
|
836
|
+
* triggerRect,
|
|
837
|
+
* tooltipRect,
|
|
838
|
+
* window.innerWidth,
|
|
839
|
+
* window.innerHeight
|
|
840
|
+
* );
|
|
841
|
+
* this.applyPosition(position);
|
|
842
|
+
* }
|
|
843
|
+
*
|
|
844
|
+
* private shouldShow(): boolean {
|
|
845
|
+
* const tag = this.el.nativeElement.tagName.toLowerCase();
|
|
846
|
+
* return this.tooltipService.shouldShowTooltip(
|
|
847
|
+
* tag,
|
|
848
|
+
* this.deviceDetection.isMobile(),
|
|
849
|
+
* !!this.content()
|
|
850
|
+
* );
|
|
851
|
+
* }
|
|
852
|
+
* }
|
|
853
|
+
*
|
|
854
|
+
* // Material implementation example
|
|
855
|
+
* @Directive({
|
|
856
|
+
* selector: '[matTooltip]'
|
|
857
|
+
* })
|
|
858
|
+
* export class MatTooltipDirective {
|
|
859
|
+
* private readonly tooltipService = inject(TooltipService);
|
|
860
|
+
*
|
|
861
|
+
* private calculatePosition(): TooltipPosition {
|
|
862
|
+
* return this.tooltipService.calculatePosition(
|
|
863
|
+
* this.triggerRect,
|
|
864
|
+
* this.tooltipRect,
|
|
865
|
+
* window.innerWidth,
|
|
866
|
+
* window.innerHeight
|
|
867
|
+
* );
|
|
868
|
+
* }
|
|
869
|
+
* }
|
|
870
|
+
* ```
|
|
871
|
+
*/
|
|
872
|
+
/**
|
|
873
|
+
* Interactive element tags that should skip tooltips on mobile devices.
|
|
874
|
+
*/
|
|
875
|
+
const INTERACTIVE_ELEMENTS = [
|
|
876
|
+
'button',
|
|
877
|
+
'ion-button',
|
|
878
|
+
'ion-select',
|
|
879
|
+
'a',
|
|
880
|
+
'input',
|
|
881
|
+
'select',
|
|
882
|
+
'textarea'
|
|
883
|
+
];
|
|
884
|
+
/**
|
|
885
|
+
* Default gap between tooltip and trigger element (in pixels).
|
|
886
|
+
*/
|
|
887
|
+
const TOOLTIP_GAP = 5;
|
|
888
|
+
/**
|
|
889
|
+
* Minimum distance from viewport edge (in pixels).
|
|
890
|
+
*/
|
|
891
|
+
const VIEWPORT_PADDING = 5;
|
|
892
|
+
// ============================================================================
|
|
893
|
+
// Service
|
|
894
|
+
// ============================================================================
|
|
895
|
+
/**
|
|
896
|
+
* Service providing UI-agnostic tooltip positioning and display logic.
|
|
897
|
+
*
|
|
898
|
+
* This service contains pure functions for calculating tooltip positions
|
|
899
|
+
* and determining when tooltips should be shown, without any DOM manipulation
|
|
900
|
+
* or framework-specific code.
|
|
901
|
+
*
|
|
902
|
+
* @usageNotes
|
|
903
|
+
*
|
|
904
|
+
* ### Positioning
|
|
905
|
+
* Use `calculatePosition()` to compute tooltip position based on element rectangles:
|
|
906
|
+
* ```typescript
|
|
907
|
+
* const position = tooltipService.calculatePosition(
|
|
908
|
+
* triggerElement.getBoundingClientRect(),
|
|
909
|
+
* tooltipElement.getBoundingClientRect(),
|
|
910
|
+
* window.innerWidth,
|
|
911
|
+
* window.innerHeight
|
|
912
|
+
* );
|
|
913
|
+
* tooltip.style.top = `${position.top}px`;
|
|
914
|
+
* tooltip.style.left = `${position.left}px`;
|
|
915
|
+
* ```
|
|
916
|
+
*
|
|
917
|
+
* ### Visibility Rules
|
|
918
|
+
* Use `shouldShowTooltip()` to determine if tooltip should appear:
|
|
919
|
+
* ```typescript
|
|
920
|
+
* if (tooltipService.shouldShowTooltip(element.tagName, isMobile, hasContent)) {
|
|
921
|
+
* this.showTooltip();
|
|
922
|
+
* }
|
|
923
|
+
* ```
|
|
924
|
+
*/
|
|
925
|
+
class TooltipService {
|
|
926
|
+
/**
|
|
927
|
+
* Calculates the optimal position for a tooltip relative to its trigger element.
|
|
928
|
+
*
|
|
929
|
+
* The function attempts to position the tooltip below the trigger element by default.
|
|
930
|
+
* If the tooltip would overflow the viewport, it adjusts the position:
|
|
931
|
+
* - Positions above if it would overflow the bottom
|
|
932
|
+
* - Aligns to the right edge if it would overflow the right
|
|
933
|
+
*
|
|
934
|
+
* @param triggerRect - Bounding rectangle of the trigger element
|
|
935
|
+
* @param tooltipRect - Bounding rectangle of the tooltip element
|
|
936
|
+
* @param viewportWidth - Current viewport width (typically window.innerWidth)
|
|
937
|
+
* @param viewportHeight - Current viewport height (typically window.innerHeight)
|
|
938
|
+
* @param preferredPlacement - Optional preferred placement before fallback logic
|
|
939
|
+
* @returns Position object with top, left coordinates and final placement
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```typescript
|
|
943
|
+
* const position = tooltipService.calculatePosition(
|
|
944
|
+
* button.getBoundingClientRect(),
|
|
945
|
+
* tooltip.getBoundingClientRect(),
|
|
946
|
+
* window.innerWidth,
|
|
947
|
+
* window.innerHeight,
|
|
948
|
+
* 'top'
|
|
949
|
+
* );
|
|
950
|
+
* // position = { top: 150, left: 100, placement: 'top' }
|
|
951
|
+
* ```
|
|
952
|
+
*/
|
|
953
|
+
calculatePosition(triggerRect, tooltipRect, viewportWidth, viewportHeight, placement = 'bottom') {
|
|
954
|
+
const clampVertical = (top) => {
|
|
955
|
+
const maxTop = viewportHeight - tooltipRect.height - VIEWPORT_PADDING;
|
|
956
|
+
return Math.min(Math.max(VIEWPORT_PADDING, top), maxTop);
|
|
957
|
+
};
|
|
958
|
+
const calculatePositionFor = (placement) => {
|
|
959
|
+
let top;
|
|
960
|
+
let left;
|
|
961
|
+
const centeredTop = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
|
|
962
|
+
switch (placement) {
|
|
963
|
+
case 'top':
|
|
964
|
+
top = triggerRect.top - tooltipRect.height - TOOLTIP_GAP;
|
|
965
|
+
left = triggerRect.left;
|
|
966
|
+
break;
|
|
967
|
+
case 'bottom':
|
|
968
|
+
top = triggerRect.bottom + TOOLTIP_GAP;
|
|
969
|
+
left = triggerRect.left;
|
|
970
|
+
break;
|
|
971
|
+
case 'left':
|
|
972
|
+
top = clampVertical(centeredTop);
|
|
973
|
+
left = triggerRect.left - tooltipRect.width - TOOLTIP_GAP;
|
|
974
|
+
break;
|
|
975
|
+
case 'right':
|
|
976
|
+
top = clampVertical(centeredTop);
|
|
977
|
+
left = triggerRect.right + TOOLTIP_GAP;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
if (placement === 'top' || placement === 'bottom') {
|
|
981
|
+
const wouldOverflowRight = triggerRect.left + tooltipRect.width > viewportWidth;
|
|
982
|
+
if (wouldOverflowRight) {
|
|
983
|
+
const rightAligned = triggerRect.right - tooltipRect.width;
|
|
984
|
+
left = Math.max(VIEWPORT_PADDING, rightAligned);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return { top, left, placement };
|
|
988
|
+
};
|
|
989
|
+
const fitsInViewport = (position) => {
|
|
990
|
+
const minTop = 0;
|
|
991
|
+
const maxTop = viewportHeight - tooltipRect.height;
|
|
992
|
+
// If the tooltip is wider than the viewport, horizontal placements
|
|
993
|
+
// ('left' or 'right') are effectively impossible and should not be
|
|
994
|
+
// considered valid. Only vertical placements are allowed in that case.
|
|
995
|
+
if (tooltipRect.width > viewportWidth) {
|
|
996
|
+
if (position.placement !== 'top' && position.placement !== 'bottom') {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
return position.top >= minTop && position.top <= maxTop;
|
|
1000
|
+
}
|
|
1001
|
+
const minLeft = VIEWPORT_PADDING;
|
|
1002
|
+
const maxLeft = viewportWidth - tooltipRect.width;
|
|
1003
|
+
return (position.left >= minLeft &&
|
|
1004
|
+
position.left <= maxLeft &&
|
|
1005
|
+
position.top >= minTop &&
|
|
1006
|
+
position.top <= maxTop);
|
|
1007
|
+
};
|
|
1008
|
+
const preferredPosition = calculatePositionFor(placement);
|
|
1009
|
+
if (fitsInViewport(preferredPosition)) {
|
|
1010
|
+
return preferredPosition;
|
|
1011
|
+
}
|
|
1012
|
+
if (placement === 'bottom') {
|
|
1013
|
+
const fallbackPosition = calculatePositionFor('top');
|
|
1014
|
+
if (fitsInViewport(fallbackPosition)) {
|
|
1015
|
+
return fallbackPosition;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (placement === 'top') {
|
|
1019
|
+
const fallbackPosition = calculatePositionFor('bottom');
|
|
1020
|
+
if (fitsInViewport(fallbackPosition)) {
|
|
1021
|
+
return fallbackPosition;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (placement === 'left' || placement === 'right') {
|
|
1025
|
+
const opposite = placement === 'left' ? 'right' : 'left';
|
|
1026
|
+
const oppositePosition = calculatePositionFor(opposite);
|
|
1027
|
+
if (fitsInViewport(oppositePosition)) {
|
|
1028
|
+
return oppositePosition;
|
|
1029
|
+
}
|
|
1030
|
+
const bottomPosition = calculatePositionFor('bottom');
|
|
1031
|
+
if (fitsInViewport(bottomPosition)) {
|
|
1032
|
+
return bottomPosition;
|
|
1033
|
+
}
|
|
1034
|
+
const topPosition = calculatePositionFor('top');
|
|
1035
|
+
if (fitsInViewport(topPosition)) {
|
|
1036
|
+
return topPosition;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return preferredPosition;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Determines whether a tooltip should be shown based on context.
|
|
1043
|
+
*
|
|
1044
|
+
* On mobile devices, tooltips are suppressed for interactive elements
|
|
1045
|
+
* (buttons, links, selects) to avoid interfering with their primary click handlers.
|
|
1046
|
+
*
|
|
1047
|
+
* @param elementTag - Lowercase tag name of the trigger element
|
|
1048
|
+
* @param isMobile - Whether the device is currently considered mobile
|
|
1049
|
+
* @param hasContent - Whether the tooltip has content to display
|
|
1050
|
+
* @returns `true` if tooltip should be shown, `false` otherwise
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* ```typescript
|
|
1054
|
+
* const shouldShow = tooltipService.shouldShowTooltip('button', true, true);
|
|
1055
|
+
* // shouldShow = false (button on mobile)
|
|
1056
|
+
*
|
|
1057
|
+
* const shouldShow2 = tooltipService.shouldShowTooltip('div', true, true);
|
|
1058
|
+
* // shouldShow2 = true (non-interactive on mobile)
|
|
1059
|
+
*
|
|
1060
|
+
* const shouldShow3 = tooltipService.shouldShowTooltip('button', false, true);
|
|
1061
|
+
* // shouldShow3 = true (button on desktop)
|
|
1062
|
+
* ```
|
|
1063
|
+
*/
|
|
1064
|
+
shouldShowTooltip(elementTag, isMobile, hasContent) {
|
|
1065
|
+
// No content = no tooltip
|
|
1066
|
+
if (!hasContent) {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
// On mobile, skip interactive elements
|
|
1070
|
+
if (isMobile && this.isInteractiveElement(elementTag)) {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Checks if an element tag represents an interactive element.
|
|
1077
|
+
*
|
|
1078
|
+
* Interactive elements (buttons, links, form controls) typically have
|
|
1079
|
+
* their own click/tap handlers, and showing tooltips on mobile can
|
|
1080
|
+
* interfere with the primary interaction.
|
|
1081
|
+
*
|
|
1082
|
+
* @param elementTag - Lowercase tag name to check
|
|
1083
|
+
* @returns `true` if the element is interactive, `false` otherwise
|
|
1084
|
+
*
|
|
1085
|
+
* @example
|
|
1086
|
+
* ```typescript
|
|
1087
|
+
* tooltipService.isInteractiveElement('button'); // true
|
|
1088
|
+
* tooltipService.isInteractiveElement('ion-button'); // true
|
|
1089
|
+
* tooltipService.isInteractiveElement('div'); // false
|
|
1090
|
+
* tooltipService.isInteractiveElement('span'); // false
|
|
1091
|
+
* ```
|
|
1092
|
+
*/
|
|
1093
|
+
isInteractiveElement(elementTag) {
|
|
1094
|
+
return INTERACTIVE_ELEMENTS.includes(elementTag.toLowerCase());
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Checks if a tooltip element is visible (has non-zero dimensions).
|
|
1098
|
+
*
|
|
1099
|
+
* This is useful for detecting if an element has been hidden or removed
|
|
1100
|
+
* from the layout, which should trigger tooltip dismissal.
|
|
1101
|
+
*
|
|
1102
|
+
* @param rect - Bounding rectangle of the element
|
|
1103
|
+
* @returns `true` if element has visible dimensions, `false` otherwise
|
|
1104
|
+
*
|
|
1105
|
+
* @example
|
|
1106
|
+
* ```typescript
|
|
1107
|
+
* const isVisible = tooltipService.isElementVisible(
|
|
1108
|
+
* element.getBoundingClientRect()
|
|
1109
|
+
* );
|
|
1110
|
+
* if (!isVisible) {
|
|
1111
|
+
* this.hideTooltip();
|
|
1112
|
+
* }
|
|
1113
|
+
* ```
|
|
1114
|
+
*/
|
|
1115
|
+
isElementVisible(rect) {
|
|
1116
|
+
return rect.width > 0 && rect.height > 0;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
*
|
|
1120
|
+
* @returns Tooltip show delay in milliseconds.
|
|
1121
|
+
*/
|
|
1122
|
+
getShowDelayMs() {
|
|
1123
|
+
return 250;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* @returns Tooltip close delay in milliseconds.
|
|
1127
|
+
*/
|
|
1128
|
+
getCloseDelayMs() {
|
|
1129
|
+
return 200;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* @returns Tooltip fade duration in milliseconds.
|
|
1133
|
+
*/
|
|
1134
|
+
getFadeDurationMs() {
|
|
1135
|
+
return 150;
|
|
1136
|
+
}
|
|
1137
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TooltipService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1138
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TooltipService, providedIn: 'root' });
|
|
1139
|
+
}
|
|
1140
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TooltipService, decorators: [{
|
|
1141
|
+
type: Injectable,
|
|
1142
|
+
args: [{
|
|
1143
|
+
providedIn: 'root'
|
|
1144
|
+
}]
|
|
1145
|
+
}] });
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Singleton service that centralizes tooltip scheduling (show/hide delays).
|
|
1149
|
+
* Purely orchestrates timers and delegates to provided callbacks; no DOM access.
|
|
1150
|
+
*/
|
|
1151
|
+
class TooltipSchedulerService {
|
|
1152
|
+
tooltipService;
|
|
1153
|
+
constructor(tooltipService) {
|
|
1154
|
+
this.tooltipService = tooltipService;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Create an isolated scheduler handle for a single tooltip instance.
|
|
1158
|
+
* The handle exposes the same event methods but keeps timers/state per-instance.
|
|
1159
|
+
*/
|
|
1160
|
+
createHandle(show, hide) {
|
|
1161
|
+
let showTimeout = null;
|
|
1162
|
+
let closeTimeout = null;
|
|
1163
|
+
let isTriggerHovering = false;
|
|
1164
|
+
let isTooltipHovering = false;
|
|
1165
|
+
let isTouching = false;
|
|
1166
|
+
const clearShowTimeout = () => {
|
|
1167
|
+
if (showTimeout) {
|
|
1168
|
+
clearTimeout(showTimeout);
|
|
1169
|
+
showTimeout = null;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
const clearCloseTimeout = () => {
|
|
1173
|
+
if (closeTimeout) {
|
|
1174
|
+
clearTimeout(closeTimeout);
|
|
1175
|
+
closeTimeout = null;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
const scheduleShow = () => {
|
|
1179
|
+
clearShowTimeout();
|
|
1180
|
+
const ms = this.tooltipService.getShowDelayMs();
|
|
1181
|
+
showTimeout = setTimeout(() => {
|
|
1182
|
+
showTimeout = null;
|
|
1183
|
+
invokeShow();
|
|
1184
|
+
}, ms);
|
|
1185
|
+
};
|
|
1186
|
+
const scheduleClose = () => {
|
|
1187
|
+
clearCloseTimeout();
|
|
1188
|
+
const ms = this.tooltipService.getCloseDelayMs();
|
|
1189
|
+
closeTimeout = setTimeout(() => {
|
|
1190
|
+
closeTimeout = null;
|
|
1191
|
+
if (!isTriggerHovering && !isTooltipHovering && !isTouching) {
|
|
1192
|
+
invokeHide();
|
|
1193
|
+
}
|
|
1194
|
+
}, ms);
|
|
1195
|
+
};
|
|
1196
|
+
const invokeShow = () => show();
|
|
1197
|
+
const invokeHide = () => hide();
|
|
1198
|
+
return {
|
|
1199
|
+
destroy() {
|
|
1200
|
+
clearShowTimeout();
|
|
1201
|
+
clearCloseTimeout();
|
|
1202
|
+
},
|
|
1203
|
+
onTriggerEnter() {
|
|
1204
|
+
isTriggerHovering = true;
|
|
1205
|
+
clearCloseTimeout();
|
|
1206
|
+
clearShowTimeout();
|
|
1207
|
+
scheduleShow();
|
|
1208
|
+
},
|
|
1209
|
+
onTriggerLeave() {
|
|
1210
|
+
isTriggerHovering = false;
|
|
1211
|
+
clearShowTimeout();
|
|
1212
|
+
scheduleClose();
|
|
1213
|
+
},
|
|
1214
|
+
onTooltipEnter() {
|
|
1215
|
+
isTooltipHovering = true;
|
|
1216
|
+
clearCloseTimeout();
|
|
1217
|
+
},
|
|
1218
|
+
onTooltipLeave() {
|
|
1219
|
+
isTooltipHovering = false;
|
|
1220
|
+
scheduleClose();
|
|
1221
|
+
},
|
|
1222
|
+
onTooltipTouchStart() {
|
|
1223
|
+
isTouching = true;
|
|
1224
|
+
},
|
|
1225
|
+
onTooltipTouchEnd() {
|
|
1226
|
+
isTouching = false;
|
|
1227
|
+
scheduleClose();
|
|
1228
|
+
},
|
|
1229
|
+
onClick(isMobile) {
|
|
1230
|
+
if (isMobile) {
|
|
1231
|
+
isTouching = true;
|
|
1232
|
+
invokeShow();
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
// Desktop toggle behavior
|
|
1236
|
+
if (closeTimeout) {
|
|
1237
|
+
clearCloseTimeout();
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
invokeShow();
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TooltipSchedulerService, deps: [{ token: TooltipService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1245
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TooltipSchedulerService, providedIn: 'root' });
|
|
1246
|
+
}
|
|
1247
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TooltipSchedulerService, decorators: [{
|
|
1248
|
+
type: Injectable,
|
|
1249
|
+
args: [{ providedIn: 'root' }]
|
|
1250
|
+
}], ctorParameters: () => [{ type: TooltipService }] });
|
|
1251
|
+
|
|
656
1252
|
/**
|
|
657
1253
|
* @file Services Barrel Export
|
|
658
1254
|
* @description Exports all services from the ui-core library.
|
|
@@ -670,5 +1266,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
670
1266
|
* Generated bundle index. Do not edit.
|
|
671
1267
|
*/
|
|
672
1268
|
|
|
673
|
-
export { ActionButtonListService, ActionButtonType, ButtonDisplayService, ButtonHandlerService, ButtonStateService };
|
|
1269
|
+
export { ActionButtonListService, ActionButtonType, ButtonDisplayService, ButtonHandlerService, ButtonStateService, DeviceDetectionService, TooltipSchedulerService, TooltipService };
|
|
674
1270
|
//# sourceMappingURL=makigamestudio-ui-core.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"makigamestudio-ui-core.mjs","sources":["../../../projects/ui-core/src/lib/interfaces/action-button-type.enum.ts","../../../projects/ui-core/src/lib/services/action-button-list.service.ts","../../../projects/ui-core/src/lib/services/button-display.service.ts","../../../projects/ui-core/src/lib/services/button-handler.service.ts","../../../projects/ui-core/src/lib/services/button-state.service.ts","../../../projects/ui-core/src/lib/services/index.ts","../../../projects/ui-core/src/public-api.ts","../../../projects/ui-core/src/makigamestudio-ui-core.ts"],"sourcesContent":["/**\n * @file Action button type enumeration\n * @description Defines the display types for action buttons\n */\n\n/**\n * Enumeration of action button types.\n *\n * Both types support flexible display formats:\n * - Icon + Label: Provide both `icon` and `label` properties\n * - Icon only: Provide `icon` without `label`\n * - Label only: Provide `label` without `icon`\n *\n * @example\n * ```typescript\n * // Default button with label and icon\n * const saveButton: ActionButton = {\n * id: 'save-btn',\n * label: 'Save',\n * icon: 'save-outline',\n * type: ActionButtonType.Default,\n * handler: () => console.log('Saved!')\n * };\n *\n * // Icon-only button (no label)\n * const iconButton: ActionButton = {\n * id: 'settings-btn',\n * icon: 'settings-outline',\n * type: ActionButtonType.Default,\n * ariaLabel: 'Settings',\n * handler: () => console.log('Settings')\n * };\n *\n * // Label-only button (no icon)\n * const textButton: ActionButton = {\n * id: 'cancel-btn',\n * label: 'Cancel',\n * type: ActionButtonType.Default,\n * handler: () => console.log('Cancelled')\n * };\n * ```\n */\nexport enum ActionButtonType {\n /**\n * Standard action button.\n * Display format (icon-only, label-only, or icon+label) is determined by which properties are provided.\n */\n Default = 'default',\n\n /**\n * Button that opens a dropdown popover with child actions.\n * Like Default, supports icon-only, label-only, or icon+label display.\n */\n Dropdown = 'dropdown'\n}\n","/**\n * @file Action Button List Service\n * @description Provides logic for managing lists of action buttons in dropdowns/popovers.\n *\n * This service encapsulates all list-related logic for action buttons, making it\n * reusable across different UI library implementations (Ionic popover, Material menu,\n * PrimeNG overlay, etc.).\n *\n * @example\n * ```typescript\n * // Ionic implementation (ActionButtonListComponent)\n * @Component({\n * providers: [ActionButtonListService],\n * template: `\n * <ion-list>\n * @for (button of buttonList(); track button.id) {\n * <ion-item\n * [disabled]=\"!canSelectButton(button)\"\n * (click)=\"onSelect(button)\"\n * >\n * @if (isButtonLoading(button)) {\n * <ion-spinner slot=\"start\" />\n * }\n * <ion-label>{{ button.label }}</ion-label>\n * </ion-item>\n * }\n * </ion-list>\n * `\n * })\n * export class ActionButtonListComponent {\n * private readonly listService = inject(ActionButtonListService);\n *\n * readonly buttonList = computed(() =>\n * this.listService.resolveButtonList(this._propsButtons(), this.buttons())\n * );\n *\n * protected isButtonLoading(button: ActionButton): boolean {\n * return this.listService.isButtonLoading(button, this.loadingChildIds());\n * }\n * }\n *\n * // Material Menu implementation example\n * @Component({\n * selector: 'mat-action-menu',\n * providers: [ActionButtonListService],\n * template: `\n * <mat-menu #menu=\"matMenu\">\n * @for (button of buttonList(); track button.id) {\n * <button mat-menu-item\n * [disabled]=\"!canSelectButton(button)\"\n * (click)=\"onSelect(button)\"\n * >\n * @if (isButtonLoading(button)) {\n * <mat-spinner diameter=\"16\" />\n * } @else if (button.icon) {\n * <mat-icon>{{ button.icon }}</mat-icon>\n * }\n * <span>{{ button.label }}</span>\n * </button>\n * }\n * </mat-menu>\n * `\n * })\n * export class MatActionMenuComponent {\n * private readonly listService = inject(ActionButtonListService);\n * // ... similar implementation\n * }\n * ```\n */\n\nimport { Injectable, Signal } from '@angular/core';\n\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Service that provides logic for managing lists of action buttons.\n *\n * This service is designed to be provided at the component level to maintain\n * consistency with other button services.\n *\n * @usageNotes\n * ### Button List Resolution\n * The service handles two input sources for buttons:\n * 1. Direct property assignment (via PopoverController.create componentProps)\n * 2. Angular signal input binding\n *\n * Props take precedence over input when both are provided.\n *\n * ### Loading State\n * Loading state can come from:\n * 1. The button's own `loading` property\n * 2. A set of loading child IDs passed from the parent button\n *\n * ### Selection Validation\n * A button can be selected only if it's not loading and not disabled.\n */\n@Injectable()\nexport class ActionButtonListService {\n /**\n * Resolves the button list from either props or input.\n * Props (from PopoverController) take precedence over input binding.\n *\n * @param propsButtons - Buttons passed via component props\n * @param inputButtons - Buttons passed via signal input\n * @returns The resolved button array\n */\n resolveButtonList(\n propsButtons: readonly ActionButton[],\n inputButtons: readonly ActionButton[]\n ): readonly ActionButton[] {\n return propsButtons.length > 0 ? propsButtons : inputButtons;\n }\n\n /**\n * Checks if a button is currently in a loading state.\n *\n * @param button - The button to check\n * @param loadingChildIds - Set of child IDs currently loading (optional)\n * @returns True if the button is loading\n */\n isButtonLoading(button: ActionButton, loadingChildIds?: ReadonlySet<string>): boolean {\n const ids = loadingChildIds ?? new Set<string>();\n return (button.loading ?? false) || ids.has(button.id);\n }\n\n /**\n * Checks if a button is currently in a loading state using a signal.\n * Convenience method when loadingChildIds is provided as a signal.\n *\n * @param button - The button to check\n * @param loadingChildIdsSignal - Signal containing loading child IDs (optional)\n * @returns True if the button is loading\n */\n isButtonLoadingFromSignal(\n button: ActionButton,\n loadingChildIdsSignal: Signal<ReadonlySet<string>> | null\n ): boolean {\n const loadingChildIds = loadingChildIdsSignal ? loadingChildIdsSignal() : new Set<string>();\n return this.isButtonLoading(button, loadingChildIds);\n }\n\n /**\n * Determines if a button can be selected (clicked).\n *\n * @param button - The button to check\n * @param loadingChildIds - Set of child IDs currently loading (optional)\n * @returns True if the button can be selected\n */\n canSelectButton(button: ActionButton, loadingChildIds?: ReadonlySet<string>): boolean {\n const isLoading = this.isButtonLoading(button, loadingChildIds);\n const isDisabled = button.config?.disabled ?? false;\n return !isLoading && !isDisabled;\n }\n\n /**\n * Determines whether to show a loading spinner for a button.\n *\n * @param button - The button to check\n * @param loadingChildIds - Set of child IDs currently loading (optional)\n * @returns True if spinner should be shown\n */\n shouldShowSpinner(button: ActionButton, loadingChildIds?: ReadonlySet<string>): boolean {\n const isLoading = this.isButtonLoading(button, loadingChildIds);\n const showSpinner = button.showLoadingSpinner ?? true;\n return isLoading && showSpinner;\n }\n}\n","/**\n * @file Button Display Service\n * @description Provides display logic for action buttons (icon slots, label visibility, etc.).\n *\n * This service encapsulates all display-related logic for buttons, making it\n * reusable across different UI library implementations.\n *\n * @example\n * ```typescript\n * // Ionic implementation\n * @Component({\n * providers: [ButtonDisplayService],\n * template: `\n * <ion-button\n [fill]=\"button().config?.fill\"\n [size]=\"button().config?.size\"\n [color]=\"button().config?.color\"\n [shape]=\"button().config?.shape\"\n [expand]=\"button().config?.expand\"\n [strong]=\"button().config?.strong\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"button().ariaLabel\"\n [title]=\"button().tooltip ?? ''\"\n (click)=\"onClick($event)\"\n >\n @if (showLoadingSpinner()) {\n <ion-spinner [slot]=\"iconSlot()\" name=\"crescent\" />\n } @else if (button().icon) {\n <ion-icon [name]=\"button().icon\" [slot]=\"iconSlot()\" class=\"button-icon\" />\n }\n @if (showLabel()) {\n {{ button().label }}\n }\n @if (showDropdownIcon()) {\n <ion-icon name=\"chevron-down-outline\" slot=\"end\" class=\"dropdown-icon\" />\n }\n </ion-button>\n * `\n * })\n * export class ButtonComponent {\n * private readonly displayService = inject(ButtonDisplayService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly showLabel = computed(() =>\n * this.displayService.shouldShowLabel(this.button())\n * );\n * }\n *\n * // PrimeNG implementation example\n * @Component({\n * selector: 'p-action-button',\n * providers: [ButtonDisplayService],\n * template: `\n * <p-button\n * [icon]=\"button().icon\"\n * [iconPos]=\"showLabel() ? 'left' : undefined\"\n * [label]=\"showLabel() ? button().label : null\"\n * />\n * `\n * })\n * export class PrimeActionButtonComponent {\n * private readonly displayService = inject(ButtonDisplayService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly showLabel = computed(() =>\n * this.displayService.shouldShowLabel(this.button())\n * );\n * }\n * ```\n */\n\nimport { Injectable } from '@angular/core';\n\nimport { ActionButtonType } from '../interfaces/action-button-type.enum';\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Service that provides display logic for action buttons.\n *\n * This service is designed to be provided at the component level to maintain\n * consistency with other button services, though it contains only pure functions.\n *\n * @usageNotes\n * ### Pure Functions\n * All methods in this service are pure functions that take a button configuration\n * and return display values. They can be safely used in Angular's `computed()`.\n *\n * ### Dropdown Icon\n * Dropdown buttons show a chevron icon unless `config.hideDropdownIcon` is true.\n */\n@Injectable()\nexport class ButtonDisplayService {\n /**\n * Determines whether to show the label text.\n *\n * @param button - The button configuration\n * @returns True when a label is provided\n */\n shouldShowLabel(button: ActionButton): boolean {\n return !!button.label;\n }\n\n /**\n * Determines whether to show the dropdown chevron icon.\n *\n * @param button - The button configuration\n * @returns True for dropdown buttons without hideDropdownIcon\n */\n shouldShowDropdownIcon(button: ActionButton): boolean {\n return button.type === ActionButtonType.Dropdown && !button.config?.hideDropdownIcon;\n }\n}\n","/**\n * @file Button Handler Service\n * @description Executes button handlers with automatic loading state management.\n *\n * This service encapsulates handler execution logic, providing automatic\n * loading state management for async handlers across different UI implementations.\n *\n * @example\n * ```typescript\n * // Ionic implementation\n * @Component({\n * providers: [ButtonStateService, ButtonHandlerService]\n * })\n * export class ButtonComponent {\n * private readonly stateService = inject(ButtonStateService);\n * private readonly handlerService = inject(ButtonHandlerService);\n *\n * protected async onClick(): Promise<void> {\n * await this.handlerService.executeHandler(\n * this.button(),\n * loading => this.stateService.setLoading(loading)\n * );\n * this.buttonClick.emit(this.button());\n * }\n * }\n *\n * // Generic web component example\n * class ActionButtonElement extends HTMLElement {\n * private handlerService = new ButtonHandlerService();\n * private loading = false;\n *\n * async handleClick() {\n * await this.handlerService.executeHandler(\n * this.buttonConfig,\n * loading => {\n * this.loading = loading;\n * this.render();\n * }\n * );\n * }\n * }\n * ```\n */\n\nimport { Injectable } from '@angular/core';\n\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Callback function type for loading state changes.\n */\nexport type LoadingCallback = (loading: boolean) => void;\n\n/**\n * Callback function type for child loading state changes.\n */\nexport type ChildLoadingCallback = (childId: string, loading: boolean) => void;\n\n/**\n * Service that executes button handlers with automatic loading state management.\n *\n * This service is designed to be provided at the component level to ensure\n * proper isolation of handler execution context.\n *\n * @usageNotes\n * ### Async Handler Support\n * When a handler returns a Promise, the service automatically:\n * 1. Calls `onLoadingChange(true)` before execution\n * 2. Awaits the Promise\n * 3. Calls `onLoadingChange(false)` after completion (success or failure)\n *\n * ### Sync Handler Support\n * For synchronous handlers, no loading state changes are triggered.\n *\n * ### Error Handling\n * The service uses try/finally to ensure loading state is always reset,\n * even if the handler throws an error. The error is not caught, allowing\n * it to propagate to the calling code.\n */\n@Injectable()\nexport class ButtonHandlerService {\n /**\n * Executes a button's handler with automatic loading state management.\n *\n * @param button - The button whose handler to execute\n * @param onLoadingChange - Callback invoked when loading state changes\n * @returns Promise that resolves when handler completes\n *\n * @example\n * ```typescript\n * await handlerService.executeHandler(\n * myButton,\n * loading => this.isLoading.set(loading)\n * );\n * ```\n */\n async executeHandler(button: ActionButton, onLoadingChange: LoadingCallback): Promise<void> {\n const result = button.handler();\n\n if (result instanceof Promise) {\n onLoadingChange(true);\n try {\n await result;\n } finally {\n onLoadingChange(false);\n }\n }\n }\n\n /**\n * Executes a child button's handler with loading state tracking by ID.\n *\n * This method is designed for dropdown children where multiple buttons\n * might be loading simultaneously and need individual tracking.\n *\n * @param child - The child button whose handler to execute\n * @param onChildLoadingChange - Callback with child ID and loading state\n * @returns Promise that resolves when handler completes\n *\n * @example\n * ```typescript\n * await handlerService.executeChildHandler(\n * selectedChild,\n * (childId, loading) => {\n * if (loading) {\n * this.stateService.addLoadingChild(childId);\n * } else {\n * this.stateService.removeLoadingChild(childId);\n * }\n * }\n * );\n * ```\n */\n async executeChildHandler(\n child: ActionButton,\n onChildLoadingChange: ChildLoadingCallback\n ): Promise<void> {\n onChildLoadingChange(child.id, true);\n try {\n const result = child.handler();\n if (result instanceof Promise) {\n await result;\n }\n } finally {\n onChildLoadingChange(child.id, false);\n }\n }\n}\n","/**\n * @file Button State Service\n * @description Manages loading states and computed state values for action buttons.\n *\n * This service encapsulates all state management logic for buttons, making it\n * reusable across different UI library implementations (Ionic, Material, PrimeNG, etc.).\n *\n * @example\n * ```typescript\n * // Ionic implementation (ButtonComponent)\n * @Component({\n * providers: [ButtonStateService]\n * })\n * export class ButtonComponent {\n * private readonly stateService = inject(ButtonStateService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly isLoading = computed(() =>\n * this.stateService.isLoading(this.button())\n * );\n * }\n *\n * // Material implementation example\n * @Component({\n * selector: 'mat-action-button',\n * providers: [ButtonStateService],\n * template: `\n * <button mat-button [disabled]=\"isDisabled()\">\n * @if (showLoadingSpinner()) {\n * <mat-spinner diameter=\"16\" />\n * }\n * {{ button().label }}\n * </button>\n * `\n * })\n * export class MatActionButtonComponent {\n * private readonly stateService = inject(ButtonStateService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly isDisabled = computed(() =>\n * this.stateService.isDisabled(this.button())\n * );\n * readonly showLoadingSpinner = computed(() =>\n * this.stateService.showLoadingSpinner(this.button())\n * );\n * }\n * ```\n */\n\nimport { Injectable, signal } from '@angular/core';\n\nimport { ActionButtonType } from '../interfaces/action-button-type.enum';\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Service that manages button loading states and computes derived state values.\n *\n * This service is designed to be provided at the component level (not root)\n * to ensure each button instance has isolated state management.\n *\n * @usageNotes\n * ### Providing the Service\n * Always provide this service at the component level to isolate state:\n * ```typescript\n * @Component({\n * providers: [ButtonStateService]\n * })\n * ```\n *\n * ### State Management\n * The service manages two types of loading state:\n * 1. **Internal loading** - Set when executing async handlers via `setLoading()`\n * 2. **Child loading** - Tracks which dropdown children are loading via `setChildLoading()`\n *\n * ### Computed States\n * All computed methods are pure functions that can be used in Angular's `computed()`:\n * - `isLoading(button)` - Combined loading state\n * - `isDisabled(button)` - Whether button should be disabled\n * - `showLoadingSpinner(button)` - Whether to show spinner\n * - `hasLoadingChild(button)` - Whether any child is loading\n */\n@Injectable()\nexport class ButtonStateService {\n /**\n * Internal loading state for async handler execution.\n */\n private readonly _isLoading = signal<boolean>(false);\n\n /**\n * Set of child button IDs that are currently loading (for dropdowns).\n */\n private readonly _loadingChildIds = signal<ReadonlySet<string>>(new Set());\n\n /**\n * Read-only access to internal loading state.\n */\n readonly internalLoading = this._isLoading.asReadonly();\n\n /**\n * Read-only access to loading child IDs.\n */\n readonly loadingChildIds = this._loadingChildIds.asReadonly();\n\n /**\n * Sets the internal loading state.\n *\n * @param loading - Whether the button is loading\n */\n setLoading(loading: boolean): void {\n this._isLoading.set(loading);\n }\n\n /**\n * Adds a child ID to the loading set.\n *\n * @param childId - The ID of the child button that started loading\n */\n addLoadingChild(childId: string): void {\n this._loadingChildIds.update(ids => new Set([...ids, childId]));\n }\n\n /**\n * Removes a child ID from the loading set.\n *\n * @param childId - The ID of the child button that finished loading\n */\n removeLoadingChild(childId: string): void {\n this._loadingChildIds.update(ids => {\n const newIds = new Set(ids);\n newIds.delete(childId);\n return newIds;\n });\n }\n\n /**\n * Checks if any child button is in a loading state with spinner enabled.\n *\n * @param button - The parent button configuration\n * @returns True if any child is loading and should show spinner\n */\n hasLoadingChild(button: ActionButton): boolean {\n if (button.type !== ActionButtonType.Dropdown || !button.children) {\n return false;\n }\n\n const loadingChildIds = this._loadingChildIds();\n\n return button.children.some(child => {\n const showSpinner = child.showLoadingSpinner ?? true;\n const isLoadingFromProp = child.loading && showSpinner;\n const isLoadingFromExecution = loadingChildIds.has(child.id) && showSpinner;\n return isLoadingFromProp || isLoadingFromExecution;\n });\n }\n\n /**\n * Computes the combined loading state for a button.\n *\n * @param button - The button configuration\n * @returns True if the button is in any loading state\n */\n isLoading(button: ActionButton): boolean {\n const parentLoading = this._isLoading() || (button.loading ?? false);\n const childLoading = this.hasLoadingChild(button) && (button.showLoadingSpinner ?? true);\n return parentLoading || childLoading;\n }\n\n /**\n * Determines whether to display the loading spinner.\n *\n * @param button - The button configuration\n * @returns True if spinner should be shown\n */\n showLoadingSpinner(button: ActionButton): boolean {\n return this.isLoading(button) && (button.showLoadingSpinner ?? true);\n }\n\n /**\n * Computes whether the button should be disabled.\n *\n * For dropdown buttons: Only disabled if config says so or if parent itself is loading\n * (not disabled by inherited child loading - user can still open dropdown).\n *\n * For non-dropdown buttons: Disabled when loading or explicitly disabled in config.\n *\n * @param button - The button configuration\n * @returns True if the button should be disabled\n */\n isDisabled(button: ActionButton): boolean {\n const configDisabled = button.config?.disabled ?? false;\n\n if (button.type === ActionButtonType.Dropdown) {\n const parentSelfLoading = this._isLoading() || (button.loading ?? false);\n return configDisabled || parentSelfLoading;\n }\n\n return this.isLoading(button) || configDisabled;\n }\n\n /**\n * Checks if a specific button is currently loading.\n * Used primarily for checking child button loading state.\n *\n * @param button - The button to check\n * @returns True if the button is loading\n */\n isButtonLoading(button: ActionButton): boolean {\n const loadingChildIds = this._loadingChildIds();\n return (button.loading ?? false) || loadingChildIds.has(button.id);\n }\n}\n","/**\n * @file Services Barrel Export\n * @description Exports all services from the ui-core library.\n */\n\nexport { ActionButtonListService } from './action-button-list.service';\nexport { ButtonDisplayService } from './button-display.service';\nexport { ButtonHandlerService } from './button-handler.service';\nexport type { ChildLoadingCallback, LoadingCallback } from './button-handler.service';\nexport { ButtonStateService } from './button-state.service';\n\n","/*\n * Public API Surface of @makigamestudio/ui-core\n *\n * This package provides UI-agnostic interfaces, services, and design tokens\n * for building action button components across different UI frameworks.\n */\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\n// Action Button Configuration (generic, UI-agnostic)\nexport type {\n ActionButtonConfig,\n BaseActionButtonConfig\n} from './lib/interfaces/action-button-config.interface';\n\n// Action Button Type Enum\nexport { ActionButtonType } from './lib/interfaces/action-button-type.enum';\n\n// Action Button Interface (generic, UI-agnostic)\nexport type { ActionButton } from './lib/interfaces/action-button.interface';\n\n// ============================================================================\n// Services\n// ============================================================================\n\nexport {\n ActionButtonListService,\n ButtonDisplayService,\n ButtonHandlerService,\n ButtonStateService\n} from './lib/services';\n\nexport type { ChildLoadingCallback, LoadingCallback } from './lib/services';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"makigamestudio-ui-core.mjs","sources":["../../../projects/ui-core/src/lib/interfaces/action-button-type.enum.ts","../../../projects/ui-core/src/lib/services/action-button-list.service.ts","../../../projects/ui-core/src/lib/services/button-display.service.ts","../../../projects/ui-core/src/lib/services/button-handler.service.ts","../../../projects/ui-core/src/lib/services/button-state.service.ts","../../../projects/ui-core/src/lib/services/device-detection.service.ts","../../../projects/ui-core/src/lib/services/tooltip.service.ts","../../../projects/ui-core/src/lib/services/tooltip-scheduler.service.ts","../../../projects/ui-core/src/lib/services/index.ts","../../../projects/ui-core/src/public-api.ts","../../../projects/ui-core/src/makigamestudio-ui-core.ts"],"sourcesContent":["/**\n * @file Action button type enumeration\n * @description Defines the display types for action buttons\n */\n\n/**\n * Enumeration of action button types.\n *\n * Both types support flexible display formats:\n * - Icon + Label: Provide both `icon` and `label` properties\n * - Icon only: Provide `icon` without `label`\n * - Label only: Provide `label` without `icon`\n *\n * @example\n * ```typescript\n * // Default button with label and icon\n * const saveButton: ActionButton = {\n * id: 'save-btn',\n * label: 'Save',\n * icon: 'save-outline',\n * type: ActionButtonType.Default,\n * handler: () => console.log('Saved!')\n * };\n *\n * // Icon-only button (no label)\n * const iconButton: ActionButton = {\n * id: 'settings-btn',\n * icon: 'settings-outline',\n * type: ActionButtonType.Default,\n * ariaLabel: 'Settings',\n * handler: () => console.log('Settings')\n * };\n *\n * // Label-only button (no icon)\n * const textButton: ActionButton = {\n * id: 'cancel-btn',\n * label: 'Cancel',\n * type: ActionButtonType.Default,\n * handler: () => console.log('Cancelled')\n * };\n * ```\n */\nexport enum ActionButtonType {\n /**\n * Standard action button.\n * Display format (icon-only, label-only, or icon+label) is determined by which properties are provided.\n */\n Default = 'default',\n\n /**\n * Button that opens a dropdown popover with child actions.\n * Like Default, supports icon-only, label-only, or icon+label display.\n */\n Dropdown = 'dropdown'\n}\n","/**\n * @file Action Button List Service\n * @description Provides logic for managing lists of action buttons in dropdowns/popovers.\n *\n * This service encapsulates all list-related logic for action buttons, making it\n * reusable across different UI library implementations (Ionic popover, Material menu,\n * PrimeNG overlay, etc.).\n *\n * @example\n * ```typescript\n * // Ionic implementation (ActionButtonListComponent)\n * @Component({\n * providers: [ActionButtonListService],\n * template: `\n * <ion-list>\n * @for (button of buttonList(); track button.id) {\n * <ion-item\n * [disabled]=\"!canSelectButton(button)\"\n * (click)=\"onSelect(button)\"\n * >\n * @if (isButtonLoading(button)) {\n * <ion-spinner slot=\"start\" />\n * }\n * <ion-label>{{ button.label }}</ion-label>\n * </ion-item>\n * }\n * </ion-list>\n * `\n * })\n * export class ActionButtonListComponent {\n * private readonly listService = inject(ActionButtonListService);\n *\n * readonly buttonList = computed(() =>\n * this.listService.resolveButtonList(this._propsButtons(), this.buttons())\n * );\n *\n * protected isButtonLoading(button: ActionButton): boolean {\n * return this.listService.isButtonLoading(button, this.loadingChildIds());\n * }\n * }\n *\n * // Material Menu implementation example\n * @Component({\n * selector: 'mat-action-menu',\n * providers: [ActionButtonListService],\n * template: `\n * <mat-menu #menu=\"matMenu\">\n * @for (button of buttonList(); track button.id) {\n * <button mat-menu-item\n * [disabled]=\"!canSelectButton(button)\"\n * (click)=\"onSelect(button)\"\n * >\n * @if (isButtonLoading(button)) {\n * <mat-spinner diameter=\"16\" />\n * } @else if (button.icon) {\n * <mat-icon>{{ button.icon }}</mat-icon>\n * }\n * <span>{{ button.label }}</span>\n * </button>\n * }\n * </mat-menu>\n * `\n * })\n * export class MatActionMenuComponent {\n * private readonly listService = inject(ActionButtonListService);\n * // ... similar implementation\n * }\n * ```\n */\n\nimport { Injectable, Signal } from '@angular/core';\n\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Service that provides logic for managing lists of action buttons.\n *\n * This service is designed to be provided at the component level to maintain\n * consistency with other button services.\n *\n * @usageNotes\n * ### Button List Resolution\n * The service handles two input sources for buttons:\n * 1. Direct property assignment (via PopoverController.create componentProps)\n * 2. Angular signal input binding\n *\n * Props take precedence over input when both are provided.\n *\n * ### Loading State\n * Loading state can come from:\n * 1. The button's own `loading` property\n * 2. A set of loading child IDs passed from the parent button\n *\n * ### Selection Validation\n * A button can be selected only if it's not loading and not disabled.\n */\n@Injectable()\nexport class ActionButtonListService {\n /**\n * Resolves the button list from either props or input.\n * Props (from PopoverController) take precedence over input binding.\n *\n * @param propsButtons - Buttons passed via component props\n * @param inputButtons - Buttons passed via signal input\n * @returns The resolved button array\n */\n resolveButtonList(\n propsButtons: readonly ActionButton[],\n inputButtons: readonly ActionButton[]\n ): readonly ActionButton[] {\n return propsButtons.length > 0 ? propsButtons : inputButtons;\n }\n\n /**\n * Checks if a button is currently in a loading state.\n *\n * @param button - The button to check\n * @param loadingChildIds - Set of child IDs currently loading (optional)\n * @returns True if the button is loading\n */\n isButtonLoading(button: ActionButton, loadingChildIds?: ReadonlySet<string>): boolean {\n const ids = loadingChildIds ?? new Set<string>();\n return (button.loading ?? false) || ids.has(button.id);\n }\n\n /**\n * Checks if a button is currently in a loading state using a signal.\n * Convenience method when loadingChildIds is provided as a signal.\n *\n * @param button - The button to check\n * @param loadingChildIdsSignal - Signal containing loading child IDs (optional)\n * @returns True if the button is loading\n */\n isButtonLoadingFromSignal(\n button: ActionButton,\n loadingChildIdsSignal: Signal<ReadonlySet<string>> | null\n ): boolean {\n const loadingChildIds = loadingChildIdsSignal ? loadingChildIdsSignal() : new Set<string>();\n return this.isButtonLoading(button, loadingChildIds);\n }\n\n /**\n * Determines if a button can be selected (clicked).\n *\n * @param button - The button to check\n * @param loadingChildIds - Set of child IDs currently loading (optional)\n * @returns True if the button can be selected\n */\n canSelectButton(button: ActionButton, loadingChildIds?: ReadonlySet<string>): boolean {\n const isLoading = this.isButtonLoading(button, loadingChildIds);\n const isDisabled = button.config?.disabled ?? false;\n return !isLoading && !isDisabled;\n }\n\n /**\n * Determines whether to show a loading spinner for a button.\n *\n * @param button - The button to check\n * @param loadingChildIds - Set of child IDs currently loading (optional)\n * @returns True if spinner should be shown\n */\n shouldShowSpinner(button: ActionButton, loadingChildIds?: ReadonlySet<string>): boolean {\n const isLoading = this.isButtonLoading(button, loadingChildIds);\n const showSpinner = button.showLoadingSpinner ?? true;\n return isLoading && showSpinner;\n }\n}\n","/**\n * @file Button Display Service\n * @description Provides display logic for action buttons (icon slots, label visibility, etc.).\n *\n * This service encapsulates all display-related logic for buttons, making it\n * reusable across different UI library implementations.\n *\n * @example\n * ```typescript\n * // Ionic implementation\n * @Component({\n * providers: [ButtonDisplayService],\n * template: `\n * <ion-button\n [fill]=\"button().config?.fill\"\n [size]=\"button().config?.size\"\n [color]=\"button().config?.color\"\n [shape]=\"button().config?.shape\"\n [expand]=\"button().config?.expand\"\n [strong]=\"button().config?.strong\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"button().ariaLabel\"\n [title]=\"button().tooltip ?? ''\"\n (click)=\"onClick($event)\"\n >\n @if (showLoadingSpinner()) {\n <ion-spinner [slot]=\"iconSlot()\" name=\"crescent\" />\n } @else if (button().icon) {\n <ion-icon [name]=\"button().icon\" [slot]=\"iconSlot()\" class=\"button-icon\" />\n }\n @if (showLabel()) {\n {{ button().label }}\n }\n @if (showDropdownIcon()) {\n <ion-icon name=\"chevron-down-outline\" slot=\"end\" class=\"dropdown-icon\" />\n }\n </ion-button>\n * `\n * })\n * export class ButtonComponent {\n * private readonly displayService = inject(ButtonDisplayService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly showLabel = computed(() =>\n * this.displayService.shouldShowLabel(this.button())\n * );\n * }\n *\n * // PrimeNG implementation example\n * @Component({\n * selector: 'p-action-button',\n * providers: [ButtonDisplayService],\n * template: `\n * <p-button\n * [icon]=\"button().icon\"\n * [iconPos]=\"showLabel() ? 'left' : undefined\"\n * [label]=\"showLabel() ? button().label : null\"\n * />\n * `\n * })\n * export class PrimeActionButtonComponent {\n * private readonly displayService = inject(ButtonDisplayService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly showLabel = computed(() =>\n * this.displayService.shouldShowLabel(this.button())\n * );\n * }\n * ```\n */\n\nimport { Injectable } from '@angular/core';\n\nimport { ActionButtonType } from '../interfaces/action-button-type.enum';\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Service that provides display logic for action buttons.\n *\n * This service is designed to be provided at the component level to maintain\n * consistency with other button services, though it contains only pure functions.\n *\n * @usageNotes\n * ### Pure Functions\n * All methods in this service are pure functions that take a button configuration\n * and return display values. They can be safely used in Angular's `computed()`.\n *\n * ### Dropdown Icon\n * Dropdown buttons show a chevron icon unless `config.hideDropdownIcon` is true.\n */\n@Injectable()\nexport class ButtonDisplayService {\n /**\n * Determines whether to show the label text.\n *\n * @param button - The button configuration\n * @returns True when a label is provided\n */\n shouldShowLabel(button: ActionButton): boolean {\n return !!button.label;\n }\n\n /**\n * Determines whether to show the dropdown chevron icon.\n *\n * @param button - The button configuration\n * @returns True for dropdown buttons without hideDropdownIcon\n */\n shouldShowDropdownIcon(button: ActionButton): boolean {\n return button.type === ActionButtonType.Dropdown && !button.config?.hideDropdownIcon;\n }\n}\n","/**\n * @file Button Handler Service\n * @description Executes button handlers with automatic loading state management.\n *\n * This service encapsulates handler execution logic, providing automatic\n * loading state management for async handlers across different UI implementations.\n *\n * @example\n * ```typescript\n * // Ionic implementation\n * @Component({\n * providers: [ButtonStateService, ButtonHandlerService]\n * })\n * export class ButtonComponent {\n * private readonly stateService = inject(ButtonStateService);\n * private readonly handlerService = inject(ButtonHandlerService);\n *\n * protected async onClick(): Promise<void> {\n * await this.handlerService.executeHandler(\n * this.button(),\n * loading => this.stateService.setLoading(loading)\n * );\n * this.buttonClick.emit(this.button());\n * }\n * }\n *\n * // Generic web component example\n * class ActionButtonElement extends HTMLElement {\n * private handlerService = new ButtonHandlerService();\n * private loading = false;\n *\n * async handleClick() {\n * await this.handlerService.executeHandler(\n * this.buttonConfig,\n * loading => {\n * this.loading = loading;\n * this.render();\n * }\n * );\n * }\n * }\n * ```\n */\n\nimport { Injectable } from '@angular/core';\n\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Callback function type for loading state changes.\n */\nexport type LoadingCallback = (loading: boolean) => void;\n\n/**\n * Callback function type for child loading state changes.\n */\nexport type ChildLoadingCallback = (childId: string, loading: boolean) => void;\n\n/**\n * Service that executes button handlers with automatic loading state management.\n *\n * This service is designed to be provided at the component level to ensure\n * proper isolation of handler execution context.\n *\n * @usageNotes\n * ### Async Handler Support\n * When a handler returns a Promise, the service automatically:\n * 1. Calls `onLoadingChange(true)` before execution\n * 2. Awaits the Promise\n * 3. Calls `onLoadingChange(false)` after completion (success or failure)\n *\n * ### Sync Handler Support\n * For synchronous handlers, no loading state changes are triggered.\n *\n * ### Error Handling\n * The service uses try/finally to ensure loading state is always reset,\n * even if the handler throws an error. The error is not caught, allowing\n * it to propagate to the calling code.\n */\n@Injectable()\nexport class ButtonHandlerService {\n /**\n * Executes a button's handler with automatic loading state management.\n *\n * @param button - The button whose handler to execute\n * @param onLoadingChange - Callback invoked when loading state changes\n * @returns Promise that resolves when handler completes\n *\n * @example\n * ```typescript\n * await handlerService.executeHandler(\n * myButton,\n * loading => this.isLoading.set(loading)\n * );\n * ```\n */\n async executeHandler(button: ActionButton, onLoadingChange: LoadingCallback): Promise<void> {\n const result = button.handler();\n\n if (result instanceof Promise) {\n onLoadingChange(true);\n try {\n await result;\n } finally {\n onLoadingChange(false);\n }\n }\n }\n\n /**\n * Executes a child button's handler with loading state tracking by ID.\n *\n * This method is designed for dropdown children where multiple buttons\n * might be loading simultaneously and need individual tracking.\n *\n * @param child - The child button whose handler to execute\n * @param onChildLoadingChange - Callback with child ID and loading state\n * @returns Promise that resolves when handler completes\n *\n * @example\n * ```typescript\n * await handlerService.executeChildHandler(\n * selectedChild,\n * (childId, loading) => {\n * if (loading) {\n * this.stateService.addLoadingChild(childId);\n * } else {\n * this.stateService.removeLoadingChild(childId);\n * }\n * }\n * );\n * ```\n */\n async executeChildHandler(\n child: ActionButton,\n onChildLoadingChange: ChildLoadingCallback\n ): Promise<void> {\n onChildLoadingChange(child.id, true);\n try {\n const result = child.handler();\n if (result instanceof Promise) {\n await result;\n }\n } finally {\n onChildLoadingChange(child.id, false);\n }\n }\n}\n","/**\n * @file Button State Service\n * @description Manages loading states and computed state values for action buttons.\n *\n * This service encapsulates all state management logic for buttons, making it\n * reusable across different UI library implementations (Ionic, Material, PrimeNG, etc.).\n *\n * @example\n * ```typescript\n * // Ionic implementation (ButtonComponent)\n * @Component({\n * providers: [ButtonStateService]\n * })\n * export class ButtonComponent {\n * private readonly stateService = inject(ButtonStateService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly isLoading = computed(() =>\n * this.stateService.isLoading(this.button())\n * );\n * }\n *\n * // Material implementation example\n * @Component({\n * selector: 'mat-action-button',\n * providers: [ButtonStateService],\n * template: `\n * <button mat-button [disabled]=\"isDisabled()\">\n * @if (showLoadingSpinner()) {\n * <mat-spinner diameter=\"16\" />\n * }\n * {{ button().label }}\n * </button>\n * `\n * })\n * export class MatActionButtonComponent {\n * private readonly stateService = inject(ButtonStateService);\n * readonly button = input.required<ActionButton>();\n *\n * readonly isDisabled = computed(() =>\n * this.stateService.isDisabled(this.button())\n * );\n * readonly showLoadingSpinner = computed(() =>\n * this.stateService.showLoadingSpinner(this.button())\n * );\n * }\n * ```\n */\n\nimport { Injectable, signal } from '@angular/core';\n\nimport { ActionButtonType } from '../interfaces/action-button-type.enum';\nimport { ActionButton } from '../interfaces/action-button.interface';\n\n/**\n * Service that manages button loading states and computes derived state values.\n *\n * This service is designed to be provided at the component level (not root)\n * to ensure each button instance has isolated state management.\n *\n * @usageNotes\n * ### Providing the Service\n * Always provide this service at the component level to isolate state:\n * ```typescript\n * @Component({\n * providers: [ButtonStateService]\n * })\n * ```\n *\n * ### State Management\n * The service manages two types of loading state:\n * 1. **Internal loading** - Set when executing async handlers via `setLoading()`\n * 2. **Child loading** - Tracks which dropdown children are loading via `setChildLoading()`\n *\n * ### Computed States\n * All computed methods are pure functions that can be used in Angular's `computed()`:\n * - `isLoading(button)` - Combined loading state\n * - `isDisabled(button)` - Whether button should be disabled\n * - `showLoadingSpinner(button)` - Whether to show spinner\n * - `hasLoadingChild(button)` - Whether any child is loading\n */\n@Injectable()\nexport class ButtonStateService {\n /**\n * Internal loading state for async handler execution.\n */\n private readonly _isLoading = signal<boolean>(false);\n\n /**\n * Set of child button IDs that are currently loading (for dropdowns).\n */\n private readonly _loadingChildIds = signal<ReadonlySet<string>>(new Set());\n\n /**\n * Read-only access to internal loading state.\n */\n readonly internalLoading = this._isLoading.asReadonly();\n\n /**\n * Read-only access to loading child IDs.\n */\n readonly loadingChildIds = this._loadingChildIds.asReadonly();\n\n /**\n * Sets the internal loading state.\n *\n * @param loading - Whether the button is loading\n */\n setLoading(loading: boolean): void {\n this._isLoading.set(loading);\n }\n\n /**\n * Adds a child ID to the loading set.\n *\n * @param childId - The ID of the child button that started loading\n */\n addLoadingChild(childId: string): void {\n this._loadingChildIds.update(ids => new Set([...ids, childId]));\n }\n\n /**\n * Removes a child ID from the loading set.\n *\n * @param childId - The ID of the child button that finished loading\n */\n removeLoadingChild(childId: string): void {\n this._loadingChildIds.update(ids => {\n const newIds = new Set(ids);\n newIds.delete(childId);\n return newIds;\n });\n }\n\n /**\n * Checks if any child button is in a loading state with spinner enabled.\n *\n * @param button - The parent button configuration\n * @returns True if any child is loading and should show spinner\n */\n hasLoadingChild(button: ActionButton): boolean {\n if (button.type !== ActionButtonType.Dropdown || !button.children) {\n return false;\n }\n\n const loadingChildIds = this._loadingChildIds();\n\n return button.children.some(child => {\n const showSpinner = child.showLoadingSpinner ?? true;\n const isLoadingFromProp = child.loading && showSpinner;\n const isLoadingFromExecution = loadingChildIds.has(child.id) && showSpinner;\n return isLoadingFromProp || isLoadingFromExecution;\n });\n }\n\n /**\n * Computes the combined loading state for a button.\n *\n * @param button - The button configuration\n * @returns True if the button is in any loading state\n */\n isLoading(button: ActionButton): boolean {\n const parentLoading = this._isLoading() || (button.loading ?? false);\n const childLoading = this.hasLoadingChild(button) && (button.showLoadingSpinner ?? true);\n return parentLoading || childLoading;\n }\n\n /**\n * Determines whether to display the loading spinner.\n *\n * @param button - The button configuration\n * @returns True if spinner should be shown\n */\n showLoadingSpinner(button: ActionButton): boolean {\n return this.isLoading(button) && (button.showLoadingSpinner ?? true);\n }\n\n /**\n * Computes whether the button should be disabled.\n *\n * For dropdown buttons: Only disabled if config says so or if parent itself is loading\n * (not disabled by inherited child loading - user can still open dropdown).\n *\n * For non-dropdown buttons: Disabled when loading or explicitly disabled in config.\n *\n * @param button - The button configuration\n * @returns True if the button should be disabled\n */\n isDisabled(button: ActionButton): boolean {\n const configDisabled = button.config?.disabled ?? false;\n\n if (button.type === ActionButtonType.Dropdown) {\n const parentSelfLoading = this._isLoading() || (button.loading ?? false);\n return configDisabled || parentSelfLoading;\n }\n\n return this.isLoading(button) || configDisabled;\n }\n\n /**\n * Checks if a specific button is currently loading.\n * Used primarily for checking child button loading state.\n *\n * @param button - The button to check\n * @returns True if the button is loading\n */\n isButtonLoading(button: ActionButton): boolean {\n const loadingChildIds = this._loadingChildIds();\n return (button.loading ?? false) || loadingChildIds.has(button.id);\n }\n}\n","/**\n * @file Device Detection Service\n * @description Detects and tracks device type (mobile/desktop) based on user-agent and viewport width.\n *\n * This service provides a reactive signal that updates when the viewport is resized,\n * allowing components to respond to device type changes. The service uses OR logic,\n * considering a device mobile if EITHER the user-agent indicates a mobile device\n * OR the viewport width is less than 768px.\n *\n * @example\n * ```typescript\n * // Component usage\n * @Component({\n * selector: 'app-example',\n * template: `\n * @if (deviceDetection.isMobile()) {\n * <p>Mobile view</p>\n * } @else {\n * <p>Desktop view</p>\n * }\n * `\n * })\n * export class ExampleComponent {\n * readonly deviceDetection = inject(DeviceDetectionService);\n * }\n *\n * // Direct usage in computed signals\n * @Component({\n * selector: 'app-layout'\n * })\n * export class LayoutComponent {\n * private readonly deviceDetection = inject(DeviceDetectionService);\n *\n * readonly showSidebar = computed(() =>\n * !this.deviceDetection.isMobile()\n * );\n * }\n * ```\n */\n\nimport { DestroyRef, Injectable, inject, signal } from '@angular/core';\n\n/**\n * Service for detecting and tracking device type based on user-agent and viewport width.\n *\n * The service initializes on construction and sets up a window resize listener to\n * track viewport changes. It uses OR logic to determine if the device is mobile:\n * - User-agent matches mobile device patterns\n * - OR viewport width is less than 768px\n *\n * The resize listener is automatically cleaned up when the service is destroyed.\n *\n * @usageNotes\n *\n * ### Basic Usage\n * Inject the service and use the `isMobile()` signal in templates or computed signals:\n *\n * ```typescript\n * export class MyComponent {\n * readonly deviceDetection = inject(DeviceDetectionService);\n * }\n * ```\n *\n * ### Template Usage\n * ```typescript\n * @if (deviceDetection.isMobile()) {\n * <ion-icon name=\"phone-portrait-outline\" />\n * } @else {\n * <ion-icon name=\"desktop-outline\" />\n * }\n * ```\n *\n * ### Computed Signals\n * ```typescript\n * readonly layout = computed(() =>\n * this.deviceDetection.isMobile() ? 'mobile' : 'desktop'\n * );\n * ```\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class DeviceDetectionService {\n private readonly destroyRef = inject(DestroyRef);\n\n /**\n * Regular expression pattern for detecting mobile devices via user-agent.\n * Matches common mobile device identifiers including:\n * - Android devices\n * - iOS devices (iPhone, iPad, iPod)\n * - Windows Phone (IEMobile)\n * - BlackBerry\n * - Opera Mini mobile browser\n * - webOS devices\n */\n private readonly MOBILE_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n\n /**\n * Breakpoint width (in pixels) below which viewport is considered mobile.\n * @constant\n */\n private readonly MOBILE_BREAKPOINT = 768;\n\n /**\n * Private writable signal tracking mobile device state.\n */\n private readonly _isMobile = signal<boolean>(false);\n\n /**\n * Public readonly signal indicating whether the device is mobile.\n * Returns `true` if EITHER user-agent indicates mobile OR viewport width < 768px.\n *\n * @returns Readonly signal of mobile device state\n *\n * @example\n * ```typescript\n * const deviceDetection = inject(DeviceDetectionService);\n * const isMobile = deviceDetection.isMobile();\n * console.log(`Device is ${isMobile ? 'mobile' : 'desktop'}`);\n * ```\n */\n readonly isMobile = this._isMobile.asReadonly();\n\n constructor() {\n // Initialize mobile state\n this._isMobile.set(this.checkDevice());\n\n // Set up resize listener\n const resizeHandler = (): void => {\n this._isMobile.set(this.checkDevice());\n };\n\n window.addEventListener('resize', resizeHandler);\n\n // Cleanup on service destruction\n this.destroyRef.onDestroy(() => {\n window.removeEventListener('resize', resizeHandler);\n });\n }\n\n /**\n * Checks if the device should be considered mobile based on user-agent and viewport width.\n *\n * Uses OR logic: returns `true` if EITHER condition is met:\n * - User-agent matches mobile device pattern\n * - Viewport width is less than 768px\n *\n * @returns `true` if device is mobile, `false` otherwise\n *\n * @example\n * ```typescript\n * const deviceDetection = inject(DeviceDetectionService);\n * const isMobile = deviceDetection.checkDevice();\n * // isMobile will be true if:\n * // - User-agent contains \"Android\", \"iPhone\", etc.\n * // - OR window.innerWidth < 768\n * ```\n */\n private checkDevice(): boolean {\n const isMobileUA = this.MOBILE_REGEX.test(navigator.userAgent);\n const isMobileViewport = window.innerWidth < this.MOBILE_BREAKPOINT;\n return isMobileUA || isMobileViewport;\n }\n}\n","/**\n * @file Tooltip Service\n * @description UI-agnostic service for tooltip positioning and display logic.\n *\n * This service provides pure computational functions for tooltip positioning\n * and visibility rules, making it reusable across different UI library\n * implementations (Ionic, Material, PrimeNG, etc.).\n *\n * @example\n * ```typescript\n * // Ionic implementation (MakiTooltipDirective)\n * @Directive({\n * selector: '[makiTooltip]'\n * })\n * export class MakiTooltipDirective {\n * private readonly tooltipService = inject(TooltipService);\n * private readonly deviceDetection = inject(DeviceDetectionService);\n *\n * private positionTooltip(): void {\n * const triggerRect = this.el.nativeElement.getBoundingClientRect();\n * const tooltipRect = this.tooltip.getBoundingClientRect();\n * const position = this.tooltipService.calculatePosition(\n * triggerRect,\n * tooltipRect,\n * window.innerWidth,\n * window.innerHeight\n * );\n * this.applyPosition(position);\n * }\n *\n * private shouldShow(): boolean {\n * const tag = this.el.nativeElement.tagName.toLowerCase();\n * return this.tooltipService.shouldShowTooltip(\n * tag,\n * this.deviceDetection.isMobile(),\n * !!this.content()\n * );\n * }\n * }\n *\n * // Material implementation example\n * @Directive({\n * selector: '[matTooltip]'\n * })\n * export class MatTooltipDirective {\n * private readonly tooltipService = inject(TooltipService);\n *\n * private calculatePosition(): TooltipPosition {\n * return this.tooltipService.calculatePosition(\n * this.triggerRect,\n * this.tooltipRect,\n * window.innerWidth,\n * window.innerHeight\n * );\n * }\n * }\n * ```\n */\n\nimport { Injectable } from '@angular/core';\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\n/**\n * Position data for tooltip placement.\n */\nexport interface TooltipPosition {\n /** CSS top value in pixels */\n readonly top: number;\n /** CSS left value in pixels */\n readonly left: number;\n /** Actual placement after overflow adjustment */\n readonly placement: TooltipPlacement;\n}\n\n/**\n * Tooltip placement options.\n */\nexport type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';\n\n/**\n * Rectangle dimensions for positioning calculations.\n * Compatible with DOMRect from getBoundingClientRect().\n */\nexport interface ElementRect {\n readonly top: number;\n readonly bottom: number;\n readonly left: number;\n readonly right: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Interactive element tags that should skip tooltips on mobile devices.\n */\nconst INTERACTIVE_ELEMENTS: readonly string[] = [\n 'button',\n 'ion-button',\n 'ion-select',\n 'a',\n 'input',\n 'select',\n 'textarea'\n];\n\n/**\n * Default gap between tooltip and trigger element (in pixels).\n */\nconst TOOLTIP_GAP = 5;\n\n/**\n * Minimum distance from viewport edge (in pixels).\n */\nconst VIEWPORT_PADDING = 5;\n\n// ============================================================================\n// Service\n// ============================================================================\n\n/**\n * Service providing UI-agnostic tooltip positioning and display logic.\n *\n * This service contains pure functions for calculating tooltip positions\n * and determining when tooltips should be shown, without any DOM manipulation\n * or framework-specific code.\n *\n * @usageNotes\n *\n * ### Positioning\n * Use `calculatePosition()` to compute tooltip position based on element rectangles:\n * ```typescript\n * const position = tooltipService.calculatePosition(\n * triggerElement.getBoundingClientRect(),\n * tooltipElement.getBoundingClientRect(),\n * window.innerWidth,\n * window.innerHeight\n * );\n * tooltip.style.top = `${position.top}px`;\n * tooltip.style.left = `${position.left}px`;\n * ```\n *\n * ### Visibility Rules\n * Use `shouldShowTooltip()` to determine if tooltip should appear:\n * ```typescript\n * if (tooltipService.shouldShowTooltip(element.tagName, isMobile, hasContent)) {\n * this.showTooltip();\n * }\n * ```\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class TooltipService {\n /**\n * Calculates the optimal position for a tooltip relative to its trigger element.\n *\n * The function attempts to position the tooltip below the trigger element by default.\n * If the tooltip would overflow the viewport, it adjusts the position:\n * - Positions above if it would overflow the bottom\n * - Aligns to the right edge if it would overflow the right\n *\n * @param triggerRect - Bounding rectangle of the trigger element\n * @param tooltipRect - Bounding rectangle of the tooltip element\n * @param viewportWidth - Current viewport width (typically window.innerWidth)\n * @param viewportHeight - Current viewport height (typically window.innerHeight)\n * @param preferredPlacement - Optional preferred placement before fallback logic\n * @returns Position object with top, left coordinates and final placement\n *\n * @example\n * ```typescript\n * const position = tooltipService.calculatePosition(\n * button.getBoundingClientRect(),\n * tooltip.getBoundingClientRect(),\n * window.innerWidth,\n * window.innerHeight,\n * 'top'\n * );\n * // position = { top: 150, left: 100, placement: 'top' }\n * ```\n */\n calculatePosition(\n triggerRect: ElementRect,\n tooltipRect: ElementRect,\n viewportWidth: number,\n viewportHeight: number,\n placement: TooltipPlacement = 'bottom'\n ): TooltipPosition {\n const clampVertical = (top: number): number => {\n const maxTop = viewportHeight - tooltipRect.height - VIEWPORT_PADDING;\n return Math.min(Math.max(VIEWPORT_PADDING, top), maxTop);\n };\n\n const calculatePositionFor = (placement: TooltipPlacement): TooltipPosition => {\n let top: number;\n let left: number;\n const centeredTop = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;\n\n switch (placement) {\n case 'top':\n top = triggerRect.top - tooltipRect.height - TOOLTIP_GAP;\n left = triggerRect.left;\n break;\n case 'bottom':\n top = triggerRect.bottom + TOOLTIP_GAP;\n left = triggerRect.left;\n break;\n case 'left':\n top = clampVertical(centeredTop);\n left = triggerRect.left - tooltipRect.width - TOOLTIP_GAP;\n break;\n case 'right':\n top = clampVertical(centeredTop);\n left = triggerRect.right + TOOLTIP_GAP;\n break;\n }\n\n if (placement === 'top' || placement === 'bottom') {\n const wouldOverflowRight = triggerRect.left + tooltipRect.width > viewportWidth;\n if (wouldOverflowRight) {\n const rightAligned = triggerRect.right - tooltipRect.width;\n left = Math.max(VIEWPORT_PADDING, rightAligned);\n }\n }\n\n return { top, left, placement };\n };\n\n const fitsInViewport = (position: TooltipPosition): boolean => {\n const minTop = 0;\n const maxTop = viewportHeight - tooltipRect.height;\n\n // If the tooltip is wider than the viewport, horizontal placements\n // ('left' or 'right') are effectively impossible and should not be\n // considered valid. Only vertical placements are allowed in that case.\n if (tooltipRect.width > viewportWidth) {\n if (position.placement !== 'top' && position.placement !== 'bottom') {\n return false;\n }\n return position.top >= minTop && position.top <= maxTop;\n }\n\n const minLeft = VIEWPORT_PADDING;\n const maxLeft = viewportWidth - tooltipRect.width;\n\n return (\n position.left >= minLeft &&\n position.left <= maxLeft &&\n position.top >= minTop &&\n position.top <= maxTop\n );\n };\n\n const preferredPosition = calculatePositionFor(placement);\n if (fitsInViewport(preferredPosition)) {\n return preferredPosition;\n }\n\n if (placement === 'bottom') {\n const fallbackPosition = calculatePositionFor('top');\n if (fitsInViewport(fallbackPosition)) {\n return fallbackPosition;\n }\n }\n\n if (placement === 'top') {\n const fallbackPosition = calculatePositionFor('bottom');\n if (fitsInViewport(fallbackPosition)) {\n return fallbackPosition;\n }\n }\n\n if (placement === 'left' || placement === 'right') {\n const opposite: TooltipPlacement = placement === 'left' ? 'right' : 'left';\n const oppositePosition = calculatePositionFor(opposite);\n if (fitsInViewport(oppositePosition)) {\n return oppositePosition;\n }\n\n const bottomPosition = calculatePositionFor('bottom');\n if (fitsInViewport(bottomPosition)) {\n return bottomPosition;\n }\n\n const topPosition = calculatePositionFor('top');\n if (fitsInViewport(topPosition)) {\n return topPosition;\n }\n }\n\n return preferredPosition;\n }\n\n /**\n * Determines whether a tooltip should be shown based on context.\n *\n * On mobile devices, tooltips are suppressed for interactive elements\n * (buttons, links, selects) to avoid interfering with their primary click handlers.\n *\n * @param elementTag - Lowercase tag name of the trigger element\n * @param isMobile - Whether the device is currently considered mobile\n * @param hasContent - Whether the tooltip has content to display\n * @returns `true` if tooltip should be shown, `false` otherwise\n *\n * @example\n * ```typescript\n * const shouldShow = tooltipService.shouldShowTooltip('button', true, true);\n * // shouldShow = false (button on mobile)\n *\n * const shouldShow2 = tooltipService.shouldShowTooltip('div', true, true);\n * // shouldShow2 = true (non-interactive on mobile)\n *\n * const shouldShow3 = tooltipService.shouldShowTooltip('button', false, true);\n * // shouldShow3 = true (button on desktop)\n * ```\n */\n shouldShowTooltip(elementTag: string, isMobile: boolean, hasContent: boolean): boolean {\n // No content = no tooltip\n if (!hasContent) {\n return false;\n }\n\n // On mobile, skip interactive elements\n if (isMobile && this.isInteractiveElement(elementTag)) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Checks if an element tag represents an interactive element.\n *\n * Interactive elements (buttons, links, form controls) typically have\n * their own click/tap handlers, and showing tooltips on mobile can\n * interfere with the primary interaction.\n *\n * @param elementTag - Lowercase tag name to check\n * @returns `true` if the element is interactive, `false` otherwise\n *\n * @example\n * ```typescript\n * tooltipService.isInteractiveElement('button'); // true\n * tooltipService.isInteractiveElement('ion-button'); // true\n * tooltipService.isInteractiveElement('div'); // false\n * tooltipService.isInteractiveElement('span'); // false\n * ```\n */\n isInteractiveElement(elementTag: string): boolean {\n return INTERACTIVE_ELEMENTS.includes(elementTag.toLowerCase());\n }\n\n /**\n * Checks if a tooltip element is visible (has non-zero dimensions).\n *\n * This is useful for detecting if an element has been hidden or removed\n * from the layout, which should trigger tooltip dismissal.\n *\n * @param rect - Bounding rectangle of the element\n * @returns `true` if element has visible dimensions, `false` otherwise\n *\n * @example\n * ```typescript\n * const isVisible = tooltipService.isElementVisible(\n * element.getBoundingClientRect()\n * );\n * if (!isVisible) {\n * this.hideTooltip();\n * }\n * ```\n */\n isElementVisible(rect: ElementRect): boolean {\n return rect.width > 0 && rect.height > 0;\n }\n\n /**\n *\n * @returns Tooltip show delay in milliseconds.\n */\n getShowDelayMs(): number {\n return 250;\n }\n\n /**\n * @returns Tooltip close delay in milliseconds.\n */\n getCloseDelayMs(): number {\n return 200;\n }\n\n /**\n * @returns Tooltip fade duration in milliseconds.\n */\n getFadeDurationMs(): number {\n return 150;\n }\n}\n","import { Injectable } from '@angular/core';\nimport { TooltipService } from './tooltip.service';\n\n/**\n * Handle interface for a single tooltip's scheduling logic.\n * Exposes event methods to be called by the tooltip directive/component.\n */\nexport interface TooltipSchedulerHandle {\n destroy(): void;\n onTriggerEnter(): void;\n onTriggerLeave(): void;\n onTooltipEnter(): void;\n onTooltipLeave(): void;\n onTooltipTouchStart(): void;\n onTooltipTouchEnd(): void;\n onClick(isMobile: boolean): void;\n}\n\n/**\n * Singleton service that centralizes tooltip scheduling (show/hide delays).\n * Purely orchestrates timers and delegates to provided callbacks; no DOM access.\n */\n@Injectable({ providedIn: 'root' })\nexport class TooltipSchedulerService {\n constructor(private readonly tooltipService: TooltipService) {}\n\n /**\n * Create an isolated scheduler handle for a single tooltip instance.\n * The handle exposes the same event methods but keeps timers/state per-instance.\n */\n createHandle(show: () => void, hide: () => void): TooltipSchedulerHandle {\n let showTimeout: ReturnType<typeof setTimeout> | null = null;\n let closeTimeout: ReturnType<typeof setTimeout> | null = null;\n\n let isTriggerHovering = false;\n let isTooltipHovering = false;\n let isTouching = false;\n\n const clearShowTimeout = (): void => {\n if (showTimeout) {\n clearTimeout(showTimeout);\n showTimeout = null;\n }\n };\n\n const clearCloseTimeout = (): void => {\n if (closeTimeout) {\n clearTimeout(closeTimeout);\n closeTimeout = null;\n }\n };\n\n const scheduleShow = (): void => {\n clearShowTimeout();\n const ms = this.tooltipService.getShowDelayMs();\n showTimeout = setTimeout(() => {\n showTimeout = null;\n invokeShow();\n }, ms);\n };\n\n const scheduleClose = (): void => {\n clearCloseTimeout();\n const ms = this.tooltipService.getCloseDelayMs();\n closeTimeout = setTimeout(() => {\n closeTimeout = null;\n if (!isTriggerHovering && !isTooltipHovering && !isTouching) {\n invokeHide();\n }\n }, ms);\n };\n\n const invokeShow = (): void => show();\n const invokeHide = (): void => hide();\n\n return {\n destroy(): void {\n clearShowTimeout();\n clearCloseTimeout();\n },\n onTriggerEnter(): void {\n isTriggerHovering = true;\n clearCloseTimeout();\n clearShowTimeout();\n scheduleShow();\n },\n onTriggerLeave(): void {\n isTriggerHovering = false;\n clearShowTimeout();\n scheduleClose();\n },\n onTooltipEnter(): void {\n isTooltipHovering = true;\n clearCloseTimeout();\n },\n onTooltipLeave(): void {\n isTooltipHovering = false;\n scheduleClose();\n },\n onTooltipTouchStart(): void {\n isTouching = true;\n },\n onTooltipTouchEnd(): void {\n isTouching = false;\n scheduleClose();\n },\n onClick(isMobile: boolean): void {\n if (isMobile) {\n isTouching = true;\n invokeShow();\n return;\n }\n\n // Desktop toggle behavior\n if (closeTimeout) {\n clearCloseTimeout();\n return;\n }\n\n invokeShow();\n }\n };\n }\n}\n","/**\n * @file Services Barrel Export\n * @description Exports all services from the ui-core library.\n */\n\nexport { ActionButtonListService } from './action-button-list.service';\nexport { ButtonDisplayService } from './button-display.service';\nexport { ButtonHandlerService } from './button-handler.service';\nexport type { ChildLoadingCallback, LoadingCallback } from './button-handler.service';\nexport { ButtonStateService } from './button-state.service';\nexport { DeviceDetectionService } from './device-detection.service';\nexport { TooltipSchedulerService } from './tooltip-scheduler.service';\nexport type { TooltipSchedulerHandle } from './tooltip-scheduler.service';\nexport { TooltipService } from './tooltip.service';\nexport type { ElementRect, TooltipPlacement, TooltipPosition } from './tooltip.service';\n","/*\n * Public API Surface of @makigamestudio/ui-core\n *\n * This package provides UI-agnostic interfaces, services, and design tokens\n * for building action button components across different UI frameworks.\n */\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\n// Action Button Configuration (generic, UI-agnostic)\nexport type {\n ActionButtonConfig,\n BaseActionButtonConfig\n} from './lib/interfaces/action-button-config.interface';\n\n// Action Button Type Enum\nexport { ActionButtonType } from './lib/interfaces/action-button-type.enum';\n\n// Action Button Interface (generic, UI-agnostic)\nexport type { ActionButton } from './lib/interfaces/action-button.interface';\n\n// ============================================================================\n// Services\n// ============================================================================\n\nexport {\n ActionButtonListService,\n ButtonDisplayService,\n ButtonHandlerService,\n ButtonStateService,\n DeviceDetectionService,\n TooltipSchedulerService,\n TooltipService\n} from './lib/services';\n\nexport type {\n ChildLoadingCallback,\n ElementRect,\n LoadingCallback,\n TooltipPlacement,\n TooltipPosition,\n TooltipSchedulerHandle\n} from './lib/services';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["i1.TooltipService"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makigamestudio/ui-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "UI-agnostic Angular component library core with interfaces, services, and design tokens. Provides the foundation for UI implementations like @makigamestudio/ui-ionic.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Signal } from '@angular/core';
|
|
2
|
+
import { TemplateRef, Signal } from '@angular/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @file Action button configuration interface
|
|
@@ -290,9 +290,26 @@ interface ActionButton<T = void, Config extends ActionButtonConfig = ActionButto
|
|
|
290
290
|
*/
|
|
291
291
|
readonly ariaLabel?: string;
|
|
292
292
|
/**
|
|
293
|
-
* Tooltip
|
|
293
|
+
* Tooltip content shown on hover (desktop) or click (mobile).
|
|
294
|
+
* Can be a simple string or a TemplateRef for rich content.
|
|
295
|
+
* Uses the makiTooltip directive for device-aware behavior.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* // String tooltip
|
|
300
|
+
* tooltip: 'Save changes'
|
|
301
|
+
*
|
|
302
|
+
* // Template tooltip (in component)
|
|
303
|
+
* @ViewChild('customTooltip') tooltipTemplate!: TemplateRef<unknown>;
|
|
304
|
+
* button = { tooltip: this.tooltipTemplate };
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
readonly tooltip?: string | TemplateRef<unknown>;
|
|
308
|
+
/**
|
|
309
|
+
* Optional color for the tooltip background.
|
|
310
|
+
* The specific color values depend on the UI library implementation.
|
|
294
311
|
*/
|
|
295
|
-
readonly
|
|
312
|
+
readonly tooltipColor?: string;
|
|
296
313
|
}
|
|
297
314
|
|
|
298
315
|
/**
|
|
@@ -649,5 +666,298 @@ declare class ButtonStateService {
|
|
|
649
666
|
static ɵprov: i0.ɵɵInjectableDeclaration<ButtonStateService>;
|
|
650
667
|
}
|
|
651
668
|
|
|
652
|
-
|
|
653
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Service for detecting and tracking device type based on user-agent and viewport width.
|
|
671
|
+
*
|
|
672
|
+
* The service initializes on construction and sets up a window resize listener to
|
|
673
|
+
* track viewport changes. It uses OR logic to determine if the device is mobile:
|
|
674
|
+
* - User-agent matches mobile device patterns
|
|
675
|
+
* - OR viewport width is less than 768px
|
|
676
|
+
*
|
|
677
|
+
* The resize listener is automatically cleaned up when the service is destroyed.
|
|
678
|
+
*
|
|
679
|
+
* @usageNotes
|
|
680
|
+
*
|
|
681
|
+
* ### Basic Usage
|
|
682
|
+
* Inject the service and use the `isMobile()` signal in templates or computed signals:
|
|
683
|
+
*
|
|
684
|
+
* ```typescript
|
|
685
|
+
* export class MyComponent {
|
|
686
|
+
* readonly deviceDetection = inject(DeviceDetectionService);
|
|
687
|
+
* }
|
|
688
|
+
* ```
|
|
689
|
+
*
|
|
690
|
+
* ### Template Usage
|
|
691
|
+
* ```typescript
|
|
692
|
+
* @if (deviceDetection.isMobile()) {
|
|
693
|
+
* <ion-icon name="phone-portrait-outline" />
|
|
694
|
+
* } @else {
|
|
695
|
+
* <ion-icon name="desktop-outline" />
|
|
696
|
+
* }
|
|
697
|
+
* ```
|
|
698
|
+
*
|
|
699
|
+
* ### Computed Signals
|
|
700
|
+
* ```typescript
|
|
701
|
+
* readonly layout = computed(() =>
|
|
702
|
+
* this.deviceDetection.isMobile() ? 'mobile' : 'desktop'
|
|
703
|
+
* );
|
|
704
|
+
* ```
|
|
705
|
+
*/
|
|
706
|
+
declare class DeviceDetectionService {
|
|
707
|
+
private readonly destroyRef;
|
|
708
|
+
/**
|
|
709
|
+
* Regular expression pattern for detecting mobile devices via user-agent.
|
|
710
|
+
* Matches common mobile device identifiers including:
|
|
711
|
+
* - Android devices
|
|
712
|
+
* - iOS devices (iPhone, iPad, iPod)
|
|
713
|
+
* - Windows Phone (IEMobile)
|
|
714
|
+
* - BlackBerry
|
|
715
|
+
* - Opera Mini mobile browser
|
|
716
|
+
* - webOS devices
|
|
717
|
+
*/
|
|
718
|
+
private readonly MOBILE_REGEX;
|
|
719
|
+
/**
|
|
720
|
+
* Breakpoint width (in pixels) below which viewport is considered mobile.
|
|
721
|
+
* @constant
|
|
722
|
+
*/
|
|
723
|
+
private readonly MOBILE_BREAKPOINT;
|
|
724
|
+
/**
|
|
725
|
+
* Private writable signal tracking mobile device state.
|
|
726
|
+
*/
|
|
727
|
+
private readonly _isMobile;
|
|
728
|
+
/**
|
|
729
|
+
* Public readonly signal indicating whether the device is mobile.
|
|
730
|
+
* Returns `true` if EITHER user-agent indicates mobile OR viewport width < 768px.
|
|
731
|
+
*
|
|
732
|
+
* @returns Readonly signal of mobile device state
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```typescript
|
|
736
|
+
* const deviceDetection = inject(DeviceDetectionService);
|
|
737
|
+
* const isMobile = deviceDetection.isMobile();
|
|
738
|
+
* console.log(`Device is ${isMobile ? 'mobile' : 'desktop'}`);
|
|
739
|
+
* ```
|
|
740
|
+
*/
|
|
741
|
+
readonly isMobile: i0.Signal<boolean>;
|
|
742
|
+
constructor();
|
|
743
|
+
/**
|
|
744
|
+
* Checks if the device should be considered mobile based on user-agent and viewport width.
|
|
745
|
+
*
|
|
746
|
+
* Uses OR logic: returns `true` if EITHER condition is met:
|
|
747
|
+
* - User-agent matches mobile device pattern
|
|
748
|
+
* - Viewport width is less than 768px
|
|
749
|
+
*
|
|
750
|
+
* @returns `true` if device is mobile, `false` otherwise
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```typescript
|
|
754
|
+
* const deviceDetection = inject(DeviceDetectionService);
|
|
755
|
+
* const isMobile = deviceDetection.checkDevice();
|
|
756
|
+
* // isMobile will be true if:
|
|
757
|
+
* // - User-agent contains "Android", "iPhone", etc.
|
|
758
|
+
* // - OR window.innerWidth < 768
|
|
759
|
+
* ```
|
|
760
|
+
*/
|
|
761
|
+
private checkDevice;
|
|
762
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DeviceDetectionService, never>;
|
|
763
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<DeviceDetectionService>;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Position data for tooltip placement.
|
|
768
|
+
*/
|
|
769
|
+
interface TooltipPosition {
|
|
770
|
+
/** CSS top value in pixels */
|
|
771
|
+
readonly top: number;
|
|
772
|
+
/** CSS left value in pixels */
|
|
773
|
+
readonly left: number;
|
|
774
|
+
/** Actual placement after overflow adjustment */
|
|
775
|
+
readonly placement: TooltipPlacement;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Tooltip placement options.
|
|
779
|
+
*/
|
|
780
|
+
type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
781
|
+
/**
|
|
782
|
+
* Rectangle dimensions for positioning calculations.
|
|
783
|
+
* Compatible with DOMRect from getBoundingClientRect().
|
|
784
|
+
*/
|
|
785
|
+
interface ElementRect {
|
|
786
|
+
readonly top: number;
|
|
787
|
+
readonly bottom: number;
|
|
788
|
+
readonly left: number;
|
|
789
|
+
readonly right: number;
|
|
790
|
+
readonly width: number;
|
|
791
|
+
readonly height: number;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Service providing UI-agnostic tooltip positioning and display logic.
|
|
795
|
+
*
|
|
796
|
+
* This service contains pure functions for calculating tooltip positions
|
|
797
|
+
* and determining when tooltips should be shown, without any DOM manipulation
|
|
798
|
+
* or framework-specific code.
|
|
799
|
+
*
|
|
800
|
+
* @usageNotes
|
|
801
|
+
*
|
|
802
|
+
* ### Positioning
|
|
803
|
+
* Use `calculatePosition()` to compute tooltip position based on element rectangles:
|
|
804
|
+
* ```typescript
|
|
805
|
+
* const position = tooltipService.calculatePosition(
|
|
806
|
+
* triggerElement.getBoundingClientRect(),
|
|
807
|
+
* tooltipElement.getBoundingClientRect(),
|
|
808
|
+
* window.innerWidth,
|
|
809
|
+
* window.innerHeight
|
|
810
|
+
* );
|
|
811
|
+
* tooltip.style.top = `${position.top}px`;
|
|
812
|
+
* tooltip.style.left = `${position.left}px`;
|
|
813
|
+
* ```
|
|
814
|
+
*
|
|
815
|
+
* ### Visibility Rules
|
|
816
|
+
* Use `shouldShowTooltip()` to determine if tooltip should appear:
|
|
817
|
+
* ```typescript
|
|
818
|
+
* if (tooltipService.shouldShowTooltip(element.tagName, isMobile, hasContent)) {
|
|
819
|
+
* this.showTooltip();
|
|
820
|
+
* }
|
|
821
|
+
* ```
|
|
822
|
+
*/
|
|
823
|
+
declare class TooltipService {
|
|
824
|
+
/**
|
|
825
|
+
* Calculates the optimal position for a tooltip relative to its trigger element.
|
|
826
|
+
*
|
|
827
|
+
* The function attempts to position the tooltip below the trigger element by default.
|
|
828
|
+
* If the tooltip would overflow the viewport, it adjusts the position:
|
|
829
|
+
* - Positions above if it would overflow the bottom
|
|
830
|
+
* - Aligns to the right edge if it would overflow the right
|
|
831
|
+
*
|
|
832
|
+
* @param triggerRect - Bounding rectangle of the trigger element
|
|
833
|
+
* @param tooltipRect - Bounding rectangle of the tooltip element
|
|
834
|
+
* @param viewportWidth - Current viewport width (typically window.innerWidth)
|
|
835
|
+
* @param viewportHeight - Current viewport height (typically window.innerHeight)
|
|
836
|
+
* @param preferredPlacement - Optional preferred placement before fallback logic
|
|
837
|
+
* @returns Position object with top, left coordinates and final placement
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* ```typescript
|
|
841
|
+
* const position = tooltipService.calculatePosition(
|
|
842
|
+
* button.getBoundingClientRect(),
|
|
843
|
+
* tooltip.getBoundingClientRect(),
|
|
844
|
+
* window.innerWidth,
|
|
845
|
+
* window.innerHeight,
|
|
846
|
+
* 'top'
|
|
847
|
+
* );
|
|
848
|
+
* // position = { top: 150, left: 100, placement: 'top' }
|
|
849
|
+
* ```
|
|
850
|
+
*/
|
|
851
|
+
calculatePosition(triggerRect: ElementRect, tooltipRect: ElementRect, viewportWidth: number, viewportHeight: number, placement?: TooltipPlacement): TooltipPosition;
|
|
852
|
+
/**
|
|
853
|
+
* Determines whether a tooltip should be shown based on context.
|
|
854
|
+
*
|
|
855
|
+
* On mobile devices, tooltips are suppressed for interactive elements
|
|
856
|
+
* (buttons, links, selects) to avoid interfering with their primary click handlers.
|
|
857
|
+
*
|
|
858
|
+
* @param elementTag - Lowercase tag name of the trigger element
|
|
859
|
+
* @param isMobile - Whether the device is currently considered mobile
|
|
860
|
+
* @param hasContent - Whether the tooltip has content to display
|
|
861
|
+
* @returns `true` if tooltip should be shown, `false` otherwise
|
|
862
|
+
*
|
|
863
|
+
* @example
|
|
864
|
+
* ```typescript
|
|
865
|
+
* const shouldShow = tooltipService.shouldShowTooltip('button', true, true);
|
|
866
|
+
* // shouldShow = false (button on mobile)
|
|
867
|
+
*
|
|
868
|
+
* const shouldShow2 = tooltipService.shouldShowTooltip('div', true, true);
|
|
869
|
+
* // shouldShow2 = true (non-interactive on mobile)
|
|
870
|
+
*
|
|
871
|
+
* const shouldShow3 = tooltipService.shouldShowTooltip('button', false, true);
|
|
872
|
+
* // shouldShow3 = true (button on desktop)
|
|
873
|
+
* ```
|
|
874
|
+
*/
|
|
875
|
+
shouldShowTooltip(elementTag: string, isMobile: boolean, hasContent: boolean): boolean;
|
|
876
|
+
/**
|
|
877
|
+
* Checks if an element tag represents an interactive element.
|
|
878
|
+
*
|
|
879
|
+
* Interactive elements (buttons, links, form controls) typically have
|
|
880
|
+
* their own click/tap handlers, and showing tooltips on mobile can
|
|
881
|
+
* interfere with the primary interaction.
|
|
882
|
+
*
|
|
883
|
+
* @param elementTag - Lowercase tag name to check
|
|
884
|
+
* @returns `true` if the element is interactive, `false` otherwise
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* ```typescript
|
|
888
|
+
* tooltipService.isInteractiveElement('button'); // true
|
|
889
|
+
* tooltipService.isInteractiveElement('ion-button'); // true
|
|
890
|
+
* tooltipService.isInteractiveElement('div'); // false
|
|
891
|
+
* tooltipService.isInteractiveElement('span'); // false
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
isInteractiveElement(elementTag: string): boolean;
|
|
895
|
+
/**
|
|
896
|
+
* Checks if a tooltip element is visible (has non-zero dimensions).
|
|
897
|
+
*
|
|
898
|
+
* This is useful for detecting if an element has been hidden or removed
|
|
899
|
+
* from the layout, which should trigger tooltip dismissal.
|
|
900
|
+
*
|
|
901
|
+
* @param rect - Bounding rectangle of the element
|
|
902
|
+
* @returns `true` if element has visible dimensions, `false` otherwise
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* ```typescript
|
|
906
|
+
* const isVisible = tooltipService.isElementVisible(
|
|
907
|
+
* element.getBoundingClientRect()
|
|
908
|
+
* );
|
|
909
|
+
* if (!isVisible) {
|
|
910
|
+
* this.hideTooltip();
|
|
911
|
+
* }
|
|
912
|
+
* ```
|
|
913
|
+
*/
|
|
914
|
+
isElementVisible(rect: ElementRect): boolean;
|
|
915
|
+
/**
|
|
916
|
+
*
|
|
917
|
+
* @returns Tooltip show delay in milliseconds.
|
|
918
|
+
*/
|
|
919
|
+
getShowDelayMs(): number;
|
|
920
|
+
/**
|
|
921
|
+
* @returns Tooltip close delay in milliseconds.
|
|
922
|
+
*/
|
|
923
|
+
getCloseDelayMs(): number;
|
|
924
|
+
/**
|
|
925
|
+
* @returns Tooltip fade duration in milliseconds.
|
|
926
|
+
*/
|
|
927
|
+
getFadeDurationMs(): number;
|
|
928
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<TooltipService, never>;
|
|
929
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<TooltipService>;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Handle interface for a single tooltip's scheduling logic.
|
|
934
|
+
* Exposes event methods to be called by the tooltip directive/component.
|
|
935
|
+
*/
|
|
936
|
+
interface TooltipSchedulerHandle {
|
|
937
|
+
destroy(): void;
|
|
938
|
+
onTriggerEnter(): void;
|
|
939
|
+
onTriggerLeave(): void;
|
|
940
|
+
onTooltipEnter(): void;
|
|
941
|
+
onTooltipLeave(): void;
|
|
942
|
+
onTooltipTouchStart(): void;
|
|
943
|
+
onTooltipTouchEnd(): void;
|
|
944
|
+
onClick(isMobile: boolean): void;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Singleton service that centralizes tooltip scheduling (show/hide delays).
|
|
948
|
+
* Purely orchestrates timers and delegates to provided callbacks; no DOM access.
|
|
949
|
+
*/
|
|
950
|
+
declare class TooltipSchedulerService {
|
|
951
|
+
private readonly tooltipService;
|
|
952
|
+
constructor(tooltipService: TooltipService);
|
|
953
|
+
/**
|
|
954
|
+
* Create an isolated scheduler handle for a single tooltip instance.
|
|
955
|
+
* The handle exposes the same event methods but keeps timers/state per-instance.
|
|
956
|
+
*/
|
|
957
|
+
createHandle(show: () => void, hide: () => void): TooltipSchedulerHandle;
|
|
958
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<TooltipSchedulerService, never>;
|
|
959
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<TooltipSchedulerService>;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export { ActionButtonListService, ActionButtonType, ButtonDisplayService, ButtonHandlerService, ButtonStateService, DeviceDetectionService, TooltipSchedulerService, TooltipService };
|
|
963
|
+
export type { ActionButton, ActionButtonConfig, BaseActionButtonConfig, ChildLoadingCallback, ElementRect, LoadingCallback, TooltipPlacement, TooltipPosition, TooltipSchedulerHandle };
|