@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.7.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 text shown on hover.
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 tooltip?: string;
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
- export { ActionButtonListService, ActionButtonType, ButtonDisplayService, ButtonHandlerService, ButtonStateService };
653
- export type { ActionButton, ActionButtonConfig, BaseActionButtonConfig, ChildLoadingCallback, LoadingCallback };
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 };