@starwind-ui/core 1.15.4 → 1.16.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.
Files changed (183) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +67 -43
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/components/accordion/Accordion.astro +1 -1
  5. package/dist/src/components/accordion/AccordionContent.astro +1 -1
  6. package/dist/src/components/accordion/AccordionItem.astro +1 -1
  7. package/dist/src/components/accordion/AccordionTrigger.astro +1 -1
  8. package/dist/src/components/alert/Alert.astro +1 -1
  9. package/dist/src/components/alert/AlertDescription.astro +1 -1
  10. package/dist/src/components/alert/AlertTitle.astro +1 -1
  11. package/dist/src/components/alert-dialog/AlertDialog.astro +1 -1
  12. package/dist/src/components/alert-dialog/AlertDialogAction.astro +1 -1
  13. package/dist/src/components/alert-dialog/AlertDialogCancel.astro +1 -1
  14. package/dist/src/components/alert-dialog/AlertDialogContent.astro +3 -3
  15. package/dist/src/components/alert-dialog/AlertDialogDescription.astro +1 -1
  16. package/dist/src/components/alert-dialog/AlertDialogFooter.astro +1 -1
  17. package/dist/src/components/alert-dialog/AlertDialogHeader.astro +1 -1
  18. package/dist/src/components/alert-dialog/AlertDialogTitle.astro +1 -1
  19. package/dist/src/components/alert-dialog/AlertDialogTrigger.astro +2 -2
  20. package/dist/src/components/aspect-ratio/AspectRatio.astro +1 -1
  21. package/dist/src/components/avatar/Avatar.astro +1 -1
  22. package/dist/src/components/avatar/AvatarFallback.astro +1 -1
  23. package/dist/src/components/avatar/AvatarImage.astro +2 -2
  24. package/dist/src/components/badge/Badge.astro +1 -1
  25. package/dist/src/components/breadcrumb/Breadcrumb.astro +1 -1
  26. package/dist/src/components/breadcrumb/BreadcrumbEllipsis.astro +1 -1
  27. package/dist/src/components/breadcrumb/BreadcrumbItem.astro +1 -1
  28. package/dist/src/components/breadcrumb/BreadcrumbLink.astro +1 -1
  29. package/dist/src/components/breadcrumb/BreadcrumbList.astro +2 -2
  30. package/dist/src/components/breadcrumb/BreadcrumbPage.astro +1 -1
  31. package/dist/src/components/breadcrumb/BreadcrumbSeparator.astro +1 -1
  32. package/dist/src/components/button/Button.astro +1 -1
  33. package/dist/src/components/button-group/ButtonGroup.astro +1 -1
  34. package/dist/src/components/button-group/ButtonGroupSeparator.astro +1 -1
  35. package/dist/src/components/button-group/ButtonGroupText.astro +1 -1
  36. package/dist/src/components/card/Card.astro +1 -1
  37. package/dist/src/components/card/CardAction.astro +1 -1
  38. package/dist/src/components/card/CardContent.astro +1 -1
  39. package/dist/src/components/card/CardDescription.astro +1 -1
  40. package/dist/src/components/card/CardFooter.astro +1 -1
  41. package/dist/src/components/card/CardHeader.astro +1 -1
  42. package/dist/src/components/card/CardTitle.astro +1 -1
  43. package/dist/src/components/carousel/Carousel.astro +1 -1
  44. package/dist/src/components/carousel/CarouselContent.astro +1 -1
  45. package/dist/src/components/carousel/CarouselItem.astro +1 -1
  46. package/dist/src/components/carousel/CarouselNext.astro +1 -1
  47. package/dist/src/components/carousel/CarouselPrevious.astro +1 -1
  48. package/dist/src/components/checkbox/Checkbox.astro +1 -1
  49. package/dist/src/components/collapsible/Collapsible.astro +1 -1
  50. package/dist/src/components/collapsible/CollapsibleContent.astro +1 -1
  51. package/dist/src/components/collapsible/CollapsibleTrigger.astro +1 -1
  52. package/dist/src/components/dialog/Dialog.astro +1 -1
  53. package/dist/src/components/dialog/DialogClose.astro +1 -1
  54. package/dist/src/components/dialog/DialogContent.astro +2 -2
  55. package/dist/src/components/dialog/DialogDescription.astro +1 -1
  56. package/dist/src/components/dialog/DialogFooter.astro +1 -1
  57. package/dist/src/components/dialog/DialogHeader.astro +1 -1
  58. package/dist/src/components/dialog/DialogTitle.astro +1 -1
  59. package/dist/src/components/dialog/DialogTrigger.astro +1 -1
  60. package/dist/src/components/dropdown/Dropdown.astro +261 -34
  61. package/dist/src/components/dropdown/DropdownContent.astro +41 -5
  62. package/dist/src/components/dropdown/DropdownItem.astro +2 -2
  63. package/dist/src/components/dropdown/DropdownLabel.astro +2 -2
  64. package/dist/src/components/dropdown/DropdownSeparator.astro +1 -1
  65. package/dist/src/components/dropdown/DropdownShortcut.astro +17 -0
  66. package/dist/src/components/dropdown/DropdownSub.astro +15 -0
  67. package/dist/src/components/dropdown/DropdownSubContent.astro +36 -0
  68. package/dist/src/components/dropdown/DropdownSubTrigger.astro +34 -0
  69. package/dist/src/components/dropdown/DropdownTrigger.astro +1 -1
  70. package/dist/src/components/dropdown/index.ts +12 -0
  71. package/dist/src/components/dropzone/DropzoneFilesList.astro +1 -1
  72. package/dist/src/components/image/Image.astro +1 -1
  73. package/dist/src/components/input/Input.astro +1 -1
  74. package/dist/src/components/input-group/InputGroup.astro +28 -0
  75. package/dist/src/components/input-group/InputGroupAddon.astro +79 -0
  76. package/dist/src/components/input-group/InputGroupButton.astro +34 -0
  77. package/dist/src/components/input-group/InputGroupInput.astro +16 -0
  78. package/dist/src/components/input-group/InputGroupText.astro +17 -0
  79. package/dist/src/components/input-group/InputGroupTextarea.astro +20 -0
  80. package/dist/src/components/input-group/index.ts +33 -0
  81. package/dist/src/components/input-otp/InputOtp.astro +1 -1
  82. package/dist/src/components/input-otp/InputOtpGroup.astro +1 -1
  83. package/dist/src/components/input-otp/InputOtpSeparator.astro +1 -1
  84. package/dist/src/components/input-otp/InputOtpSlot.astro +1 -1
  85. package/dist/src/components/item/Item.astro +1 -1
  86. package/dist/src/components/item/ItemActions.astro +1 -1
  87. package/dist/src/components/item/ItemContent.astro +1 -1
  88. package/dist/src/components/item/ItemDescription.astro +1 -1
  89. package/dist/src/components/item/ItemFooter.astro +1 -1
  90. package/dist/src/components/item/ItemGroup.astro +1 -1
  91. package/dist/src/components/item/ItemHeader.astro +1 -1
  92. package/dist/src/components/item/ItemMedia.astro +1 -1
  93. package/dist/src/components/item/ItemSeparator.astro +1 -1
  94. package/dist/src/components/item/ItemTitle.astro +1 -1
  95. package/dist/src/components/kbd/Kbd.astro +1 -1
  96. package/dist/src/components/kbd/KbdGroup.astro +1 -1
  97. package/dist/src/components/label/Label.astro +1 -1
  98. package/dist/src/components/native-select/NativeSelect.astro +64 -0
  99. package/dist/src/components/native-select/NativeSelectOptGroup.astro +15 -0
  100. package/dist/src/components/native-select/NativeSelectOption.astro +15 -0
  101. package/dist/src/components/native-select/index.ts +21 -0
  102. package/dist/src/components/pagination/Pagination.astro +1 -1
  103. package/dist/src/components/pagination/PaginationContent.astro +1 -1
  104. package/dist/src/components/pagination/PaginationEllipsis.astro +1 -1
  105. package/dist/src/components/pagination/PaginationItem.astro +1 -1
  106. package/dist/src/components/pagination/PaginationLink.astro +1 -1
  107. package/dist/src/components/pagination/PaginationNext.astro +1 -1
  108. package/dist/src/components/pagination/PaginationPrevious.astro +1 -1
  109. package/dist/src/components/popover/Popover.astro +717 -0
  110. package/dist/src/components/popover/PopoverContent.astro +102 -0
  111. package/dist/src/components/popover/PopoverDescription.astro +14 -0
  112. package/dist/src/components/popover/PopoverHeader.astro +14 -0
  113. package/dist/src/components/popover/PopoverTitle.astro +14 -0
  114. package/dist/src/components/popover/PopoverTrigger.astro +51 -0
  115. package/dist/src/components/popover/index.ts +34 -0
  116. package/dist/src/components/progress/Progress.astro +1 -1
  117. package/dist/src/components/prose/Prose.astro +1 -1
  118. package/dist/src/components/radio-group/RadioGroup.astro +1 -1
  119. package/dist/src/components/radio-group/RadioGroupItem.astro +1 -1
  120. package/dist/src/components/select/Select.astro +1 -1
  121. package/dist/src/components/select/SelectContent.astro +1 -1
  122. package/dist/src/components/select/SelectItem.astro +1 -1
  123. package/dist/src/components/select/SelectLabel.astro +1 -1
  124. package/dist/src/components/select/SelectSearch.astro +1 -1
  125. package/dist/src/components/select/SelectSeparator.astro +1 -1
  126. package/dist/src/components/select/SelectTrigger.astro +1 -1
  127. package/dist/src/components/select/SelectValue.astro +1 -1
  128. package/dist/src/components/separator/Separator.astro +1 -1
  129. package/dist/src/components/sheet/Sheet.astro +1 -1
  130. package/dist/src/components/sheet/SheetContent.astro +1 -1
  131. package/dist/src/components/sheet/SheetDescription.astro +1 -1
  132. package/dist/src/components/sheet/SheetFooter.astro +1 -1
  133. package/dist/src/components/sheet/SheetHeader.astro +1 -1
  134. package/dist/src/components/sheet/SheetTitle.astro +1 -1
  135. package/dist/src/components/sidebar/Sidebar.astro +19 -2
  136. package/dist/src/components/sidebar/SidebarContent.astro +1 -1
  137. package/dist/src/components/sidebar/SidebarFooter.astro +1 -1
  138. package/dist/src/components/sidebar/SidebarGroup.astro +1 -1
  139. package/dist/src/components/sidebar/SidebarGroupContent.astro +1 -1
  140. package/dist/src/components/sidebar/SidebarGroupLabel.astro +2 -2
  141. package/dist/src/components/sidebar/SidebarHeader.astro +1 -1
  142. package/dist/src/components/sidebar/SidebarInput.astro +1 -1
  143. package/dist/src/components/sidebar/SidebarInset.astro +1 -1
  144. package/dist/src/components/sidebar/SidebarMenu.astro +1 -1
  145. package/dist/src/components/sidebar/SidebarMenuAction.astro +1 -1
  146. package/dist/src/components/sidebar/SidebarMenuBadge.astro +1 -1
  147. package/dist/src/components/sidebar/SidebarMenuButton.astro +2 -14
  148. package/dist/src/components/sidebar/SidebarMenuItem.astro +1 -1
  149. package/dist/src/components/sidebar/SidebarMenuSkeleton.astro +1 -1
  150. package/dist/src/components/sidebar/SidebarMenuSub.astro +1 -1
  151. package/dist/src/components/sidebar/SidebarMenuSubButton.astro +1 -1
  152. package/dist/src/components/sidebar/SidebarMenuSubItem.astro +1 -1
  153. package/dist/src/components/sidebar/SidebarProvider.astro +1 -1
  154. package/dist/src/components/sidebar/SidebarRail.astro +2 -2
  155. package/dist/src/components/sidebar/SidebarSeparator.astro +1 -1
  156. package/dist/src/components/sidebar/SidebarTrigger.astro +1 -1
  157. package/dist/src/components/skeleton/Skeleton.astro +1 -1
  158. package/dist/src/components/slider/Slider.astro +1 -1
  159. package/dist/src/components/spinner/Spinner.astro +1 -1
  160. package/dist/src/components/switch/Switch.astro +1 -1
  161. package/dist/src/components/table/Table.astro +1 -1
  162. package/dist/src/components/table/TableBody.astro +1 -1
  163. package/dist/src/components/table/TableCaption.astro +1 -1
  164. package/dist/src/components/table/TableCell.astro +1 -1
  165. package/dist/src/components/table/TableFoot.astro +1 -1
  166. package/dist/src/components/table/TableHead.astro +1 -1
  167. package/dist/src/components/table/TableHeader.astro +1 -1
  168. package/dist/src/components/table/TableRow.astro +1 -1
  169. package/dist/src/components/tabs/Tabs.astro +1 -1
  170. package/dist/src/components/tabs/TabsContent.astro +1 -1
  171. package/dist/src/components/tabs/TabsList.astro +1 -1
  172. package/dist/src/components/tabs/TabsTrigger.astro +1 -1
  173. package/dist/src/components/textarea/Textarea.astro +1 -1
  174. package/dist/src/components/theme-toggle/ThemeToggle.astro +1 -1
  175. package/dist/src/components/toast/ToastDescription.astro +1 -1
  176. package/dist/src/components/toast/ToastItem.astro +1 -1
  177. package/dist/src/components/toast/Toaster.astro +1 -1
  178. package/dist/src/components/toggle/Toggle.astro +1 -1
  179. package/dist/src/components/tooltip/Tooltip.astro +260 -122
  180. package/dist/src/components/tooltip/TooltipContent.astro +13 -23
  181. package/dist/src/components/video/Video.astro +2 -2
  182. package/dist/src/lib/utils/starwind/positioning.ts +318 -0
  183. package/package.json +1 -1
