@starwind-ui/core 1.13.0 → 1.15.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 (47) hide show
  1. package/dist/index.js +28 -5
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/components/badge/Badge.astro +8 -2
  4. package/dist/src/components/button/Button.astro +8 -7
  5. package/dist/src/components/collapsible/Collapsible.astro +161 -0
  6. package/dist/src/components/collapsible/CollapsibleContent.astro +22 -0
  7. package/dist/src/components/collapsible/CollapsibleTrigger.astro +44 -0
  8. package/dist/src/components/collapsible/index.ts +13 -0
  9. package/dist/src/components/dialog/Dialog.astro +35 -1
  10. package/dist/src/components/input-otp/InputOtp.astro +319 -0
  11. package/dist/src/components/input-otp/InputOtpGroup.astro +16 -0
  12. package/dist/src/components/input-otp/InputOtpSeparator.astro +25 -0
  13. package/dist/src/components/input-otp/InputOtpSlot.astro +48 -0
  14. package/dist/src/components/input-otp/InputOtpTypes.ts +6 -0
  15. package/dist/src/components/input-otp/index.ts +33 -0
  16. package/dist/src/components/prose/Prose.astro +617 -0
  17. package/dist/src/components/prose/index.ts +9 -0
  18. package/dist/src/components/select/Select.astro +3 -0
  19. package/dist/src/components/sidebar/Sidebar.astro +213 -0
  20. package/dist/src/components/sidebar/SidebarContent.astro +24 -0
  21. package/dist/src/components/sidebar/SidebarFooter.astro +21 -0
  22. package/dist/src/components/sidebar/SidebarGroup.astro +21 -0
  23. package/dist/src/components/sidebar/SidebarGroupContent.astro +21 -0
  24. package/dist/src/components/sidebar/SidebarGroupLabel.astro +52 -0
  25. package/dist/src/components/sidebar/SidebarHeader.astro +21 -0
  26. package/dist/src/components/sidebar/SidebarInput.astro +22 -0
  27. package/dist/src/components/sidebar/SidebarInset.astro +21 -0
  28. package/dist/src/components/sidebar/SidebarMenu.astro +21 -0
  29. package/dist/src/components/sidebar/SidebarMenuAction.astro +59 -0
  30. package/dist/src/components/sidebar/SidebarMenuBadge.astro +30 -0
  31. package/dist/src/components/sidebar/SidebarMenuButton.astro +129 -0
  32. package/dist/src/components/sidebar/SidebarMenuItem.astro +21 -0
  33. package/dist/src/components/sidebar/SidebarMenuSkeleton.astro +40 -0
  34. package/dist/src/components/sidebar/SidebarMenuSub.astro +24 -0
  35. package/dist/src/components/sidebar/SidebarMenuSubButton.astro +49 -0
  36. package/dist/src/components/sidebar/SidebarMenuSubItem.astro +16 -0
  37. package/dist/src/components/sidebar/SidebarProvider.astro +213 -0
  38. package/dist/src/components/sidebar/SidebarRail.astro +71 -0
  39. package/dist/src/components/sidebar/SidebarSeparator.astro +22 -0
  40. package/dist/src/components/sidebar/SidebarTrigger.astro +66 -0
  41. package/dist/src/components/sidebar/index.ts +103 -0
  42. package/dist/src/components/theme-toggle/ThemeToggle.astro +208 -0
  43. package/dist/src/components/theme-toggle/index.ts +7 -0
  44. package/dist/src/components/toggle/Toggle.astro +1 -1
  45. package/dist/src/components/tooltip/Tooltip.astro +80 -37
  46. package/dist/src/components/tooltip/TooltipContent.astro +9 -34
  47. package/package.json +1 -1
