@starwind-ui/core 1.6.2 → 1.7.1

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 (101) hide show
  1. package/dist/index.js +8 -4
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/components/accordion/Accordion.astro +224 -224
  4. package/dist/src/components/accordion/AccordionContent.astro +13 -13
  5. package/dist/src/components/accordion/AccordionItem.astro +6 -6
  6. package/dist/src/components/accordion/AccordionTrigger.astro +13 -13
  7. package/dist/src/components/accordion/index.ts +4 -4
  8. package/dist/src/components/alert/Alert.astro +15 -15
  9. package/dist/src/components/alert/AlertDescription.astro +1 -1
  10. package/dist/src/components/alert/AlertTitle.astro +2 -2
  11. package/dist/src/components/alert/index.ts +3 -3
  12. package/dist/src/components/avatar/Avatar.astro +16 -16
  13. package/dist/src/components/avatar/AvatarFallback.astro +3 -3
  14. package/dist/src/components/avatar/AvatarImage.astro +12 -12
  15. package/dist/src/components/avatar/index.ts +4 -4
  16. package/dist/src/components/badge/Badge.astro +33 -33
  17. package/dist/src/components/breadcrumb/Breadcrumb.astro +1 -1
  18. package/dist/src/components/breadcrumb/BreadcrumbEllipsis.astro +6 -6
  19. package/dist/src/components/breadcrumb/BreadcrumbItem.astro +1 -1
  20. package/dist/src/components/breadcrumb/BreadcrumbLink.astro +8 -8
  21. package/dist/src/components/breadcrumb/BreadcrumbList.astro +2 -2
  22. package/dist/src/components/breadcrumb/BreadcrumbPage.astro +6 -6
  23. package/dist/src/components/breadcrumb/BreadcrumbSeparator.astro +7 -7
  24. package/dist/src/components/breadcrumb/index.ts +14 -14
  25. package/dist/src/components/button/Button.astro +38 -38
  26. package/dist/src/components/card/Card.astro +1 -1
  27. package/dist/src/components/card/CardContent.astro +1 -1
  28. package/dist/src/components/card/CardDescription.astro +1 -1
  29. package/dist/src/components/card/CardFooter.astro +1 -1
  30. package/dist/src/components/card/CardHeader.astro +1 -1
  31. package/dist/src/components/card/CardTitle.astro +1 -1
  32. package/dist/src/components/card/index.ts +7 -7
  33. package/dist/src/components/checkbox/Checkbox.astro +89 -89
  34. package/dist/src/components/dialog/Dialog.astro +237 -178
  35. package/dist/src/components/dialog/DialogClose.astro +14 -14
  36. package/dist/src/components/dialog/DialogContent.astro +32 -32
  37. package/dist/src/components/dialog/DialogDescription.astro +1 -1
  38. package/dist/src/components/dialog/DialogFooter.astro +1 -1
  39. package/dist/src/components/dialog/DialogHeader.astro +1 -1
  40. package/dist/src/components/dialog/DialogTitle.astro +6 -6
  41. package/dist/src/components/dialog/DialogTrigger.astro +26 -20
  42. package/dist/src/components/dialog/index.ts +16 -16
  43. package/dist/src/components/dropdown/Dropdown.astro +359 -359
  44. package/dist/src/components/dropdown/DropdownContent.astro +63 -63
  45. package/dist/src/components/dropdown/DropdownItem.astro +31 -31
  46. package/dist/src/components/dropdown/DropdownLabel.astro +14 -14
  47. package/dist/src/components/dropdown/DropdownSeparator.astro +5 -5
  48. package/dist/src/components/dropdown/DropdownTrigger.astro +26 -26
  49. package/dist/src/components/dropdown/index.ts +12 -12
  50. package/dist/src/components/dropzone/Dropzone.astro +232 -0
  51. package/dist/src/components/dropzone/DropzoneFilesList.astro +25 -0
  52. package/dist/src/components/dropzone/DropzoneLoadingIndicator.astro +10 -0
  53. package/dist/src/components/dropzone/DropzoneUploadIndicator.astro +10 -0
  54. package/dist/src/components/dropzone/index.ts +13 -0
  55. package/dist/src/components/input/Input.astro +12 -12
  56. package/dist/src/components/label/Label.astro +8 -8
  57. package/dist/src/components/pagination/Pagination.astro +1 -1
  58. package/dist/src/components/pagination/PaginationContent.astro +3 -3
  59. package/dist/src/components/pagination/PaginationEllipsis.astro +2 -2
  60. package/dist/src/components/pagination/PaginationItem.astro +3 -3
  61. package/dist/src/components/pagination/PaginationLink.astro +27 -27
  62. package/dist/src/components/pagination/PaginationNext.astro +7 -6
  63. package/dist/src/components/pagination/PaginationPrevious.astro +7 -6
  64. package/dist/src/components/pagination/index.ts +14 -14
  65. package/dist/src/components/progress/Progress.astro +151 -0
  66. package/dist/src/components/progress/index.ts +5 -0
  67. package/dist/src/components/radio-group/RadioGroup.astro +156 -0
  68. package/dist/src/components/radio-group/RadioGroupItem.astro +125 -0
  69. package/dist/src/components/radio-group/RadioGroupTypes.ts +6 -0
  70. package/dist/src/components/radio-group/index.ts +10 -0
  71. package/dist/src/components/select/Select.astro +515 -475
  72. package/dist/src/components/select/SelectContent.astro +62 -62
  73. package/dist/src/components/select/SelectItem.astro +27 -27
  74. package/dist/src/components/select/SelectLabel.astro +1 -1
  75. package/dist/src/components/select/SelectTrigger.astro +28 -28
  76. package/dist/src/components/select/SelectTypes.ts +11 -5
  77. package/dist/src/components/select/SelectValue.astro +5 -5
  78. package/dist/src/components/select/index.ts +16 -16
  79. package/dist/src/components/skeleton/Skeleton.astro +14 -0
  80. package/dist/src/components/skeleton/index.ts +5 -0
  81. package/dist/src/components/switch/Switch.astro +150 -150
  82. package/dist/src/components/switch/SwitchTypes.ts +4 -4
  83. package/dist/src/components/table/Table.astro +5 -5
  84. package/dist/src/components/table/TableBody.astro +3 -3
  85. package/dist/src/components/table/TableCaption.astro +3 -3
  86. package/dist/src/components/table/TableCell.astro +3 -3
  87. package/dist/src/components/table/TableFoot.astro +3 -3
  88. package/dist/src/components/table/TableHead.astro +3 -3
  89. package/dist/src/components/table/TableHeader.astro +3 -3
  90. package/dist/src/components/table/TableRow.astro +3 -3
  91. package/dist/src/components/table/index.ts +8 -8
  92. package/dist/src/components/tabs/Tabs.astro +250 -250
  93. package/dist/src/components/tabs/TabsContent.astro +10 -10
  94. package/dist/src/components/tabs/TabsList.astro +2 -2
  95. package/dist/src/components/tabs/TabsTrigger.astro +15 -15
  96. package/dist/src/components/tabs/index.ts +4 -4
  97. package/dist/src/components/textarea/Textarea.astro +16 -16
  98. package/dist/src/components/tooltip/Tooltip.astro +217 -217
  99. package/dist/src/components/tooltip/TooltipContent.astro +81 -81
  100. package/dist/src/components/tooltip/index.ts +3 -3
  101. package/package.json +6 -6
