tailjng 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +12 -4
  2. package/cli/execute/init-app.js +5 -2
  3. package/cli/execute/sync-app.js +14 -2
  4. package/cli/settings/colors-config-utils.js +43 -8
  5. package/cli/settings/icons-config-utils.js +62 -0
  6. package/cli/settings/path-utils.js +32 -2
  7. package/cli/settings/project-utils.js +7 -1
  8. package/cli/templates/app.generator.js +2 -2
  9. package/fesm2022/tailjng.mjs +247 -80
  10. package/fesm2022/tailjng.mjs.map +1 -1
  11. package/lib/services/static/theme.service.d.ts +39 -1
  12. package/lib/utils/theme/theme-variables.util.d.ts +31 -0
  13. package/package.json +1 -1
  14. package/public-api.d.ts +2 -1
  15. package/registry/components.json +41 -18
  16. package/src/colors.safelist.css +2 -2
  17. package/src/lib/components/.config/README.md +11 -0
  18. package/src/lib/components/.config/colors/README.md +38 -0
  19. package/src/lib/components/{colors-config → .config/colors}/colors.config.ts +5 -5
  20. package/src/lib/components/{colors-config → .config/colors}/colors.safelist.css +2 -2
  21. package/src/lib/components/.config/icons/README.md +26 -0
  22. package/src/lib/components/.config/icons/icons.lucide.ts +134 -0
  23. package/src/lib/components/.config/input/README.md +24 -0
  24. package/src/lib/components/.config/input/input.classes.ts +119 -0
  25. package/src/lib/components/alert/alert-dialog/dialog-alert.component.css +244 -2
  26. package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +25 -38
  27. package/src/lib/components/alert/alert-dialog/dialog-alert.component.ts +66 -56
  28. package/src/lib/components/alert/alert-dialog/dialog-alert.types.ts +19 -0
  29. package/src/lib/components/alert/alert-toast/toast-alert.component.css +630 -12
  30. package/src/lib/components/alert/alert-toast/toast-alert.component.html +103 -102
  31. package/src/lib/components/alert/alert-toast/toast-alert.component.ts +485 -128
  32. package/src/lib/components/alert/alert-toast/toast-alert.types.ts +25 -0
  33. package/src/lib/components/badge/badge.component.html +34 -21
  34. package/src/lib/components/badge/badge.component.ts +140 -31
  35. package/src/lib/components/button/button.component.html +16 -10
  36. package/src/lib/components/button/button.component.ts +162 -22
  37. package/src/lib/components/card/card-complete/complete-card.component.html +2 -2
  38. package/src/lib/components/card/card-complete/complete-card.component.ts +26 -16
  39. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +2 -2
  40. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +26 -16
  41. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.css +97 -0
  42. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.html +54 -46
  43. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.ts +135 -64
  44. package/src/lib/components/checkbox/checkbox-input/input-checkbox.types.ts +3 -0
  45. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.css +112 -0
  46. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +28 -25
  47. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.ts +67 -15
  48. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.types.ts +1 -0
  49. package/src/lib/components/coach-mark/coach-mark.component.html +4 -22
  50. package/src/lib/components/coach-mark/coach-mark.component.scss +1 -1
  51. package/src/lib/components/coach-mark/coach-mark.component.ts +51 -18
  52. package/src/lib/components/coach-mark/coach-mark.directive.ts +133 -78
  53. package/src/lib/components/coach-mark/coach-mark.types.ts +12 -0
  54. package/src/lib/components/dialog/dialog.component.css +103 -1
  55. package/src/lib/components/dialog/dialog.component.html +46 -66
  56. package/src/lib/components/dialog/dialog.component.ts +136 -110
  57. package/src/lib/components/dialog/dialog.types.ts +19 -0
  58. package/src/lib/components/filter/filter-complete/complete-filter.component.html +16 -19
  59. package/src/lib/components/filter/filter-complete/complete-filter.component.scss +35 -0
  60. package/src/lib/components/filter/filter-complete/complete-filter.component.ts +58 -34
  61. package/src/lib/components/filter/filter-complete/complete-filter.types.ts +7 -0
  62. package/src/lib/components/filter/filter-complete/complete-filter.util.ts +16 -0
  63. package/src/lib/components/form/form-container/container-form.component.css +4 -0
  64. package/src/lib/components/form/form-container/container-form.component.html +2 -2
  65. package/src/lib/components/form/form-container/container-form.component.ts +72 -16
  66. package/src/lib/components/form/form-container/container-form.types.ts +42 -0
  67. package/src/lib/components/form/form-container/form-col-span.directive.ts +25 -0
  68. package/src/lib/components/form/form-sidebar/sidebar-form.component.css +276 -0
  69. package/src/lib/components/form/form-sidebar/sidebar-form.component.html +117 -125
  70. package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +109 -34
  71. package/src/lib/components/form/form-sidebar/sidebar-form.types.ts +3 -0
  72. package/src/lib/components/{toggle-radio/toggle-radio.component.css → form/form-validation/validation-form.component.css} +0 -1
  73. package/src/lib/components/form/form-validation/validation-form.component.html +10 -6
  74. package/src/lib/components/form/form-validation/validation-form.component.ts +99 -12
  75. package/src/lib/components/form/form-validation/validation-form.types.ts +33 -0
  76. package/src/lib/components/icon/icon.component.html +8 -5
  77. package/src/lib/components/icon/icon.component.ts +111 -9
  78. package/src/lib/components/input/input/input.component.html +19 -16
  79. package/src/lib/components/input/input/input.component.ts +130 -53
  80. package/src/lib/components/input/input/input.types.ts +8 -0
  81. package/src/lib/components/input/input-file/file-input.component.html +65 -56
  82. package/src/lib/components/input/input-file/file-input.component.ts +276 -173
  83. package/src/lib/components/input/input-file/file-input.types.ts +2 -0
  84. package/src/lib/components/input/input-range/range-input.component.css +67 -0
  85. package/src/lib/components/input/input-range/range-input.component.html +50 -58
  86. package/src/lib/components/input/input-range/range-input.component.ts +148 -60
  87. package/src/lib/components/input/input-range/range-input.types.ts +7 -0
  88. package/src/lib/components/input/input-textarea/textarea-input.component.html +16 -7
  89. package/src/lib/components/input/input-textarea/textarea-input.component.ts +140 -50
  90. package/src/lib/components/input/input-textarea/textarea-input.types.ts +2 -0
  91. package/src/lib/components/label/label.component.html +17 -16
  92. package/src/lib/components/label/label.component.ts +70 -16
  93. package/src/lib/components/label/label.types.ts +2 -0
  94. package/src/lib/components/menu/menu-options-table/menu-options-defaults.ts +34 -0
  95. package/src/lib/components/menu/menu-options-table/options-table-menu.component.html +34 -20
  96. package/src/lib/components/menu/menu-options-table/options-table-menu.component.ts +211 -58
  97. package/src/lib/components/menu/menu-options-table/options-table-menu.types.ts +38 -0
  98. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +49 -52
  99. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +112 -24
  100. package/src/lib/components/menu/options-coach-menu/options-coach-menu.types.ts +9 -0
  101. package/src/lib/components/mode-toggle/mode-toggle.component.html +11 -16
  102. package/src/lib/components/mode-toggle/mode-toggle.component.ts +69 -33
  103. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.html +4 -4
  104. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.ts +31 -7
  105. package/src/lib/components/paginator/paginator-complete/complete-paginator.types.ts +12 -0
  106. package/src/lib/components/paginator/paginator-complete/complete-paginator.util.ts +36 -0
  107. package/src/lib/components/progress-bar/progress-bar.component.css +11 -0
  108. package/src/lib/components/progress-bar/progress-bar.component.html +41 -40
  109. package/src/lib/components/progress-bar/progress-bar.component.ts +95 -11
  110. package/src/lib/components/progress-bar/progress-bar.types.ts +2 -0
  111. package/src/lib/components/select/select-dropdown/dropdown-select.component.css +6 -0
  112. package/src/lib/components/select/select-dropdown/dropdown-select.component.html +54 -44
  113. package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +450 -509
  114. package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +43 -0
  115. package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +179 -0
  116. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.css +6 -0
  117. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +131 -42
  118. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +491 -475
  119. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.types.ts +22 -0
  120. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +20 -0
  121. package/src/lib/components/select/select-multi-table/multi-table-select.component.css +10 -0
  122. package/src/lib/components/select/select-multi-table/multi-table-select.component.html +76 -60
  123. package/src/lib/components/select/select-multi-table/multi-table-select.component.ts +250 -313
  124. package/src/lib/components/select/select-multi-table/multi-table-select.types.ts +10 -0
  125. package/src/lib/components/select/select-multi-table/multi-table-select.util.ts +5 -0
  126. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.css +212 -0
  127. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.html +62 -53
  128. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.ts +84 -27
  129. package/src/lib/components/sidebar/sidebar-static/static-sidebar.types.ts +2 -0
  130. package/src/lib/components/table/table-complete/complete-table.component.html +15 -17
  131. package/src/lib/components/table/table-complete/complete-table.component.ts +190 -338
  132. package/src/lib/components/table/table-complete/complete-table.types.ts +28 -0
  133. package/src/lib/components/table/table-complete/complete-table.util.ts +236 -0
  134. package/src/lib/components/table/table-complete/index.ts +2 -0
  135. package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +34 -0
  136. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +73 -128
  137. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +542 -829
  138. package/src/lib/components/table/table-crud-complete/complete-crud-table.types.ts +57 -0
  139. package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +723 -0
  140. package/src/lib/components/table/table-crud-complete/index.ts +3 -0
  141. package/src/lib/components/theme-generator/theme-generator.component.css +21 -0
  142. package/src/lib/components/theme-generator/theme-generator.component.html +146 -116
  143. package/src/lib/components/theme-generator/theme-generator.component.ts +44 -24
  144. package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +8 -0
  145. package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +44 -0
  146. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.css +135 -0
  147. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.html +52 -0
  148. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +198 -0
  149. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.types.ts +1 -0
  150. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.css +108 -0
  151. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.html +37 -0
  152. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +193 -0
  153. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +1 -0
  154. package/src/lib/components/tooltip/tooltip.directive.ts +12 -9
  155. package/src/lib/components/tooltip/tooltip.service.ts +331 -133
  156. package/src/lib/components/tooltip/tooltip.types.ts +9 -0
  157. package/src/lib/components/viewer/viewer-image/image-viewer.component.css +90 -4
  158. package/src/lib/components/viewer/viewer-image/image-viewer.component.html +52 -103
  159. package/src/lib/components/viewer/viewer-image/image-viewer.component.ts +182 -177
  160. package/src/lib/components/viewer/viewer-image/image-viewer.types.ts +3 -0
  161. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.css +177 -0
  162. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.html +74 -24
  163. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.ts +168 -15
  164. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.types.ts +1 -0
  165. package/src/styles.css +2 -2
  166. package/lib/services/static/icons.service.d.ts +0 -65
  167. package/src/lib/components/colors-config/README.md +0 -38
  168. package/src/lib/components/form/form-sidebar/sidebar-form.component.scss +0 -0
  169. package/src/lib/components/form/form-validation/validation-form.component.scss +0 -0
  170. package/src/lib/components/menu/menu-options-table/options-table-menu.component.scss +0 -0
  171. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +0 -12
  172. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.scss +0 -0
  173. package/src/lib/components/toggle-radio/toggle-radio.component.html +0 -51
  174. package/src/lib/components/toggle-radio/toggle-radio.component.ts +0 -222
  175. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.scss +0 -0
  176. package/tailjng-0.1.6.tgz +0 -0