@@ -0,0 +1,208 @@
1
+ ---
2
+ // put the following script in the <head> of each page of your site
3
+ // this ensures it is run early to eliminate any flashes of incorrect color theme
4
+
5
+ /*
6
+ <script is:inline>
7
+ function initTheme() {
8
+ const colorTheme = localStorage.getItem("colorTheme");
9
+ if (!colorTheme) {
10
+ // if no color theme yet, use the users preferences
11
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
12
+ document.documentElement.classList.add("dark");
13
+ localStorage.setItem("colorTheme", "dark");
14
+ } else {
15
+ document.documentElement.classList.remove("dark");
16
+ localStorage.setItem("colorTheme", "light");
17
+ }
18
+ } else {
19
+ // assign the theme based on the value in local storage
20
+ if (colorTheme === "dark") {
21
+ document.documentElement.classList.add("dark");
22
+ } else if (colorTheme === "light") {
23
+ document.documentElement.classList.remove("dark");
24
+ } else if (colorTheme === "system") {
25
+ // use system preference
26
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
27
+ document.documentElement.classList.toggle("dark", prefersDark);
28
+ }
29
+ }
30
+ }
31
+
32
+ // runs on initial page load
33
+ initTheme();
34
+
35
+ // runs on view transitions navigation
36
+ document.addEventListener("astro:after-swap", initTheme);
37
+ </script>
38
+ */
39
+
40
+ import Moon from "@tabler/icons/outline/moon.svg";
41
+ import Sun from "@tabler/icons/outline/sun.svg";
42
+ import { tv, type VariantProps } from "tailwind-variants";
43
+
44
+ import { Toggle, ToggleVariants } from "@/components/starwind/toggle";
45
+
46
+ export const themeToggle = tv({
47
+ base: [
48
+ "starwind-theme-toggle",
49
+ "group hover:border-muted-foreground hover:bg-transparent data-[state=on]:bg-transparent",
50
+ ],
51
+ });
52
+
53
+ type Props = VariantProps<typeof ToggleVariants.toggle> & {
54
+ /**
55
+ * The ARIA label for the toggle
56
+ */
57
+ ariaLabel?: string;
58
+ /**
59
+ * Additional classes for the toggle
60
+ */
61
+ class?: string;
62
+ };
63
+
64
+ const {
65
+ class: className = "",
66
+ variant = "outline",
67
+ size = "md",
68
+ ariaLabel = "Toggle theme",
69
+ ...rest
70
+ } = Astro.props as Props;
71
+ ---
72
+
73
+ <Toggle
74
+ class={themeToggle({ class: className })}
75
+ variant={variant}
76
+ size={size}
77
+ aria-label={ariaLabel}
78
+ data-slot="theme-toggle"
79
+ {...rest}
80
+ >
81
+ <slot>
82
+ <span class="size-5" data-theme-icon-wrapper>
83
+ <slot name="light-icon">
84
+ <Sun class="hidden size-5 group-data-[state=off]:data-ready:block" data-theme-icon />
85
+ </slot>
86
+ <slot name="dark-icon">
87
+ <Moon class="hidden size-5 group-data-[state=on]:data-ready:block" data-theme-icon />
88
+ </slot>
89
+ </span>
90
+ </slot>
91
+ </Toggle>
92
+
93
+ <script>
94
+ import type { ToggleChangeEvent } from "@/components/starwind/toggle";
95
+
96
+ class ThemeToggleHandler {
97
+ private toggle: HTMLButtonElement;
98
+ private isInitializing = true;
99
+
100
+ constructor(toggle: HTMLButtonElement) {
101
+ this.toggle = toggle;
102
+
103
+ this.initializeState();
104
+ this.setupEventListeners();
105
+
106
+ // Mark initialization complete after a microtask
107
+ queueMicrotask(() => {
108
+ this.isInitializing = false;
109
+ });
110
+ }
111
+
112
+ private initializeState(): void {
113
+ // Determine if dark mode is active based on the document class
114
+ // This handles "system" correctly since initTheme already applied the right class
115
+ const isDark = document.documentElement.classList.contains("dark");
116
+
117
+ this.toggle.setAttribute("data-state", isDark ? "on" : "off");
118
+ this.toggle.setAttribute("aria-pressed", isDark.toString());
119
+
120
+ // Mark icons as ready to show (prevents flash of wrong icon)
121
+ this.toggle.querySelectorAll("[data-theme-icon]").forEach((icon) => {
122
+ icon.setAttribute("data-ready", "");
123
+ });
124
+ }
125
+
126
+ private setupEventListeners(): void {
127
+ this.toggle.addEventListener("starwind-toggle:change", (event: Event) =>
128
+ this.handleToggleChange(event),
129
+ );
130
+ }
131
+
132
+ private handleToggleChange(event: Event): void {
133
+ // Don't dispatch theme:change during initialization
134
+ if (this.isInitializing) {
135
+ return;
136
+ }
137
+
138
+ const { pressed } = (event as ToggleChangeEvent).detail;
139
+
140
+ // Update theme based on toggle state
141
+ const newTheme = pressed ? "dark" : "light";
142
+
143
+ document.documentElement.classList.toggle("dark", pressed);
144
+ localStorage.setItem("colorTheme", newTheme);
145
+
146
+ // Dispatch custom event to sync other theme toggles
147
+ document.dispatchEvent(
148
+ new CustomEvent("theme:change", {
149
+ detail: { theme: newTheme },
150
+ }),
151
+ );
152
+ }
153
+
154
+ public syncState(theme: string): void {
155
+ let shouldBeOn: boolean;
156
+ if (theme === "system") {
157
+ shouldBeOn = document.documentElement.classList.contains("dark");
158
+ } else {
159
+ shouldBeOn = theme === "dark";
160
+ }
161
+
162
+ const newState = shouldBeOn ? "on" : "off";
163
+
164
+ // Only update if state actually changed to prevent loops
165
+ if (this.toggle.getAttribute("data-state") !== newState) {
166
+ this.toggle.setAttribute("data-state", newState);
167
+ this.toggle.setAttribute("aria-pressed", shouldBeOn.toString());
168
+ }
169
+ }
170
+ }
171
+
172
+ // Store instances in a WeakMap to avoid memory leaks
173
+ const themeToggleInstances = new WeakMap<HTMLElement, ThemeToggleHandler>();
174
+ // Track active toggles for iteration (WeakMap is not iterable)
175
+ const activeToggles = new Set<HTMLButtonElement>();
176
+
177
+ const setupThemeToggles = (clearExisting = false) => {
178
+ // Clear stale references on page transitions to prevent memory leaks
179
+ if (clearExisting) {
180
+ activeToggles.clear();
181
+ }
182
+
183
+ document.querySelectorAll<HTMLButtonElement>(".starwind-theme-toggle").forEach((toggle) => {
184
+ if (!themeToggleInstances.has(toggle)) {
185
+ themeToggleInstances.set(toggle, new ThemeToggleHandler(toggle));
186
+ }
187
+ activeToggles.add(toggle);
188
+ });
189
+ };
190
+
191
+ // Listen for theme changes from other toggles
192
+ const handleThemeChange = (event: Event) => {
193
+ const { theme } = (event as CustomEvent).detail;
194
+
195
+ activeToggles.forEach((toggle) => {
196
+ const instance = themeToggleInstances.get(toggle);
197
+ if (instance) {
198
+ instance.syncState(theme);
199
+ }
200
+ });
201
+ };
202
+
203
+ document.addEventListener("theme:change", handleThemeChange);
204
+
205
+ setupThemeToggles();
206
+ document.addEventListener("astro:after-swap", () => setupThemeToggles(true));
207
+ document.addEventListener("starwind:init", () => setupThemeToggles());
208
+ </script>
@@ -0,0 +1,7 @@
1
+ import ThemeToggle, { themeToggle } from "./ThemeToggle.astro";
2
+
3
+ const ThemeToggleVariants = { themeToggle };
4
+
5
+ export { ThemeToggle, ThemeToggleVariants };
6
+
7
+ export default ThemeToggle;
@@ -10,7 +10,7 @@ export const toggle = tv({
10
10
  "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
11
11
  "focus-visible:border-outline focus-visible:ring-outline/50 focus-visible:ring-3",
12
12
  "transition-colors outline-none",
13
- "aria-invalid:ring-error/20 dark:aria-invalid:ring-error/40 aria-invalid:border-error",
13
+ "aria-invalid:ring-error/40 aria-invalid:border-error",
14
14
  ],
15
15
  variants: {
16
16
  variant: {
@@ -100,18 +100,6 @@ const {
100
100
  this.content.addEventListener("mouseleave", () => this.hide());
101
101
  }
102
102
 
103
- // if data-avoid-collisions exists, add resize listener to reset any translations
104
- if (this.content.hasAttribute("data-avoid-collisions")) {
105
- window.addEventListener(
106
- "resize",
107
- () => {
108
- if (!this.content) return;
109
- this.content.style.transform = "";
110
- },
111
- { passive: true },
112
- );
113
- }
114
-
115
103
  // Document events
116
104
  document.addEventListener("keydown", (e) => {
117
105
  if (e.key === "Escape" && this.tooltip.getAttribute("data-state") === "open") {
@@ -135,7 +123,7 @@ const {
135
123
  this.tooltip.setAttribute("data-state", "open");
136
124
  this.content.setAttribute("data-state", "open");
137
125
  this.content.style.display = "block";
138
- this.checkBoundary(this.content);
126
+ this.positionTooltip();
139
127
  this.clearOpenTimer();
140
128
  return;
141
129
  }
@@ -148,11 +136,89 @@ const {
148
136
  this.tooltip.setAttribute("data-state", "open");
149
137
  this.content.setAttribute("data-state", "open");
150
138
  this.content.style.display = "block";
151
- this.checkBoundary(this.content);
139
+ this.positionTooltip();
152
140
  this.openTimerRef = null;
153
141
  }, delay);
154
142
  }
155
143
 
144
+ private positionTooltip() {
145
+ if (!this.content || !this.trigger) return;
146
+
147
+ 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
+ }
198
+
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
+ }
217
+
218
+ this.content.style.top = `${top}px`;
219
+ this.content.style.left = `${left}px`;
220
+ }
221
+
156
222
  private hide(immediate: boolean = false) {
157
223
  if (!this.content || !this.trigger) return;
158
224
  this.clearOpenTimer();
@@ -183,29 +249,6 @@ const {
183
249
  );
184
250
  }
185
251
 
186
- private checkBoundary(tooltipElement: HTMLElement) {
187
- if (!tooltipElement) return;
188
-
189
- // if data-avoid-collisions does not exist, return
190
- if (!tooltipElement.hasAttribute("data-avoid-collisions")) return;
191
-
192
- const viewportWidth = window.innerWidth;
193
- const tooltipRect = tooltipElement.getBoundingClientRect();
194
- const padding = 16; // Add some padding from viewport edges
195
-
196
- // Check if tooltip extends beyond right edge of viewport
197
- if (tooltipRect.right > viewportWidth - padding) {
198
- const overflow = tooltipRect.right - (viewportWidth - padding);
199
- tooltipElement.style.transform = `translateX(-${overflow + padding}px)`;
200
- }
201
-
202
- // Check if tooltip extends beyond left edge of viewport
203
- if (tooltipRect.left < padding) {
204
- const overflow = padding - tooltipRect.left;
205
- tooltipElement.style.transform = `translateX(${overflow + padding}px)`;
206
- }
207
- }
208
-
209
252
  private clearOpenTimer() {
210
253
  if (this.openTimerRef) {
211
254
  window.clearTimeout(this.openTimerRef);
@@ -34,43 +34,20 @@ type Props = HTMLAttributes<"div"> & {
34
34
  export const tooltipContent = tv({
35
35
  base: [
36
36
  "starwind-tooltip-content",
37
- "absolute z-50 hidden w-fit px-3 py-1.5 will-change-transform",
37
+ "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
41
  ],
42
42
  variants: {
43
43
  side: {
44
- left: "slide-in-from-right-2 right-(--tooltip-offset)",
45
- right: "slide-in-from-left-2 left-(--tooltip-offset)",
46
- bottom: "slide-in-from-top-2 top-(--tooltip-offset)",
47
- top: "slide-in-from-bottom-2 bottom-(--tooltip-offset)",
48
- },
49
- align: { center: "", start: "", end: "" },
50
- sideAlign: {
51
- "top-center": "left-[50%] translate-x-[-50%]",
52
- "bottom-center": "left-[50%] translate-x-[-50%]",
53
- "left-center": "top-[50%] translate-y-[-50%]",
54
- "right-center": "top-[50%] translate-y-[-50%]",
55
- "top-start": "left-0",
56
- "bottom-start": "left-0",
57
- "top-end": "right-0",
58
- "bottom-end": "right-0",
59
- "left-start": "top-0",
60
- "right-start": "top-0",
61
- "left-end": "bottom-0",
62
- "right-end": "bottom-0",
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",
63
48
  },
64
49
  },
65
- defaultVariants: { side: "top", align: "center" },
66
- compoundVariants: [
67
- { side: ["top", "bottom"], align: "center", class: "left-[50%] translate-x-[-50%]" },
68
- { side: ["left", "right"], align: "center", class: "top-[50%] translate-y-[-50%]" },
69
- { side: ["top", "bottom"], align: "start", class: "left-0" },
70
- { side: ["top", "bottom"], align: "end", class: "right-0" },
71
- { side: ["left", "right"], align: "start", class: "top-0" },
72
- { side: ["left", "right"], align: "end", class: "bottom-0" },
73
- ],
50
+ defaultVariants: { side: "top" },
74
51
  });
75
52
 
76
53
  export const tooltipCaret = tv({
@@ -97,17 +74,15 @@ const {
97
74
  ---
98
75
 
99
76
  <div
100
- class={tooltipContent({ side, align, class: className })}
77
+ class={tooltipContent({ side, class: className })}
101
78
  data-slot="tooltip-content"
102
79
  data-state="closed"
103
80
  data-side={side}
104
81
  data-align={align}
82
+ data-side-offset={sideOffset}
105
83
  {...avoidCollisions && { "data-avoid-collisions": "" }}
106
84
  role="tooltip"
107
- style={{
108
- "--tooltip-offset": `calc(100% + ${sideOffset}px)`,
109
- animationDuration: `${animationDuration}ms`,
110
- }}
85
+ style={{ animationDuration: `${animationDuration}ms` }}
111
86
  >
112
87
  <slot> My tooltip! </slot>
113
88
  <CaretUp class={tooltipCaret({ side })} />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starwind-ui/core",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Starwind UI core components and registry",
5
5
  "license": "MIT",
6
6
  "author": {