@@ -1,67 +1,67 @@
1
1
  ---
2
2
  import Check from "@tabler/icons/outline/check.svg";
3
3
  import type { HTMLAttributes } from "astro/types";
4
- import { type VariantProps, tv } from "tailwind-variants";
4
+ import { tv, type VariantProps } from "tailwind-variants";
5
5
 
6
6
  type Props = Omit<HTMLAttributes<"input">, "type"> &
7
- VariantProps<typeof checkbox> & {
8
- /**
9
- * Optional label text to display next to the checkbox
10
- */
11
- label?: string;
12
- };
7
+ VariantProps<typeof checkbox> & {
8
+ /**
9
+ * Optional label text to display next to the checkbox
10
+ */
11
+ label?: string;
12
+ };
13
13
 
14
14
  const checkbox = tv({
15
- slots: {
16
- base: "starwind-checkbox relative flex items-center space-x-2",
17
- input: [
18
- "peer border-input bg-background starwind-transition-colors shrink-0 transform-gpu rounded-sm border",
19
- "focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:transition-none",
20
- "outline-0 focus:ring-0 focus:ring-offset-0",
21
- "not-disabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
22
- ],
23
- icon: [
24
- "pointer-events-none absolute stroke-3 p-0.5 opacity-0 transition-opacity peer-checked:opacity-100",
25
- "starwind-check-icon",
26
- ],
27
- label:
28
- "font-medium peer-not-disabled:cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
29
- },
30
- variants: {
31
- size: {
32
- sm: { input: "size-4", icon: "size-4", label: "text-sm" },
33
- md: { input: "size-5", icon: "size-5", label: "text-base" },
34
- lg: { input: "size-6", icon: "size-6", label: "text-lg" },
35
- },
36
- variant: {
37
- default: {
38
- input: "checked:bg-foreground focus-visible:outline-outline",
39
- icon: "text-background",
40
- },
41
- primary: {
42
- input: "checked:bg-primary focus-visible:outline-primary",
43
- icon: "text-primary-foreground",
44
- },
45
- secondary: {
46
- input: "checked:bg-secondary focus-visible:outline-secondary",
47
- icon: "text-secondary-foreground",
48
- },
49
- info: { input: "checked:bg-info focus-visible:outline-info", icon: "text-info-foreground" },
50
- success: {
51
- input: "checked:bg-success focus-visible:outline-success",
52
- icon: "text-success-foreground",
53
- },
54
- warning: {
55
- input: "checked:bg-warning focus-visible:outline-warning",
56
- icon: "text-warning-foreground",
57
- },
58
- error: {
59
- input: "checked:bg-error focus-visible:outline-error",
60
- icon: "text-error-foreground",
61
- },
62
- },
63
- },
64
- defaultVariants: { size: "md", variant: "default" },
15
+ slots: {
16
+ base: "starwind-checkbox relative flex items-center space-x-2",
17
+ input: [
18
+ "peer border-input bg-background starwind-transition-colors shrink-0 transform-gpu rounded-sm border",
19
+ "focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:transition-none",
20
+ "outline-0 focus:ring-0 focus:ring-offset-0",
21
+ "not-disabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
22
+ ],
23
+ icon: [
24
+ "pointer-events-none absolute stroke-3 p-0.5 opacity-0 transition-opacity peer-checked:opacity-100",
25
+ "starwind-check-icon",
26
+ ],
27
+ label:
28
+ "font-medium peer-not-disabled:cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
29
+ },
30
+ variants: {
31
+ size: {
32
+ sm: { input: "size-4", icon: "size-4", label: "text-sm" },
33
+ md: { input: "size-5", icon: "size-5", label: "text-base" },
34
+ lg: { input: "size-6", icon: "size-6", label: "text-lg" },
35
+ },
36
+ variant: {
37
+ default: {
38
+ input: "checked:bg-foreground focus-visible:outline-outline",
39
+ icon: "text-background",
40
+ },
41
+ primary: {
42
+ input: "checked:bg-primary focus-visible:outline-primary",
43
+ icon: "text-primary-foreground",
44
+ },
45
+ secondary: {
46
+ input: "checked:bg-secondary focus-visible:outline-secondary",
47
+ icon: "text-secondary-foreground",
48
+ },
49
+ info: { input: "checked:bg-info focus-visible:outline-info", icon: "text-info-foreground" },
50
+ success: {
51
+ input: "checked:bg-success focus-visible:outline-success",
52
+ icon: "text-success-foreground",
53
+ },
54
+ warning: {
55
+ input: "checked:bg-warning focus-visible:outline-warning",
56
+ icon: "text-warning-foreground",
57
+ },
58
+ error: {
59
+ input: "checked:bg-error focus-visible:outline-error",
60
+ icon: "text-error-foreground",
61
+ },
62
+ },
63
+ },
64
+ defaultVariants: { size: "md", variant: "default" },
65
65
  });