@@ -1,190 +1,388 @@
1
- import { Injectable, Renderer2, RendererFactory2 } from "@angular/core"
1
+ import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
2
+ import { TOOLTIP_OPPOSITE, TooltipPosition } from './tooltip.types';
2
3
 
4
+ export type { TooltipPosition } from './tooltip.types';
5
+
6
+ /** Minimum margin from the viewport edge. */
7
+ const VIEWPORT_MARGIN = 8;
8
+
9
+ /** Gap between tooltip and anchor (includes arrow). */
10
+ const TOOLTIP_GAP = 6;
11
+
12
+ /** Default maximum tooltip width. */
13
+ const DEFAULT_MAX_WIDTH = 230;
14
+
15
+ /**
16
+ * Singleton service that creates and positions the floating tooltip in `document.body`.
17
+ */
3
18
  @Injectable({
4
- providedIn: 'root'
19
+ providedIn: 'root',
5
20
  })
6
21
  export class JTooltipService {
7
-
8
22
  private readonly renderer: Renderer2;
9
23
 
10
24
  private tooltipElement: HTMLElement | null = null;
11
25
  private arrowElement: HTMLElement | null = null;
12
- private showArrow = true;
13
26
 
27
+ private showArrow = true;
14
28
  private offsetX = 0;
15
29
  private offsetY = 0;
30
+ private autoFlip = true;
31
+
32
+ private currentTarget: HTMLElement | null = null;
33
+ private preferredPosition: TooltipPosition = 'top';
16
34
 
17
35
  constructor(rendererFactory: RendererFactory2) {
18
36
  this.renderer = rendererFactory.createRenderer(null, null);
19
37
  }
20
38
 
21
-
22
39
  /**
23
- * Show the tooltip with the specified content, target element, position, arrow visibility, and offsets.
24
- * @param content The content to display in the tooltip.
25
- * @param target The target element to attach the tooltip to.
26
- * @param position The position of the tooltip relative to the target element.
27
- * @param showArrow Whether to show the arrow pointing to the target element.
28
- * @param offsetX The horizontal offset of the tooltip.
29
- * @param offsetY The vertical offset of the tooltip.
30
- * @returns
40
+ * Shows the tooltip anchored to the target element.
41
+ * @param content Tooltip text or HTML fragment.
42
+ * @param target Host element that triggers the tooltip.
43
+ * @param position Preferred position (may flip when `autoFlip` is true).
44
+ * @param showArrow Whether to show the arrow pointing at the anchor.
45
+ * @param offsetX Extra horizontal offset in px.
46
+ * @param offsetY Extra vertical offset in px.
47
+ * @param autoFlip When true, flips position when there is not enough space.
31
48
  */
32
49
  show(
33
50
  content: string | HTMLElement,
34
51
  target: HTMLElement,
35
- position = "top",
52
+ position: TooltipPosition = 'top',
36
53
  showArrow = true,
37
54
  offsetX = 0,
38
55
  offsetY = 0,
56
+ autoFlip = true,
39
57
  ) {
40
- if (!content || !target) return
58
+ if (!content || !target) {
59
+ return;
60
+ }
61
+
62
+ this.hide();
63
+ this.showArrow = showArrow;
64
+ this.offsetX = offsetX;
65
+ this.offsetY = offsetY;
66
+ this.autoFlip = autoFlip;
67
+ this.currentTarget = target;
68
+ this.preferredPosition = position;
41
69
 
42
- this.hide()
43
- this.showArrow = showArrow
44
- this.offsetX = offsetX
45
- this.offsetY = offsetY
70
+ this.ensureTooltipElement();
71
+ this.setContent(content);
72
+
73
+ if (this.showArrow && this.arrowElement) {
74
+ this.renderer.appendChild(this.tooltipElement!, this.arrowElement);
75
+ }
76
+
77
+ this.renderer.appendChild(document.body, this.tooltipElement!);
78
+ this.renderer.setStyle(this.tooltipElement, 'opacity', '0');
79
+ this.renderer.setStyle(this.tooltipElement, 'transform', 'scale(1)');
80
+ this.renderer.setStyle(this.tooltipElement, 'display', 'block');
81
+
82
+ requestAnimationFrame(() => {
83
+ this.applyPosition();
84
+ this.renderer.setStyle(this.tooltipElement!, 'opacity', '1');
85
+ });
86
+
87
+ window.addEventListener('scroll', this.repositionHandler, true);
88
+ window.addEventListener('resize', this.repositionHandler);
89
+ }
90
+
91
+ /**
92
+ * Hides the tooltip and removes global listeners.
93
+ */
94
+ hide() {
95
+ window.removeEventListener('scroll', this.repositionHandler, true);
96
+ window.removeEventListener('resize', this.repositionHandler);
97
+
98
+ this.currentTarget = null;
46
99
 
47
100
  if (!this.tooltipElement) {
48
- this.tooltipElement = this.renderer.createElement("div")
49
- this.renderer.addClass(this.tooltipElement, "j-tooltip")
50
- this.renderer.setStyle(this.tooltipElement, "position", "absolute")
51
- this.renderer.setStyle(this.tooltipElement, "z-index", "999999")
52
- this.renderer.setStyle(this.tooltipElement, "background-color", "#333")
53
- this.renderer.setStyle(this.tooltipElement, "color", "white")
54
- this.renderer.setStyle(this.tooltipElement, "padding", "0.5rem")
55
- this.renderer.setStyle(this.tooltipElement, "border-radius", "0.25rem")
56
- this.renderer.setStyle(this.tooltipElement, "font-size", "0.875rem")
57
- this.renderer.setStyle(this.tooltipElement, "pointer-events", "none")
58
- this.renderer.setStyle(this.tooltipElement, "box-shadow", "0 2px 4px rgba(0,0,0,0.2)")
59
- this.renderer.setStyle(this.tooltipElement, "max-width", "230px")
60
- this.renderer.setStyle(this.tooltipElement, "word-wrap", "break-word")
61
- this.renderer.setStyle(this.tooltipElement, "transition", "opacity 0.2s ease, transform 0.2s ease")
62
-
63
- this.arrowElement = this.renderer.createElement("div")
64
- this.renderer.setStyle(this.arrowElement, "position", "absolute")
65
- this.renderer.setStyle(this.arrowElement, "width", "0")
66
- this.renderer.setStyle(this.arrowElement, "height", "0")
67
- this.renderer.setStyle(this.arrowElement, "border-style", "solid")
101
+ return;
68
102
  }
69
103
 
70
- if (this.tooltipElement) {
71
- while (this.tooltipElement.firstChild) {
72
- this.tooltipElement.removeChild(this.tooltipElement.firstChild)
73
- }
104
+ this.renderer.setStyle(this.tooltipElement, 'opacity', '0');
105
+ this.renderer.setStyle(this.tooltipElement, 'transform', 'scale(0.95)');
106
+ this.renderer.setStyle(this.tooltipElement, 'display', 'none');
107
+ }
108
+
109
+ /** Repositions on scroll/resize while the tooltip is visible. */
110
+ private repositionHandler = () => {
111
+ if (this.tooltipElement && this.currentTarget && this.isVisible()) {
112
+ this.applyPosition();
74
113
  }
114
+ };
75
115
 
76
- if (typeof content === "string") {
77
- this.tooltipElement!.textContent = content
78
- } else if (content instanceof HTMLElement) {
79
- this.renderer.appendChild(this.tooltipElement!, content.cloneNode(true))
116
+ /**
117
+ * Computes position, flip, viewport clamp, and arrow aligned to the anchor.
118
+ */
119
+ private applyPosition() {
120
+ if (!this.tooltipElement || !this.currentTarget) {
121
+ return;
80
122
  }
81
123
 
82
- if (this.showArrow) {
83
- this.renderer.appendChild(this.tooltipElement!, this.arrowElement!)
124
+ let placement = this.preferredPosition;
125
+ const targetRect = this.currentTarget.getBoundingClientRect();
126
+
127
+ this.constrainMaxWidth(placement, targetRect);
128
+
129
+ const tooltipRect = this.tooltipElement.getBoundingClientRect();
130
+
131
+ if (this.autoFlip) {
132
+ placement = this.resolvePlacement(targetRect, tooltipRect, placement);
133
+ this.constrainMaxWidth(placement, targetRect);
84
134
  }
85
135
 
86
- this.renderer.appendChild(document.body, this.tooltipElement)
87
- this.renderer.setStyle(this.tooltipElement, "opacity", "0")
88
- this.renderer.setStyle(this.tooltipElement, "transform", "scale(1)")
89
- this.renderer.setStyle(this.tooltipElement, "display", "block")
136
+ const { top, left } = this.calculateCoords(
137
+ targetRect,
138
+ this.tooltipElement.getBoundingClientRect(),
139
+ placement,
140
+ );
90
141
 
91
- this.positionAbsolute(target, position)
142
+ this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
143
+ this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
92
144
 
93
- requestAnimationFrame(() => {
94
- this.renderer.setStyle(this.tooltipElement!, "opacity", "1")
95
- })
145
+ if (this.showArrow && this.arrowElement) {
146
+ this.positionArrow(
147
+ placement,
148
+ targetRect,
149
+ this.tooltipElement.getBoundingClientRect(),
150
+ left,
151
+ top,
152
+ );
153
+ }
96
154
  }
97
155
 
156
+ /**
157
+ * Reduces max-width when lateral space is smaller than the default.
158
+ * @param placement Current or candidate position.
159
+ * @param targetRect Anchor element bounding rect.
160
+ */
161
+ private constrainMaxWidth(placement: TooltipPosition, targetRect: DOMRect) {
162
+ if (!this.tooltipElement) {
163
+ return;
164
+ }
165
+
166
+ let maxWidth = DEFAULT_MAX_WIDTH;
98
167
 
168
+ if (placement === 'top' || placement === 'bottom') {
169
+ maxWidth = Math.min(maxWidth, window.innerWidth - VIEWPORT_MARGIN * 2);
170
+ } else if (placement === 'left') {
171
+ maxWidth = Math.min(maxWidth, targetRect.left - VIEWPORT_MARGIN - TOOLTIP_GAP);
172
+ } else {
173
+ maxWidth = Math.min(
174
+ maxWidth,
175
+ window.innerWidth - targetRect.right - VIEWPORT_MARGIN - TOOLTIP_GAP,
176
+ );
177
+ }
178
+
179
+ maxWidth = Math.max(80, maxWidth);
180
+ this.renderer.setStyle(this.tooltipElement, 'max-width', `${maxWidth}px`);
181
+ }
99
182
 
100
183
  /**
101
- * Hide the tooltip if it is currently displayed.
102
- * * This method sets the tooltip's opacity to 0, scales it down, and hides it from view.
103
- * @returns
184
+ * Chooses the final position; flips when the preferred side does not fit.
185
+ * @param targetRect Anchor bounding rect.
186
+ * @param tooltipRect Tooltip bounding rect.
187
+ * @param preferred Requested position.
188
+ * @returns Effective position.
104
189
  */
105
- hide() {
106
- if (this.tooltipElement) {
107
- this.renderer.setStyle(this.tooltipElement, "opacity", "0")
108
- this.renderer.setStyle(this.tooltipElement, "transform", "scale(0.95)")
109
- this.renderer.setStyle(this.tooltipElement, "display", "none")
190
+ private resolvePlacement(
191
+ targetRect: DOMRect,
192
+ tooltipRect: DOMRect,
193
+ preferred: TooltipPosition,
194
+ ): TooltipPosition {
195
+ if (this.fits(preferred, targetRect, tooltipRect)) {
196
+ return preferred;
197
+ }
198
+
199
+ const flipped = TOOLTIP_OPPOSITE[preferred];
200
+ if (this.fits(flipped, targetRect, tooltipRect)) {
201
+ return flipped;
110
202
  }
203
+
204
+ return preferred;
111
205
  }
112
206
 
207
+ /**
208
+ * Checks whether there is enough viewport space for the given position.
209
+ */
210
+ private fits(
211
+ placement: TooltipPosition,
212
+ targetRect: DOMRect,
213
+ tooltipRect: DOMRect,
214
+ ): boolean {
215
+ const needW = tooltipRect.width + TOOLTIP_GAP + VIEWPORT_MARGIN;
216
+ const needH = tooltipRect.height + TOOLTIP_GAP + VIEWPORT_MARGIN;
113
217
 
218
+ switch (placement) {
219
+ case 'top':
220
+ return targetRect.top >= needH;
221
+ case 'bottom':
222
+ return window.innerHeight - targetRect.bottom >= needH;
223
+ case 'left':
224
+ return targetRect.left >= needW;
225
+ case 'right':
226
+ return window.innerWidth - targetRect.right >= needW;
227
+ }
228
+ }
114
229
 
115
230
  /**
116
- * Position the tooltip absolutely relative to the target element.
117
- * @param target The target element to position the tooltip against.
118
- * @param position The position of the tooltip (top, bottom, left, right).
119
- * @returns
231
+ * Computes top/left in fixed coordinates and clamps them to the viewport.
120
232
  */
121
- positionAbsolute(target: HTMLElement, position: string) {
122
- if (!this.tooltipElement) return
233
+ private calculateCoords(
234
+ targetRect: DOMRect,
235
+ tooltipRect: DOMRect,
236
+ placement: TooltipPosition,
237
+ ): { top: number; left: number } {
238
+ let top = 0;
239
+ let left = 0;
123
240
 
124
- const tooltipRect = this.tooltipElement.getBoundingClientRect()
125
- const targetRect = target.getBoundingClientRect()
241
+ switch (placement) {
242
+ case 'top':
243
+ top = targetRect.top - tooltipRect.height - TOOLTIP_GAP;
244
+ left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
245
+ break;
246
+ case 'bottom':
247
+ top = targetRect.bottom + TOOLTIP_GAP;
248
+ left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
249
+ break;
250
+ case 'left':
251
+ top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2;
252
+ left = targetRect.left - tooltipRect.width - TOOLTIP_GAP;
253
+ break;
254
+ case 'right':
255
+ top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2;
256
+ left = targetRect.right + TOOLTIP_GAP;
257
+ break;
258
+ }
126
259
 
127
- const arrowSize = 6
128
- let left = 0
129
- let top = 0
260
+ left += this.offsetX;
261
+ top += this.offsetY;
130
262
 
131
- if (this.showArrow && this.arrowElement) {
132
- this.renderer.setStyle(this.arrowElement, "border-width", `${arrowSize}px`)
133
- this.renderer.setStyle(this.arrowElement, "border-color", "transparent")
134
- this.renderer.setStyle(this.arrowElement, "top", "")
135
- this.renderer.setStyle(this.arrowElement, "bottom", "")
136
- this.renderer.setStyle(this.arrowElement, "left", "")
137
- this.renderer.setStyle(this.arrowElement, "right", "")
138
- }
139
-
140
- switch (position) {
141
- case "top":
142
- left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 + this.offsetX
143
- top = targetRect.top - tooltipRect.height - arrowSize + this.offsetY
144
- if (this.showArrow && this.arrowElement) {
145
- this.renderer.setStyle(this.arrowElement, "bottom", `-5px`)
146
- this.renderer.setStyle(this.arrowElement, "left", `calc(50% - ${arrowSize}px)`)
147
- this.renderer.setStyle(this.arrowElement, "border-top-color", "#333")
148
- this.renderer.setStyle(this.arrowElement, "border-bottom-width", "0")
149
- }
150
- break
151
-
152
- case "bottom":
153
- left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 + this.offsetX
154
- top = targetRect.bottom + arrowSize + this.offsetY
155
- if (this.showArrow && this.arrowElement) {
156
- this.renderer.setStyle(this.arrowElement, "top", `-5px`)
157
- this.renderer.setStyle(this.arrowElement, "left", `calc(50% - ${arrowSize}px)`)
158
- this.renderer.setStyle(this.arrowElement, "border-bottom-color", "#333")
159
- this.renderer.setStyle(this.arrowElement, "border-top-width", "0")
160
- }
161
- break
162
-
163
- case "left":
164
- left = targetRect.left - tooltipRect.width - arrowSize + this.offsetX
165
- top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2 + this.offsetY
166
- if (this.showArrow && this.arrowElement) {
167
- this.renderer.setStyle(this.arrowElement, "right", `-5px`)
168
- this.renderer.setStyle(this.arrowElement, "top", `calc(50% - ${arrowSize}px)`)
169
- this.renderer.setStyle(this.arrowElement, "border-left-color", "#333")
170
- this.renderer.setStyle(this.arrowElement, "border-right-width", "0")
171
- }
172
- break
173
-
174
- case "right":
175
- left = targetRect.right + arrowSize + this.offsetX
176
- top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2 + this.offsetY
177
- if (this.showArrow && this.arrowElement) {
178
- this.renderer.setStyle(this.arrowElement, "left", `-5px`)
179
- this.renderer.setStyle(this.arrowElement, "top", `calc(50% - ${arrowSize}px)`)
180
- this.renderer.setStyle(this.arrowElement, "border-right-color", "#333")
181
- this.renderer.setStyle(this.arrowElement, "border-left-width", "0")
182
- }
183
- break
184
- }
185
-
186
- this.renderer.setStyle(this.tooltipElement, "left", `${left + window.scrollX}px`)
187
- this.renderer.setStyle(this.tooltipElement, "top", `${top + window.scrollY}px`)
263
+ left = Math.max(
264
+ VIEWPORT_MARGIN,
265
+ Math.min(left, window.innerWidth - tooltipRect.width - VIEWPORT_MARGIN),
266
+ );
267
+ top = Math.max(
268
+ VIEWPORT_MARGIN,
269
+ Math.min(top, window.innerHeight - tooltipRect.height - VIEWPORT_MARGIN),
270
+ );
271
+
272
+ return { top, left };
273
+ }
274
+
275
+ /**
276
+ * Positions the arrow toward the anchor center even when the tooltip was shifted.
277
+ */
278
+ private positionArrow(
279
+ placement: TooltipPosition,
280
+ targetRect: DOMRect,
281
+ tooltipRect: DOMRect,
282
+ tooltipLeft: number,
283
+ tooltipTop: number,
284
+ ) {
285
+ if (!this.arrowElement) {
286
+ return;
287
+ }
288
+
289
+ const arrowSize = 6;
290
+ this.renderer.setStyle(this.arrowElement, 'border-width', `${arrowSize}px`);
291
+ this.renderer.setStyle(this.arrowElement, 'border-color', 'transparent');
292
+ this.renderer.setStyle(this.arrowElement, 'top', '');
293
+ this.renderer.setStyle(this.arrowElement, 'bottom', '');
294
+ this.renderer.setStyle(this.arrowElement, 'left', '');
295
+ this.renderer.setStyle(this.arrowElement, 'right', '');
296
+ this.renderer.setStyle(this.arrowElement, 'transform', '');
297
+
298
+ const targetCenterX = targetRect.left + targetRect.width / 2;
299
+ const targetCenterY = targetRect.top + targetRect.height / 2;
300
+
301
+ const arrowPad = 10;
302
+ const arrowX = Math.max(
303
+ arrowPad,
304
+ Math.min(tooltipRect.width - arrowPad, targetCenterX - tooltipLeft),
305
+ );
306
+ const arrowY = Math.max(
307
+ arrowPad,
308
+ Math.min(tooltipRect.height - arrowPad, targetCenterY - tooltipTop),
309
+ );
310
+
311
+ switch (placement) {
312
+ case 'top':
313
+ this.renderer.setStyle(this.arrowElement, 'bottom', '-5px');
314
+ this.renderer.setStyle(this.arrowElement, 'left', `${arrowX - arrowSize}px`);
315
+ this.renderer.setStyle(this.arrowElement, 'border-top-color', '#333');
316
+ this.renderer.setStyle(this.arrowElement, 'border-bottom-width', '0');
317
+ break;
318
+ case 'bottom':
319
+ this.renderer.setStyle(this.arrowElement, 'top', '-5px');
320
+ this.renderer.setStyle(this.arrowElement, 'left', `${arrowX - arrowSize}px`);
321
+ this.renderer.setStyle(this.arrowElement, 'border-bottom-color', '#333');
322
+ this.renderer.setStyle(this.arrowElement, 'border-top-width', '0');
323
+ break;
324
+ case 'left':
325
+ this.renderer.setStyle(this.arrowElement, 'right', '-5px');
326
+ this.renderer.setStyle(this.arrowElement, 'top', `${arrowY - arrowSize}px`);
327
+ this.renderer.setStyle(this.arrowElement, 'border-left-color', '#333');
328
+ this.renderer.setStyle(this.arrowElement, 'border-right-width', '0');
329
+ break;
330
+ case 'right':
331
+ this.renderer.setStyle(this.arrowElement, 'left', '-5px');
332
+ this.renderer.setStyle(this.arrowElement, 'top', `${arrowY - arrowSize}px`);
333
+ this.renderer.setStyle(this.arrowElement, 'border-right-color', '#333');
334
+ this.renderer.setStyle(this.arrowElement, 'border-left-width', '0');
335
+ break;
336
+ }
337
+ }
338
+
339
+ /** Creates the tooltip DOM node if it does not exist yet. */
340
+ private ensureTooltipElement() {
341
+ if (this.tooltipElement) {
342
+ return;
343
+ }
344
+
345
+ this.tooltipElement = this.renderer.createElement('div');
346
+ this.renderer.addClass(this.tooltipElement, 'j-tooltip');
347
+ this.renderer.setStyle(this.tooltipElement, 'position', 'fixed');
348
+ this.renderer.setStyle(this.tooltipElement, 'z-index', '999999');
349
+ this.renderer.setStyle(this.tooltipElement, 'background-color', '#333');
350
+ this.renderer.setStyle(this.tooltipElement, 'color', 'white');
351
+ this.renderer.setStyle(this.tooltipElement, 'padding', '0.5rem');
352
+ this.renderer.setStyle(this.tooltipElement, 'border-radius', '0.25rem');
353
+ this.renderer.setStyle(this.tooltipElement, 'font-size', '0.875rem');
354
+ this.renderer.setStyle(this.tooltipElement, 'pointer-events', 'none');
355
+ this.renderer.setStyle(this.tooltipElement, 'box-shadow', '0 2px 4px rgba(0,0,0,0.2)');
356
+ this.renderer.setStyle(this.tooltipElement, 'max-width', `${DEFAULT_MAX_WIDTH}px`);
357
+ this.renderer.setStyle(this.tooltipElement, 'word-wrap', 'break-word');
358
+ this.renderer.setStyle(this.tooltipElement, 'transition', 'opacity 0.2s ease, transform 0.2s ease');
359
+
360
+ this.arrowElement = this.renderer.createElement('div');
361
+ this.renderer.setStyle(this.arrowElement, 'position', 'absolute');
362
+ this.renderer.setStyle(this.arrowElement, 'width', '0');
363
+ this.renderer.setStyle(this.arrowElement, 'height', '0');
364
+ this.renderer.setStyle(this.arrowElement, 'border-style', 'solid');
365
+ }
366
+
367
+ /** Inserts text or HTML into the tooltip. */
368
+ private setContent(content: string | HTMLElement) {
369
+ if (!this.tooltipElement) {
370
+ return;
371
+ }
372
+
373
+ while (this.tooltipElement.firstChild) {
374
+ this.tooltipElement.removeChild(this.tooltipElement.firstChild);
375
+ }
376
+
377
+ if (typeof content === 'string') {
378
+ this.tooltipElement.textContent = content;
379
+ } else {
380
+ this.renderer.appendChild(this.tooltipElement, content.cloneNode(true));
381
+ }
188
382
  }
189
383
 
384
+ /** @returns true when the tooltip is visible on screen. */
385
+ private isVisible(): boolean {
386
+ return this.tooltipElement?.style.display === 'block';
387
+ }
190
388
  }
@@ -0,0 +1,9 @@
1
+ /** Preferred tooltip position relative to the anchor element. */
2
+ export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
3
+
4
+ export const TOOLTIP_OPPOSITE: Record<TooltipPosition, TooltipPosition> = {
5
+ top: 'bottom',
6
+ bottom: 'top',
7
+ left: 'right',
8
+ right: 'left',
9
+ };
@@ -1,4 +1,90 @@
1
- img {
2
- -webkit-user-drag: none; /* Safari y Chrome */
3
- user-select: none; /* Evita selección de texto */
4
- }
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ height: 100%;
5
+ }
6
+
7
+ .j-image-viewer {
8
+ position: relative;
9
+ width: 100%;
10
+ height: 100%;
11
+ }
12
+
13
+ .j-image-viewer__toolbar {
14
+ position: absolute;
15
+ z-index: 20;
16
+ display: flex;
17
+ flex-wrap: wrap;
18
+ align-items: center;
19
+ justify-content: flex-end;
20
+ gap: 0.25rem;
21
+ max-width: calc(100% - 1rem);
22
+ }
23
+
24
+ .j-image-viewer__stage {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ width: 100%;
29
+ height: 100%;
30
+ user-select: none;
31
+ }
32
+
33
+ .j-image-viewer__stage--fullscreen {
34
+ background-color: var(--color-background);
35
+ }
36
+
37
+ :host-context(.dark) .j-image-viewer__stage--fullscreen,
38
+ :host-context(html.dark) .j-image-viewer__stage--fullscreen {
39
+ background-color: var(--color-dark-background);
40
+ }
41
+
42
+ .j-image-viewer__canvas {
43
+ position: relative;
44
+ width: 100%;
45
+ height: 100%;
46
+ overflow: hidden;
47
+ }
48
+
49
+ .j-image-viewer__image {
50
+ width: 100%;
51
+ height: 100%;
52
+ object-fit: contain;
53
+ pointer-events: auto;
54
+ user-select: none;
55
+ -webkit-user-drag: none;
56
+ }
57
+
58
+ .j-image-viewer__overlay {
59
+ position: absolute;
60
+ inset: 0;
61
+ display: flex;
62
+ flex-direction: column;
63
+ align-items: center;
64
+ justify-content: center;
65
+ gap: 0.75rem;
66
+ }
67
+
68
+ .j-image-viewer__overlay--loading {
69
+ background-color: color-mix(in srgb, var(--color-background) 70%, transparent);
70
+ }
71
+
72
+ :host-context(.dark) .j-image-viewer__overlay--loading,
73
+ :host-context(html.dark) .j-image-viewer__overlay--loading {
74
+ background-color: color-mix(in srgb, var(--color-dark-background) 55%, transparent);
75
+ }
76
+
77
+ .j-image-viewer__message {
78
+ font-size: 0.875rem;
79
+ line-height: 1.25;
80
+ color: var(--color-muted-foreground);
81
+ }
82
+
83
+ :host-context(.dark) .j-image-viewer__message,
84
+ :host-context(html.dark) .j-image-viewer__message {
85
+ color: var(--color-dark-muted-foreground);
86
+ }
87
+
88
+ .j-image-viewer__message--error {
89
+ color: var(--color-destructive);
90
+ }