@@ -29,28 +29,45 @@ const {
29
29
 
30
30
  <div
31
31
  class={tooltip({ class: className })}
32
- data-slot="tooltip"
33
32
  data-state="closed"
34
33
  data-open-delay={openDelay}
35
34
  data-close-delay={closeDelay}
36
35
  {...!disableHoverableContent && { "data-content-hoverable": "" }}
36
+ data-slot="tooltip"
37
37
  >
38
38
  <slot />
39
39
  </div>
40
40
 
41
41
  <script>
42
+ import {
43
+ type FloatingAlign,
44
+ type FloatingSide,
45
+ getTransformOrigin,
46
+ resolvePlacement,
47
+ } from "@/lib/utils/starwind/positioning";
48
+
42
49
  class TooltipHandler {
43
50
  private tooltip: HTMLElement;
44
51
  private trigger: HTMLElement | null;
45
52
  private content: HTMLElement | null;
46
53
  private openTimerRef: number | null = null;
47
54
  private closeTimerRef: number | null = null;
55
+ private hideAnimationTimerRef: number | null = null;
48
56
  private contentId: string;
49
57
  private animationDuration = 150;
58
+ private side: FloatingSide = "top";
59
+ private align: FloatingAlign = "center";
60
+ private sideOffset = 8;
61
+ private avoidCollisions = true;
62
+ private isOpen = false;
63
+ private contentPlaceholder: Comment | null = null;
64
+ private cleanupAutoUpdate: (() => void) | null = null;
65
+ private abortController: AbortController;
50
66
 
51
67
  constructor(tooltip: HTMLElement, idx: number) {
52
68
  this.contentId = `starwind-tooltip${idx}`;
53
69
  this.tooltip = tooltip;
70
+ this.abortController = new AbortController();
54
71
  this.content = tooltip.querySelector(".starwind-tooltip-content");
55
72
 
56
73
  // if tooltip.firstElementChild is this.content, then get the next element
@@ -59,10 +76,10 @@ const {
59
76
  this.trigger = this.trigger.nextElementSibling as HTMLElement;
60
77
  }
61
78
 
62
- this.trigger.classList.add("starwind-tooltip-trigger");
63
-
64
79
  if (!this.trigger || !this.content) return;
65
80
 
81
+ this.trigger.classList.add("starwind-tooltip-trigger");
82
+
66
83
  // animationDuration is set with inline styles through passed prop to TooltipContent
67
84
  const animationDurationString = this.content.style.animationDuration;
68
85
  if (animationDurationString.endsWith("ms")) {
@@ -71,12 +88,37 @@ const {
71
88
  // using something like @playform/compress might optimize to use "s" instead of "ms"
72
89
  this.animationDuration = parseFloat(animationDurationString) * 1000;
73
90
  }
91
+
92
+ const contentSide = this.content.getAttribute("data-side");
93
+ if (
94
+ contentSide === "top" ||
95
+ contentSide === "bottom" ||
96
+ contentSide === "left" ||
97
+ contentSide === "right"
98
+ ) {
99
+ this.side = contentSide;
100
+ }
101
+
102
+ const contentAlign = this.content.getAttribute("data-align");
103
+ if (contentAlign === "start" || contentAlign === "center" || contentAlign === "end") {
104
+ this.align = contentAlign;
105
+ }
106
+
107
+ const contentSideOffset = parseFloat(this.content.getAttribute("data-side-offset") || "8");
108
+ if (Number.isFinite(contentSideOffset)) {
109
+ this.sideOffset = contentSideOffset;
110
+ }
111
+
112
+ this.avoidCollisions = this.content.hasAttribute("data-avoid-collisions");
113
+ this.isOpen = this.tooltip.getAttribute("data-state") === "open";
114
+
74
115
  this.init();
75
116
  }
76
117
 
77
118
  private init() {
78
119
  this.setupAccessibility();
79
120
  this.setupEvents();
121
+ this.setInitialState();
80
122
  }
81
123
 
82
124
  private setupAccessibility() {
@@ -85,170 +127,240 @@ const {
85
127
  this.content.id = this.contentId;
86
128
  }
87
129
 
130
+ private setInitialState() {
131
+ if (!this.content || !this.trigger) return;
132
+
133
+ if (this.isOpen) {
134
+ this.tooltip.setAttribute("data-state", "open");
135
+ this.trigger.setAttribute("data-state", "open");
136
+ this.content.setAttribute("data-state", "open");
137
+ this.content.classList.remove("hidden");
138
+ this.portalContent();
139
+ this.positionTooltip();
140
+ requestAnimationFrame(() => this.positionTooltip());
141
+ } else {
142
+ this.tooltip.setAttribute("data-state", "closed");
143
+ this.trigger.setAttribute("data-state", "closed");
144
+ this.content.setAttribute("data-state", "closed");
145
+ this.content.classList.add("hidden");
146
+ }
147
+ }
148
+
88
149
  private setupEvents() {
89
150
  if (!this.trigger || !this.content) return;
90
151
 
91
152
  // Trigger events
92
- this.trigger.addEventListener("mouseenter", () => this.show());
93
- this.trigger.addEventListener("mouseleave", () => this.hide());
94
- this.trigger.addEventListener("focus", () => this.show(true));
95
- this.trigger.addEventListener("blur", () => this.hide(true));
153
+ this.trigger.addEventListener("mouseenter", () => this.show(), {
154
+ signal: this.abortController.signal,
155
+ });
156
+ this.trigger.addEventListener("mouseleave", () => this.hide(), {
157
+ signal: this.abortController.signal,
158
+ });
159
+ this.trigger.addEventListener("focus", () => this.show(true), {
160
+ signal: this.abortController.signal,
161
+ });
162
+ this.trigger.addEventListener("blur", () => this.hide(true), {
163
+ signal: this.abortController.signal,
164
+ });
96
165
 
97
166
  // Content events
98
167
  if (this.tooltip.hasAttribute("data-content-hoverable")) {
99
- this.content.addEventListener("mouseenter", () => this.show());
100
- this.content.addEventListener("mouseleave", () => this.hide());
168
+ this.content.addEventListener("mouseenter", () => this.show(), {
169
+ signal: this.abortController.signal,
170
+ });
171
+ this.content.addEventListener("mouseleave", () => this.hide(), {
172
+ signal: this.abortController.signal,
173
+ });
101
174
  }
102
175
 
103
176
  // Document events
104
- document.addEventListener("keydown", (e) => {
105
- if (e.key === "Escape" && this.tooltip.getAttribute("data-state") === "open") {
106
- this.hide(true);
107
- }
108
- });
177
+ document.addEventListener(
178
+ "keydown",
179
+ (e) => {
180
+ if (e.key === "Escape" && this.isOpen) {
181
+ this.hide(true);
182
+ }
183
+ },
184
+ { signal: this.abortController.signal },
185
+ );
109
186
 
110
- document.addEventListener("click", (e) => {
111
- if (
112
- !this.tooltip.contains(e.target as Node) &&
113
- this.tooltip.getAttribute("data-state") === "open"
114
- ) {
115
- this.hide(true);
116
- }
117
- });
187
+ document.addEventListener(
188
+ "pointerdown",
189
+ (e) => {
190
+ if (
191
+ this.isOpen &&
192
+ !this.tooltip.contains(e.target as Node) &&
193
+ !this.content?.contains(e.target as Node)
194
+ ) {
195
+ this.hide(true);
196
+ }
197
+ },
198
+ { signal: this.abortController.signal },
199
+ );
200
+ }
201
+
202
+ private portalContent() {
203
+ if (!this.content) return;
204
+ if (this.contentPlaceholder || this.content.parentElement === document.body) return;
205
+
206
+ this.contentPlaceholder = document.createComment("tooltip-content-placeholder");
207
+ this.content.parentNode?.insertBefore(this.contentPlaceholder, this.content);
208
+
209
+ document.body.appendChild(this.content);
210
+
211
+ this.content.style.position = "fixed";
212
+ this.content.style.zIndex = "50";
213
+
214
+ const updatePosition = () => this.positionTooltip();
215
+ window.addEventListener("scroll", updatePosition, true);
216
+ window.addEventListener("resize", updatePosition);
217
+
218
+ const resizeObserver = new ResizeObserver(() => this.positionTooltip());
219
+ resizeObserver.observe(this.content);
220
+ if (this.trigger) {
221
+ resizeObserver.observe(this.trigger);
222
+ }
223
+
224
+ this.cleanupAutoUpdate = () => {
225
+ window.removeEventListener("scroll", updatePosition, true);
226
+ window.removeEventListener("resize", updatePosition);
227
+ resizeObserver.disconnect();
228
+ };
229
+ }
230
+
231
+ private unportalContent() {
232
+ if (!this.content) return;
233
+
234
+ this.cleanupAutoUpdate?.();
235
+ this.cleanupAutoUpdate = null;
236
+
237
+ if (this.contentPlaceholder) {
238
+ this.contentPlaceholder.parentNode?.insertBefore(this.content, this.contentPlaceholder);
239
+ this.contentPlaceholder.remove();
240
+ this.contentPlaceholder = null;
241
+ }
242
+
243
+ this.content.style.removeProperty("position");
244
+ this.content.style.removeProperty("z-index");
245
+ this.content.style.removeProperty("top");
246
+ this.content.style.removeProperty("left");
247
+ this.content.style.removeProperty("transform-origin");
248
+ }
249
+
250
+ private openNow() {
251
+ if (!this.content || !this.trigger) return;
252
+ if (this.abortController.signal.aborted) return;
253
+
254
+ this.clearHideAnimationTimer();
255
+ this.isOpen = true;
256
+ this.tooltip.setAttribute("data-state", "open");
257
+ this.trigger.setAttribute("data-state", "open");
258
+ this.content.setAttribute("data-state", "open");
259
+ this.content.classList.remove("hidden");
260
+ this.portalContent();
261
+ this.positionTooltip();
262
+ requestAnimationFrame(() => this.positionTooltip());
118
263
  }
119
264
 
120
265
  private show(immediate: boolean = false) {
121
266
  if (!this.content || !this.trigger) return;
267
+
268
+ this.clearCloseTimer();
269
+ this.clearHideAnimationTimer();
270
+ this.clearOpenTimer();
271
+
122
272
  if (immediate) {
123
- this.tooltip.setAttribute("data-state", "open");
124
- this.content.setAttribute("data-state", "open");
125
- this.content.style.display = "block";
126
- this.positionTooltip();
273
+ this.openNow();
127
274
  this.clearOpenTimer();
128
275
  return;
129
276
  }
130
277
 
131
- this.clearCloseTimer();
132
-
133
- const delay = parseInt(this.tooltip.getAttribute("data-open-delay") || "700");
278
+ const delay = parseInt(this.tooltip.getAttribute("data-open-delay") || "200");
134
279
  this.openTimerRef = window.setTimeout(() => {
135
- if (!this.content || !this.trigger) return;
136
- this.tooltip.setAttribute("data-state", "open");
137
- this.content.setAttribute("data-state", "open");
138
- this.content.style.display = "block";
139
- this.positionTooltip();
280
+ if (this.abortController.signal.aborted) return;
281
+ this.openNow();
140
282
  this.openTimerRef = null;
141
283
  }, delay);
142
284
  }
143
285
 
144
286
  private positionTooltip() {
145
- if (!this.content || !this.trigger) return;
287
+ if (!this.content || !this.trigger || !this.isOpen) return;
146
288
 
147
289
  const triggerRect = this.trigger.getBoundingClientRect();
148
- const contentRect = this.content.getBoundingClientRect();
149
- const side = this.content.dataset.side || "top";
150
- const align = this.content.dataset.align || "center";
151
- const sideOffset = parseInt(this.content.dataset.sideOffset || "8");
152
-
153
- let top = 0;
154
- let left = 0;
155
-
156
- // Calculate position based on side
157
- switch (side) {
158
- case "top":
159
- top = triggerRect.top - contentRect.height - sideOffset;
160
- break;
161
- case "bottom":
162
- top = triggerRect.bottom + sideOffset;
163
- break;
164
- case "left":
165
- left = triggerRect.left - contentRect.width - sideOffset;
166
- break;
167
- case "right":
168
- left = triggerRect.right + sideOffset;
169
- break;
170
- }
171
-
172
- // Calculate alignment
173
- if (side === "top" || side === "bottom") {
174
- switch (align) {
175
- case "center":
176
- left = triggerRect.left + (triggerRect.width - contentRect.width) / 2;
177
- break;
178
- case "start":
179
- left = triggerRect.left;
180
- break;
181
- case "end":
182
- left = triggerRect.right - contentRect.width;
183
- break;
184
- }
185
- } else {
186
- switch (align) {
187
- case "center":
188
- top = triggerRect.top + (triggerRect.height - contentRect.height) / 2;
189
- break;
190
- case "start":
191
- top = triggerRect.top;
192
- break;
193
- case "end":
194
- top = triggerRect.bottom - contentRect.height;
195
- break;
196
- }
197
- }
290
+ const contentWidth = this.content.offsetWidth;
291
+ const contentHeight = this.content.offsetHeight;
292
+ if (contentWidth === 0 || contentHeight === 0) return;
293
+
294
+ const resolvedPlacement = resolvePlacement({
295
+ side: this.side,
296
+ align: this.align,
297
+ sideOffset: this.sideOffset,
298
+ triggerRect,
299
+ contentWidth,
300
+ contentHeight,
301
+ viewportWidth: window.innerWidth,
302
+ viewportHeight: window.innerHeight,
303
+ viewportPadding: 8,
304
+ avoidCollisions: this.avoidCollisions,
305
+ });
198
306
 
199
- // Apply collision avoidance if enabled
200
- if (this.content.hasAttribute("data-avoid-collisions")) {
201
- const padding = 8;
202
- const viewportWidth = window.innerWidth;
203
- const viewportHeight = window.innerHeight;
204
-
205
- // Horizontal bounds
206
- if (left < padding) left = padding;
207
- if (left + contentRect.width > viewportWidth - padding) {
208
- left = viewportWidth - contentRect.width - padding;
209
- }
210
-
211
- // Vertical bounds
212
- if (top < padding) top = padding;
213
- if (top + contentRect.height > viewportHeight - padding) {
214
- top = viewportHeight - contentRect.height - padding;
215
- }
216
- }
307
+ this.content.style.left = `${Math.round(resolvedPlacement.left)}px`;
308
+ this.content.style.top = `${Math.round(resolvedPlacement.top)}px`;
309
+ this.content.style.transformOrigin = getTransformOrigin(
310
+ resolvedPlacement.side,
311
+ resolvedPlacement.align,
312
+ );
313
+ this.content.setAttribute("data-side", resolvedPlacement.side);
314
+ this.content.setAttribute("data-align", resolvedPlacement.align);
315
+ }
217
316
 
218
- this.content.style.top = `${top}px`;
219
- this.content.style.left = `${left}px`;
317
+ private closeNow() {
318
+ if (!this.content || !this.trigger) return;
319
+ this.isOpen = false;
320
+ this.tooltip.setAttribute("data-state", "closed");
321
+ this.trigger.setAttribute("data-state", "closed");
322
+ this.content.setAttribute("data-state", "closed");
323
+
324
+ this.clearHideAnimationTimer();
325
+ this.hideAnimationTimerRef = window.setTimeout(() => {
326
+ if (this.abortController.signal.aborted) return;
327
+ if (!this.content) return;
328
+ this.content.classList.add("hidden");
329
+ this.unportalContent();
330
+ }, this.animationDuration);
220
331
  }
221
332
 
222
333
  private hide(immediate: boolean = false) {
223
334
  if (!this.content || !this.trigger) return;
224
335
  this.clearOpenTimer();
336
+ this.clearCloseTimer();
225
337
 
226
338
  if (immediate) {
227
- this.clearCloseTimer();
228
- this.tooltip.setAttribute("data-state", "closed");
229
- setTimeout(() => {
230
- if (!this.content) return;
231
- this.content.style.display = "none";
232
- }, this.animationDuration);
233
- this.content.setAttribute("data-state", "closed");
339
+ this.closeNow();
234
340
  return;
235
341
  }
236
342
 
237
343
  this.closeTimerRef = window.setTimeout(
238
344
  () => {
239
- this.tooltip.setAttribute("data-state", "closed");
240
- setTimeout(() => {
241
- if (!this.content) return;
242
- this.content.style.display = "none";
243
- }, this.animationDuration);
244
- if (!this.content) return;
245
- this.content.setAttribute("data-state", "closed");
345
+ if (this.abortController.signal.aborted) return;
346
+ this.closeNow();
246
347
  this.closeTimerRef = null;
247
348
  },
248
- parseInt(this.tooltip.getAttribute("data-close-delay") || "300"),
349
+ parseInt(this.tooltip.getAttribute("data-close-delay") || "200"),
249
350
  );
250
351
  }
251
352
 
353
+ destroy() {
354
+ if (this.abortController.signal.aborted) return;
355
+
356
+ this.abortController.abort();
357
+ this.clearOpenTimer();
358
+ this.clearCloseTimer();
359
+ this.clearHideAnimationTimer();
360
+ this.unportalContent();
361
+ this.isOpen = false;
362
+ }
363
+
252
364
  private clearOpenTimer() {
253
365
  if (this.openTimerRef) {
254
366
  window.clearTimeout(this.openTimerRef);
@@ -262,13 +374,38 @@ const {
262
374
  this.closeTimerRef = null;
263
375
  }
264
376
  }
377
+
378
+ private clearHideAnimationTimer() {
379
+ if (this.hideAnimationTimerRef) {
380
+ window.clearTimeout(this.hideAnimationTimerRef);
381
+ this.hideAnimationTimerRef = null;
382
+ }
383
+ }
265
384
  }
266
385
 
267
- // Store instances in a WeakMap to avoid memory leaks
268
- const tooltipInstances = new WeakMap<HTMLElement, TooltipHandler>();
386
+ const tooltipInstances = new Map<HTMLElement, TooltipHandler>();
269
387
  let tooltipCounter = 0;
270
388
 
389
+ const cleanupStaleTooltips = () => {
390
+ for (const [tooltipEl, tooltipHandler] of tooltipInstances.entries()) {
391
+ if (!document.contains(tooltipEl)) {
392
+ tooltipHandler.destroy();
393
+ tooltipInstances.delete(tooltipEl);
394
+ }
395
+ }
396
+ };
397
+
398
+ const destroyAllTooltips = () => {
399
+ for (const tooltipHandler of tooltipInstances.values()) {
400
+ tooltipHandler.destroy();
401
+ }
402
+
403
+ tooltipInstances.clear();
404
+ };
405
+
271
406
  const setupTooltips = () => {
407
+ cleanupStaleTooltips();
408
+
272
409
  document.querySelectorAll<HTMLElement>(".starwind-tooltip").forEach((tooltip) => {
273
410
  if (!tooltipInstances.has(tooltip)) {
274
411
  tooltipInstances.set(tooltip, new TooltipHandler(tooltip, tooltipCounter++));
@@ -277,6 +414,7 @@ const {
277
414
  };
278
415
 
279
416
  setupTooltips();
417
+ document.addEventListener("astro:before-swap", destroyAllTooltips);
280
418
  document.addEventListener("astro:after-swap", setupTooltips);
281
419
  document.addEventListener("starwind:init", setupTooltips);
282
420
  </script>
@@ -34,33 +34,23 @@ type Props = HTMLAttributes<"div"> & {
34
34
  export const tooltipContent = tv({
35
35
  base: [
36
36
  "starwind-tooltip-content",
37
- "fixed z-50 hidden w-fit px-3 py-1.5",
37
+ "group fixed z-50 hidden w-fit px-3 py-1.5",
38
38
  "bg-foreground text-background rounded-md",
39
39
  "animate-in fade-in zoom-in-95",
40
40
  "data-[state=closed]:animate-out data-[state=closed]:fill-mode-forwards fade-out zoom-out-95",
41
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
42
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
41
43
  ],
42
- variants: {
43
- side: {
44
- left: "slide-in-from-right-2",
45
- right: "slide-in-from-left-2",
46
- bottom: "slide-in-from-top-2",
47
- top: "slide-in-from-bottom-2",
48
- },
49
- },
50
- defaultVariants: { side: "top" },
51
44
  });
52
45
 
53
46
  export const tooltipCaret = tv({
54
- base: ["text-foreground absolute z-50 size-4"],
55
- variants: {
56
- side: {
57
- top: "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(50%+1px)] rotate-180",
58
- bottom: "top-0 left-1/2 -translate-x-1/2 -translate-y-[calc(50%+1px)]",
59
- left: "top-1/2 right-0 translate-x-[calc(50%+1px)] -translate-y-1/2 rotate-90",
60
- right: "top-1/2 left-0 -translate-x-[calc(50%+1px)] -translate-y-1/2 -rotate-90",
61
- },
62
- },
63
- defaultVariants: { side: "top" },
47
+ base: [
48
+ "text-foreground absolute z-50 size-4",
49
+ "group-data-[side=top]:bottom-0 group-data-[side=top]:left-1/2 group-data-[side=top]:-translate-x-1/2 group-data-[side=top]:translate-y-[calc(50%+1px)] group-data-[side=top]:rotate-180",
50
+ "group-data-[side=bottom]:top-0 group-data-[side=bottom]:left-1/2 group-data-[side=bottom]:-translate-x-1/2 group-data-[side=bottom]:-translate-y-[calc(50%+1px)]",
51
+ "group-data-[side=left]:top-1/2 group-data-[side=left]:right-0 group-data-[side=left]:translate-x-[calc(50%+1px)] group-data-[side=left]:-translate-y-1/2 group-data-[side=left]:rotate-90",
52
+ "group-data-[side=right]:top-1/2 group-data-[side=right]:left-0 group-data-[side=right]:-translate-x-[calc(50%+1px)] group-data-[side=right]:-translate-y-1/2 group-data-[side=right]:-rotate-90",
53
+ ],
64
54
  });
65
55
 
66
56
  const {
@@ -74,8 +64,7 @@ const {
74
64
  ---
75
65
 
76
66
  <div
77
- class={tooltipContent({ side, class: className })}
78
- data-slot="tooltip-content"
67
+ class={tooltipContent({ class: className })}
79
68
  data-state="closed"
80
69
  data-side={side}
81
70
  data-align={align}
@@ -83,7 +72,8 @@ const {
83
72
  {...avoidCollisions && { "data-avoid-collisions": "" }}
84
73
  role="tooltip"
85
74
  style={{ animationDuration: `${animationDuration}ms` }}
75
+ data-slot="tooltip-content"
86
76
  >
87
77
  <slot> My tooltip! </slot>
88
- <CaretUp class={tooltipCaret({ side })} />
78
+ <CaretUp class={tooltipCaret()} />
89
79
  </div>
@@ -99,8 +99,8 @@ const embedUrl = youtubeId ? buildYouTubeEmbedUrl(youtubeId, isShort) : null;
99
99
  loop={loop}
100
100
  controls={controls}
101
101
  poster={poster}
102
- data-slot="video"
103
102
  {...rest}
103
+ data-slot="video"
104
104
  >
105
105
  <track kind="captions" />
106
106
  </video>
@@ -112,9 +112,9 @@ const embedUrl = youtubeId ? buildYouTubeEmbedUrl(youtubeId, isShort) : null;
112
112
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
113
113
  referrerpolicy="strict-origin-when-cross-origin"
114
114
  allowfullscreen
115
- data-slot="video"
116
115
  data-video-type={isShort ? "youtube-shorts" : "youtube"}
117
116
  {...rest}
117
+ data-slot="video"
118
118
  />
119
119
  )
120
120
  }