66
66
 
67
67
  const { id, label, checked, size, variant, class: className, ...rest } = Astro.props;
@@ -70,42 +70,42 @@ const { base, input, icon, label: labelClass } = checkbox({ size, variant });
70
70
  ---
71
71
 
72
72
  <div class={base()}>
73
- <input type="checkbox" id={id} class={input({ class: className })} {checked} {...rest} />
74
- <Check class={icon()} />
75
- {
76
- label && (
77
- <label for={id} class={labelClass()}>
78
- {label}
79
- </label>
80
- )
81
- }
73
+ <input type="checkbox" id={id} class={input({ class: className })} {checked} {...rest} />
74
+ <Check class={icon()} />
75
+ {
76
+ label && (
77
+ <label for={id} class={labelClass()}>
78
+ {label}
79
+ </label>
80
+ )
81
+ }
82
82
  </div>
83
83
 
84
84
  <style>
85
- .starwind-checkbox input[type="checkbox"]:checked {
86
- background-image: none;
87
- }
85
+ .starwind-checkbox input[type="checkbox"]:checked {
86
+ background-image: none;
87
+ }
88
88
 
89
- /* Check drawing animation */
90
- .starwind-check-icon {
91
- stroke-dasharray: 65;
92
- stroke-dashoffset: 65;
93
- opacity: 0;
94
- }
89
+ /* Check drawing animation */
90
+ .starwind-check-icon {
91
+ stroke-dasharray: 65;
92
+ stroke-dashoffset: 65;
93
+ opacity: 0;
94
+ }
95
95
 
96
- .starwind-checkbox input[type="checkbox"]:checked + .starwind-check-icon {
97
- animation: draw-check 0.3s ease forwards;
98
- animation-delay: 0.15s;
99
- }
96
+ .starwind-checkbox input[type="checkbox"]:checked + .starwind-check-icon {
97
+ animation: draw-check 0.3s ease forwards;
98
+ animation-delay: 0.15s;
99
+ }
100
100
 
101
- @keyframes draw-check {
102
- 0% {
103
- stroke-dashoffset: 65;
104
- opacity: 1;
105
- }
106
- 100% {
107
- stroke-dashoffset: 0;
108
- opacity: 1;
109
- }
110
- }
101
+ @keyframes draw-check {
102
+ 0% {
103
+ stroke-dashoffset: 65;
104
+ opacity: 1;
105
+ }
106
+ 100% {
107
+ stroke-dashoffset: 0;
108
+ opacity: 1;
109
+ }
110
+ }
111
111
  </style>
@@ -7,185 +7,244 @@ const { class: className, ...rest } = Astro.props;
7
7
  ---
8
8
 
9
9
  <div class:list={["starwind-dialog", className]} {...rest}>
10
- <slot />
10
+ <slot />
11
11
  </div>
12
12
 
13
13
  <script>
14
- class DialogHandler {
15
- private trigger: HTMLButtonElement;
16
- private dialog: HTMLDialogElement;
17
- private closeButtons: NodeListOf<HTMLButtonElement>;
18
- private backdrop: HTMLElement;
19
- /**
20
- * The duration of the animation in milliseconds. This is used to calculate the
21
- * duration of close animation before hiding the dialog and backdrop
22
- */
23
- private animationDuration: number;
24
-
25
- constructor(dialogWrapper: HTMLElement, dialogNumber: number) {
26
- this.dialog = dialogWrapper.querySelector("dialog")!;
27
- this.backdrop = dialogWrapper.querySelector(".starwind-dialog-backdrop")!;
28
-
29
- // animationDuration is set with inline styles through passed prop to DialogContent
30
- const animationDurationString = this.dialog.style.animationDuration;
31
- if (animationDurationString.endsWith("ms")) {
32
- this.animationDuration = parseFloat(animationDurationString);
33
- } else if (animationDurationString.endsWith("s")) {
34
- // using something like @playform/compress might optimize to use "s" instead of "ms"
35
- this.animationDuration = parseFloat(animationDurationString) * 1000;
36
- } else {
37
- this.animationDuration = 200;
38
- }
39
-
40
- // if trigger is set with asChild, use the first child element for trigger button
41
- const tempTrigger = dialogWrapper.querySelector(".starwind-dialog-trigger") as HTMLElement;
42
- if (tempTrigger?.hasAttribute("data-as-child")) {
43
- this.trigger = tempTrigger.firstElementChild as HTMLButtonElement;
44
- } else {
45
- this.trigger = tempTrigger as HTMLButtonElement;
46
- }
47
-
48
- // if closeButtons are set with asChild, swap the wrapper with its first child
49
- const tempCloseButtons = dialogWrapper.querySelectorAll(
50
- ".starwind-dialog-close",
51
- ) as NodeListOf<HTMLElement>;
52
- tempCloseButtons.forEach((button: HTMLElement) => {
53
- if (button.hasAttribute("data-as-child")) {
54
- const childElement = button.firstElementChild;
55
- if (childElement) {
56
- childElement.classList.add("starwind-dialog-close");
57
- button.parentNode?.replaceChild(childElement, button);
58
- }
59
- }
60
- return button;
61
- });
62
-
63
- this.closeButtons = dialogWrapper.querySelectorAll(
64
- ".starwind-dialog-close",
65
- ) as NodeListOf<HTMLButtonElement>;
66
-
67
- // if any elements are not there, exit
68
- if (!this.trigger || !this.dialog || !this.backdrop) return;
69
-
70
- this.setupAccessibility(dialogNumber);
71
- this.setupEvents();
72
- }
73
-
74
- private setupAccessibility(dialogNumber: number): void {
75
- // get the first heading element in the dialog
76
- const firstHeading = this.dialog.querySelector("h1, h2, h3, h4, h5, h6");
77
- if (firstHeading) {
78
- // create a unique ID for the heading
79
- firstHeading.id = `starwind-dialog${dialogNumber}-heading`;
80
- // set the aria-labelledby attribute to the first heading element
81
- this.dialog.setAttribute("aria-labelledby", firstHeading.id);
82
- }
83
- }
84
-
85
- private setupEvents(): void {
86
- this.trigger?.addEventListener("click", () => {
87
- this.open();
88
- });
89
-
90
- // Add click handlers to all close buttons
91
- this.closeButtons?.forEach((button) => {
92
- button.addEventListener("click", () => {
93
- // Only close if this is the topmost dialog
94
- const openDialogs = document.querySelectorAll("dialog[open]");
95
- if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
96
- this.close();
97
- }
98
- });
99
- });
100
-
101
- // Close on click outside
102
- this.dialog.addEventListener("click", (e) => {
103
- const dialogDimensions = this.dialog.getBoundingClientRect();
104
- const clickedInDialog =
105
- e.clientX >= dialogDimensions.left &&
106
- e.clientX <= dialogDimensions.right &&
107
- e.clientY >= dialogDimensions.top &&
108
- e.clientY <= dialogDimensions.bottom;
109
-
110
- if (!clickedInDialog) {
111
- // Only close if this is the topmost dialog
112
- const openDialogs = document.querySelectorAll("dialog[open]");
113
- if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
114
- this.close();
115
- }
116
- }
117
- });
118
-
119
- // Handle escape key
120
- this.dialog.addEventListener("keydown", (e) => {
121
- if (e.key === "Escape") {
122
- // prevent default dialog closing behavior so we can add closing animation
123
- e.preventDefault();
124
- // Only close if this is the topmost dialog
125
- const openDialogs = document.querySelectorAll("dialog[open]");
126
- if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
127
- this.close();
128
- }
129
- }
130
- });
131
-
132
- // Intercept form submissions to handle dialog close
133
- const forms = this.dialog.querySelectorAll("form");
134
- forms.forEach((form) => {
135
- form.addEventListener("submit", (e) => {
136
- /**
137
- * Default form.method = "dialog" submissions cause the dialog to close
138
- * Default form.method = "post" submissions do not close the dialog
139
- * Here we intercept the form submission and manage the dialog closing if method = "dialog"
140
- * so we can add closing animation
141
- * Normal form event listeners for "submit" will still get the form data
142
- */
143
- if (form.method === "dialog") {
144
- e.preventDefault();
145
- // Only close if this is the topmost dialog
146
- const openDialogs = document.querySelectorAll("dialog[open]");
147
- if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
148
- this.close();
149
- }
150
- }
151
- });
152
- });
153
- }
154
-
155
- private open(): void {
156
- this.dialog.showModal();
157
- document.body.classList.add("overflow-hidden");
158
- this.backdrop.classList.remove("hidden");
159
- this.backdrop.dataset.state = "open";
160
- this.dialog.dataset.state = "open";
161
- }
162
-
163
- private close(): void {
164
- document.body.classList.remove("overflow-hidden");
165
- this.dialog.dataset.state = "closed";
166
- this.backdrop.dataset.state = "closed";
167
-
168
- // Wait for animation to finish before hiding backdrop
169
- setTimeout(() => {
170
- this.backdrop.classList.add("hidden");
171
- this.dialog.close();
172
- }, this.animationDuration);
173
- }
174
- }
175
-
176
- // Store instances in a WeakMap to avoid memory leaks
177
- const dialogInstances = new WeakMap<HTMLElement, DialogHandler>();
178
-
179
- // Initialize all dialogs
180
- const setupDialogs = () => {
181
- document.querySelectorAll(".starwind-dialog").forEach((dialogWrapper, idx) => {
182
- const wrapper = dialogWrapper as HTMLElement;
183
- if (!dialogInstances.has(wrapper)) {
184
- dialogInstances.set(wrapper, new DialogHandler(wrapper, idx));
185
- }
186
- });
187
- };
188
-
189
- setupDialogs();
190
- document.addEventListener("astro:after-swap", setupDialogs);
14
+ class DialogHandler {
15
+ private triggers: HTMLButtonElement[] = [];
16
+ private dialog: HTMLDialogElement;
17
+ private closeButtons: HTMLButtonElement[] = [];
18
+ private backdrop: HTMLElement;
19
+ private dialogId: string;
20
+ /**
21
+ * The duration of the animation in milliseconds. This is used to calculate the
22
+ * duration of close animation before hiding the dialog and backdrop
23
+ */
24
+ private animationDuration: number;
25
+
26
+ constructor(dialogWrapper: HTMLElement, dialogNumber: number) {
27
+ this.dialog = dialogWrapper.querySelector("dialog")!;
28
+ this.backdrop = dialogWrapper.querySelector(".starwind-dialog-backdrop")!;
29
+
30
+ // if no ID was provided for the wrapper, generate one
31
+ if (dialogWrapper.id) {
32
+ this.dialogId = dialogWrapper.id;
33
+ } else {
34
+ this.dialogId = `starwind-dialog${dialogNumber}`;
35
+ dialogWrapper.id = this.dialogId;
36
+ }
37
+
38
+ // animationDuration is set with inline styles through passed prop to DialogContent
39
+ const animationDurationString = this.dialog.style.animationDuration;
40
+ if (animationDurationString.endsWith("ms")) {
41
+ this.animationDuration = parseFloat(animationDurationString);
42
+ } else if (animationDurationString.endsWith("s")) {
43
+ // using something like @playform/compress might optimize to use "s" instead of "ms"
44
+ this.animationDuration = parseFloat(animationDurationString) * 1000;
45
+ } else {
46
+ this.animationDuration = 200;
47
+ }
48
+
49
+ // Find internal triggers and handle them
50
+ const internalTriggers = dialogWrapper.querySelectorAll(".starwind-dialog-trigger");
51
+ internalTriggers.forEach((triggerElement) => {
52
+ const tempTrigger = triggerElement as HTMLElement;
53
+ let trigger: HTMLButtonElement;
54
+
55
+ if (tempTrigger?.hasAttribute("data-as-child")) {
56
+ trigger = tempTrigger.firstElementChild as HTMLButtonElement;
57
+ } else {
58
+ trigger = tempTrigger as HTMLButtonElement;
59
+ }
60
+
61
+ if (trigger) {
62
+ this.triggers.push(trigger);
63
+ }
64
+ });
65
+
66
+ // Find external triggers that target this dialog
67
+ this.findExternalTriggers();
68
+
69
+ // if closeButtons are set with asChild, swap the wrapper with its first child
70
+ const tempCloseButtons = dialogWrapper.querySelectorAll(
71
+ ".starwind-dialog-close",
72
+ ) as NodeListOf<HTMLElement>;
73
+ tempCloseButtons.forEach((button: HTMLElement) => {
74
+ if (button.hasAttribute("data-as-child")) {
75
+ const childElement = button.firstElementChild;
76
+ if (childElement) {
77
+ childElement.classList.add("starwind-dialog-close");
78
+ button.parentNode?.replaceChild(childElement, button);
79
+ }
80
+ }
81
+ return button;
82
+ });
83
+
84
+ // Convert NodeList to Array for consistency with triggers
85
+ this.closeButtons = Array.from(
86
+ dialogWrapper.querySelectorAll(".starwind-dialog-close"),
87
+ ) as HTMLButtonElement[];
88
+
89
+ // if essential elements are not there, exit
90
+ if (!this.dialog || !this.backdrop) return;
91
+
92
+ this.setupAccessibility(dialogNumber);
93
+ this.setupEvents();
94
+ }
95
+
96
+ private setupAccessibility(dialogNumber: number): void {
97
+ // get the first heading element in the dialog
98
+ const firstHeading = this.dialog.querySelector("h1, h2, h3, h4, h5, h6");
99
+ if (firstHeading) {
100
+ // create a unique ID for the heading
101
+ firstHeading.id = `starwind-dialog${dialogNumber}-heading`;
102
+ // set the aria-labelledby attribute to the first heading element
103
+ this.dialog.setAttribute("aria-labelledby", firstHeading.id);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Find all external triggers that target this dialog
109
+ */
110
+ private findExternalTriggers(): void {
111
+ const externalTriggers = document.querySelectorAll(
112
+ `.starwind-dialog-trigger[data-dialog-for="${this.dialogId}"]`,
113
+ );
114
+
115
+ externalTriggers.forEach((triggerElement) => {
116
+ // Skip if this is an internal trigger we already processed
117
+ const dialogWrapper = triggerElement.closest(".starwind-dialog");
118
+ if (dialogWrapper && dialogWrapper.id === this.dialogId) {
119
+ return;
120
+ }
121
+
122
+ let trigger: HTMLButtonElement;
123
+ if (triggerElement.hasAttribute("data-as-child")) {
124
+ trigger = triggerElement.firstElementChild as HTMLButtonElement;
125
+ } else {
126
+ trigger = triggerElement as HTMLButtonElement;
127
+ }
128
+
129
+ if (trigger && !this.triggers.includes(trigger)) {
130
+ this.triggers.push(trigger);
131
+ }
132
+ });
133
+ }
134
+
135
+ private setupEvents(): void {
136
+ // Add click listeners to all triggers
137
+ this.triggers.forEach((trigger) => {
138
+ trigger.addEventListener("click", () => {
139
+ this.open();
140
+ });
141
+ });
142
+
143
+ // Add click handlers to all close buttons
144
+ this.closeButtons?.forEach((button) => {
145
+ button.addEventListener("click", () => {
146
+ // Only close if this is the topmost dialog
147
+ const openDialogs = document.querySelectorAll("dialog[open]");
148
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
149
+ this.close();
150
+ }
151
+ });
152
+ });
153
+
154
+ // Close on click outside
155
+ this.dialog.addEventListener("click", (e) => {
156
+ const dialogDimensions = this.dialog.getBoundingClientRect();
157
+ const clickedInDialog =
158
+ e.clientX >= dialogDimensions.left &&
159
+ e.clientX <= dialogDimensions.right &&
160
+ e.clientY >= dialogDimensions.top &&
161
+ e.clientY <= dialogDimensions.bottom;
162
+
163
+ if (!clickedInDialog) {
164
+ // Only close if this is the topmost dialog
165
+ const openDialogs = document.querySelectorAll("dialog[open]");
166
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
167
+ this.close();
168
+ }
169
+ }
170
+ });
171
+
172
+ // Handle escape key
173
+ this.dialog.addEventListener("keydown", (e) => {
174
+ if (e.key === "Escape") {
175
+ // prevent default dialog closing behavior so we can add closing animation
176
+ e.preventDefault();
177
+ // Only close if this is the topmost dialog
178
+ const openDialogs = document.querySelectorAll("dialog[open]");
179
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
180
+ this.close();
181
+ }
182
+ }
183
+ });
184
+
185
+ // Intercept form submissions to handle dialog close
186
+ const forms = this.dialog.querySelectorAll("form");
187
+ forms.forEach((form) => {
188
+ form.addEventListener("submit", (e) => {
189
+ /**
190
+ * Default form.method = "dialog" submissions cause the dialog to close
191
+ * Default form.method = "post" submissions do not close the dialog
192
+ * Here we intercept the form submission and manage the dialog closing if method = "dialog"
193
+ * so we can add closing animation
194
+ * Normal form event listeners for "submit" will still get the form data
195
+ */
196
+ if (form.method === "dialog") {
197
+ e.preventDefault();
198
+ // Only close if this is the topmost dialog
199
+ const openDialogs = document.querySelectorAll("dialog[open]");
200
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
201
+ this.close();
202
+ }
203
+ }
204
+ });
205
+ });
206
+ }
207
+
208
+ private open(): void {
209
+ this.dialog.showModal();
210
+ document.body.classList.add("overflow-hidden");
211
+ this.backdrop.classList.remove("hidden");
212
+ this.backdrop.dataset.state = "open";
213
+ this.dialog.dataset.state = "open";
214
+ }
215
+
216
+ private close(): void {
217
+ document.body.classList.remove("overflow-hidden");
218
+ this.dialog.dataset.state = "closed";
219
+ this.backdrop.dataset.state = "closed";
220
+
221
+ // Wait for animation to finish before hiding backdrop
222
+ setTimeout(() => {
223
+ this.backdrop.classList.add("hidden");
224
+ this.dialog.close();
225
+ }, this.animationDuration);
226
+ }
227
+ }
228
+
229
+ // Store instances in a WeakMap to avoid memory leaks
230
+ const dialogInstances = new WeakMap<HTMLElement, DialogHandler>();
231
+
232
+ // Initialize all dialogs
233
+ const setupDialogs = () => {
234
+ document.querySelectorAll(".starwind-dialog").forEach((dialogWrapper, idx) => {
235
+ const wrapper = dialogWrapper as HTMLElement;
236
+ if (!dialogInstances.has(wrapper)) {
237
+ dialogInstances.set(wrapper, new DialogHandler(wrapper, idx));
238
+ }
239
+ });
240
+ };
241
+
242
+ setupDialogs();
243
+ document.addEventListener("astro:after-swap", setupDialogs);
191
244
  </script>
245
+
246
+ <style>
247
+ .overflow-hidden {
248
+ overflow: hidden;
249
+ }
250
+ </style>