@starwind-ui/core 1.15.1 → 1.15.2
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.
- package/dist/index.d.ts +28 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/src/components/accordion/Accordion.astro +254 -0
- package/dist/src/components/accordion/AccordionContent.astro +33 -0
- package/dist/src/components/accordion/AccordionItem.astro +27 -0
- package/dist/src/components/accordion/AccordionTrigger.astro +32 -0
- package/dist/src/components/accordion/index.ts +15 -0
- package/dist/src/components/alert/Alert.astro +31 -0
- package/dist/src/components/alert/AlertDescription.astro +14 -0
- package/dist/src/components/alert/AlertTitle.astro +16 -0
- package/dist/src/components/alert/index.ts +13 -0
- package/dist/src/components/alert-dialog/AlertDialog.astro +275 -0
- package/dist/src/components/alert-dialog/AlertDialogAction.astro +44 -0
- package/dist/src/components/alert-dialog/AlertDialogCancel.astro +45 -0
- package/dist/src/components/alert-dialog/AlertDialogContent.astro +52 -0
- package/dist/src/components/alert-dialog/AlertDialogDescription.astro +18 -0
- package/dist/src/components/alert-dialog/AlertDialogFooter.astro +16 -0
- package/dist/src/components/alert-dialog/AlertDialogHeader.astro +14 -0
- package/dist/src/components/alert-dialog/AlertDialogTitle.astro +20 -0
- package/dist/src/components/alert-dialog/AlertDialogTrigger.astro +47 -0
- package/dist/src/components/alert-dialog/index.ts +46 -0
- package/dist/src/components/aspect-ratio/AspectRatio.astro +32 -0
- package/dist/src/components/aspect-ratio/index.ts +7 -0
- package/dist/src/components/avatar/Avatar.astro +29 -0
- package/dist/src/components/avatar/AvatarFallback.astro +18 -0
- package/dist/src/components/avatar/AvatarImage.astro +49 -0
- package/dist/src/components/avatar/index.ts +13 -0
- package/dist/src/components/badge/Badge.astro +55 -0
- package/dist/src/components/badge/index.ts +7 -0
- package/dist/src/components/breadcrumb/Breadcrumb.astro +11 -0
- package/dist/src/components/breadcrumb/BreadcrumbEllipsis.astro +28 -0
- package/dist/src/components/breadcrumb/BreadcrumbItem.astro +14 -0
- package/dist/src/components/breadcrumb/BreadcrumbLink.astro +22 -0
- package/dist/src/components/breadcrumb/BreadcrumbList.astro +16 -0
- package/dist/src/components/breadcrumb/BreadcrumbPage.astro +21 -0
- package/dist/src/components/breadcrumb/BreadcrumbSeparator.astro +23 -0
- package/dist/src/components/breadcrumb/index.ts +37 -0
- package/dist/src/components/button/Button.astro +55 -0
- package/dist/src/components/button/index.ts +7 -0
- package/dist/src/components/button-group/ButtonGroup.astro +62 -0
- package/dist/src/components/button-group/ButtonGroupSeparator.astro +27 -0
- package/dist/src/components/button-group/ButtonGroupText.astro +19 -0
- package/dist/src/components/button-group/index.ts +17 -0
- package/dist/src/components/card/Card.astro +14 -0
- package/dist/src/components/card/CardContent.astro +14 -0
- package/dist/src/components/card/CardDescription.astro +14 -0
- package/dist/src/components/card/CardFooter.astro +14 -0
- package/dist/src/components/card/CardHeader.astro +14 -0
- package/dist/src/components/card/CardTitle.astro +14 -0
- package/dist/src/components/card/index.ts +26 -0
- package/dist/src/components/carousel/Carousel.astro +55 -0
- package/dist/src/components/carousel/CarouselContent.astro +26 -0
- package/dist/src/components/carousel/CarouselItem.astro +26 -0
- package/dist/src/components/carousel/CarouselNext.astro +37 -0
- package/dist/src/components/carousel/CarouselPrevious.astro +37 -0
- package/dist/src/components/carousel/carousel-script.ts +191 -0
- package/dist/src/components/carousel/index.ts +32 -0
- package/dist/src/components/checkbox/Checkbox.astro +128 -0
- package/dist/src/components/checkbox/index.ts +7 -0
- package/dist/src/components/collapsible/Collapsible.astro +161 -0
- package/dist/src/components/collapsible/CollapsibleContent.astro +22 -0
- package/dist/src/components/collapsible/CollapsibleTrigger.astro +44 -0
- package/dist/src/components/collapsible/index.ts +13 -0
- package/dist/src/components/dialog/Dialog.astro +389 -0
- package/dist/src/components/dialog/DialogClose.astro +35 -0
- package/dist/src/components/dialog/DialogContent.astro +78 -0
- package/dist/src/components/dialog/DialogDescription.astro +14 -0
- package/dist/src/components/dialog/DialogFooter.astro +14 -0
- package/dist/src/components/dialog/DialogHeader.astro +14 -0
- package/dist/src/components/dialog/DialogTitle.astro +22 -0
- package/dist/src/components/dialog/DialogTrigger.astro +47 -0
- package/dist/src/components/dialog/index.ts +45 -0
- package/dist/src/components/dropdown/Dropdown.astro +377 -0
- package/dist/src/components/dropdown/DropdownContent.astro +81 -0
- package/dist/src/components/dropdown/DropdownItem.astro +48 -0
- package/dist/src/components/dropdown/DropdownLabel.astro +29 -0
- package/dist/src/components/dropdown/DropdownSeparator.astro +21 -0
- package/dist/src/components/dropdown/DropdownTrigger.astro +52 -0
- package/dist/src/components/dropdown/index.ts +33 -0
- package/dist/src/components/dropzone/Dropzone.astro +236 -0
- package/dist/src/components/dropzone/DropzoneFilesList.astro +26 -0
- package/dist/src/components/dropzone/DropzoneLoadingIndicator.astro +10 -0
- package/dist/src/components/dropzone/DropzoneUploadIndicator.astro +10 -0
- package/dist/src/components/dropzone/index.ts +24 -0
- package/dist/src/components/image/Image.astro +24 -0
- package/dist/src/components/image/index.ts +9 -0
- package/dist/src/components/input/Input.astro +25 -0
- package/dist/src/components/input/index.ts +7 -0
- package/dist/src/components/input-otp/InputOtp.astro +352 -0
- package/dist/src/components/input-otp/InputOtpGroup.astro +16 -0
- package/dist/src/components/input-otp/InputOtpSeparator.astro +25 -0
- package/dist/src/components/input-otp/InputOtpSlot.astro +48 -0
- package/dist/src/components/input-otp/InputOtpTypes.ts +6 -0
- package/dist/src/components/input-otp/index.ts +33 -0
- package/dist/src/components/item/Item.astro +52 -0
- package/dist/src/components/item/ItemActions.astro +16 -0
- package/dist/src/components/item/ItemContent.astro +16 -0
- package/dist/src/components/item/ItemDescription.astro +19 -0
- package/dist/src/components/item/ItemFooter.astro +16 -0
- package/dist/src/components/item/ItemGroup.astro +16 -0
- package/dist/src/components/item/ItemHeader.astro +16 -0
- package/dist/src/components/item/ItemMedia.astro +40 -0
- package/dist/src/components/item/ItemSeparator.astro +21 -0
- package/dist/src/components/item/ItemTitle.astro +16 -0
- package/dist/src/components/item/index.ts +50 -0
- package/dist/src/components/kbd/Kbd.astro +21 -0
- package/dist/src/components/kbd/KbdGroup.astro +16 -0
- package/dist/src/components/kbd/index.ts +11 -0
- package/dist/src/components/label/Label.astro +22 -0
- package/dist/src/components/label/index.ts +7 -0
- package/dist/src/components/pagination/Pagination.astro +20 -0
- package/dist/src/components/pagination/PaginationContent.astro +16 -0
- package/dist/src/components/pagination/PaginationEllipsis.astro +35 -0
- package/dist/src/components/pagination/PaginationItem.astro +16 -0
- package/dist/src/components/pagination/PaginationLink.astro +24 -0
- package/dist/src/components/pagination/PaginationNext.astro +30 -0
- package/dist/src/components/pagination/PaginationPrevious.astro +30 -0
- package/dist/src/components/pagination/index.ts +38 -0
- package/dist/src/components/progress/Progress.astro +155 -0
- package/dist/src/components/progress/index.ts +10 -0
- package/dist/src/components/prose/Prose.astro +617 -0
- package/dist/src/components/prose/index.ts +9 -0
- package/dist/src/components/radio-group/RadioGroup.astro +162 -0
- package/dist/src/components/radio-group/RadioGroupItem.astro +129 -0
- package/dist/src/components/radio-group/RadioGroupTypes.ts +6 -0
- package/dist/src/components/radio-group/index.ts +23 -0
- package/dist/src/components/select/Select.astro +752 -0
- package/dist/src/components/select/SelectContent.astro +94 -0
- package/dist/src/components/select/SelectGroup.astro +9 -0
- package/dist/src/components/select/SelectItem.astro +51 -0
- package/dist/src/components/select/SelectLabel.astro +14 -0
- package/dist/src/components/select/SelectSearch.astro +49 -0
- package/dist/src/components/select/SelectSeparator.astro +12 -0
- package/dist/src/components/select/SelectTrigger.astro +54 -0
- package/dist/src/components/select/SelectTypes.ts +13 -0
- package/dist/src/components/select/SelectValue.astro +19 -0
- package/dist/src/components/select/index.ts +49 -0
- package/dist/src/components/separator/Separator.astro +36 -0
- package/dist/src/components/separator/index.ts +7 -0
- package/dist/src/components/sheet/Sheet.astro +13 -0
- package/dist/src/components/sheet/SheetClose.astro +13 -0
- package/dist/src/components/sheet/SheetContent.astro +92 -0
- package/dist/src/components/sheet/SheetDescription.astro +16 -0
- package/dist/src/components/sheet/SheetFooter.astro +16 -0
- package/dist/src/components/sheet/SheetHeader.astro +16 -0
- package/dist/src/components/sheet/SheetTitle.astro +16 -0
- package/dist/src/components/sheet/SheetTrigger.astro +13 -0
- package/dist/src/components/sheet/index.ts +41 -0
- package/dist/src/components/sidebar/Sidebar.astro +213 -0
- package/dist/src/components/sidebar/SidebarContent.astro +24 -0
- package/dist/src/components/sidebar/SidebarFooter.astro +21 -0
- package/dist/src/components/sidebar/SidebarGroup.astro +21 -0
- package/dist/src/components/sidebar/SidebarGroupContent.astro +21 -0
- package/dist/src/components/sidebar/SidebarGroupLabel.astro +52 -0
- package/dist/src/components/sidebar/SidebarHeader.astro +21 -0
- package/dist/src/components/sidebar/SidebarInput.astro +22 -0
- package/dist/src/components/sidebar/SidebarInset.astro +21 -0
- package/dist/src/components/sidebar/SidebarMenu.astro +21 -0
- package/dist/src/components/sidebar/SidebarMenuAction.astro +59 -0
- package/dist/src/components/sidebar/SidebarMenuBadge.astro +30 -0
- package/dist/src/components/sidebar/SidebarMenuButton.astro +129 -0
- package/dist/src/components/sidebar/SidebarMenuItem.astro +21 -0
- package/dist/src/components/sidebar/SidebarMenuSkeleton.astro +40 -0
- package/dist/src/components/sidebar/SidebarMenuSub.astro +24 -0
- package/dist/src/components/sidebar/SidebarMenuSubButton.astro +49 -0
- package/dist/src/components/sidebar/SidebarMenuSubItem.astro +16 -0
- package/dist/src/components/sidebar/SidebarProvider.astro +213 -0
- package/dist/src/components/sidebar/SidebarRail.astro +71 -0
- package/dist/src/components/sidebar/SidebarSeparator.astro +22 -0
- package/dist/src/components/sidebar/SidebarTrigger.astro +66 -0
- package/dist/src/components/sidebar/index.ts +103 -0
- package/dist/src/components/skeleton/Skeleton.astro +14 -0
- package/dist/src/components/skeleton/index.ts +9 -0
- package/dist/src/components/slider/Slider.astro +411 -0
- package/dist/src/components/slider/index.ts +9 -0
- package/dist/src/components/spinner/Spinner.astro +21 -0
- package/dist/src/components/spinner/index.ts +7 -0
- package/dist/src/components/switch/Switch.astro +192 -0
- package/dist/src/components/switch/SwitchTypes.ts +6 -0
- package/dist/src/components/switch/index.ts +12 -0
- package/dist/src/components/table/Table.astro +18 -0
- package/dist/src/components/table/TableBody.astro +16 -0
- package/dist/src/components/table/TableCaption.astro +16 -0
- package/dist/src/components/table/TableCell.astro +16 -0
- package/dist/src/components/table/TableFoot.astro +16 -0
- package/dist/src/components/table/TableHead.astro +16 -0
- package/dist/src/components/table/TableHeader.astro +16 -0
- package/dist/src/components/table/TableRow.astro +16 -0
- package/dist/src/components/table/index.ts +42 -0
- package/dist/src/components/tabs/Tabs.astro +271 -0
- package/dist/src/components/tabs/TabsContent.astro +28 -0
- package/dist/src/components/tabs/TabsList.astro +22 -0
- package/dist/src/components/tabs/TabsTrigger.astro +34 -0
- package/dist/src/components/tabs/index.ts +20 -0
- package/dist/src/components/textarea/Textarea.astro +29 -0
- package/dist/src/components/textarea/index.ts +9 -0
- package/dist/src/components/theme-toggle/ThemeToggle.astro +208 -0
- package/dist/src/components/theme-toggle/index.ts +7 -0
- package/dist/src/components/toast/ToastDescription.astro +21 -0
- package/dist/src/components/toast/ToastItem.astro +54 -0
- package/dist/src/components/toast/ToastTemplate.astro +25 -0
- package/dist/src/components/toast/ToastTitle.astro +57 -0
- package/dist/src/components/toast/Toaster.astro +982 -0
- package/dist/src/components/toast/index.ts +29 -0
- package/dist/src/components/toast/toast-manager.ts +216 -0
- package/dist/src/components/toggle/Toggle.astro +174 -0
- package/dist/src/components/toggle/ToggleTypes.ts +14 -0
- package/dist/src/components/toggle/index.ts +8 -0
- package/dist/src/components/tooltip/Tooltip.astro +282 -0
- package/dist/src/components/tooltip/TooltipContent.astro +89 -0
- package/dist/src/components/tooltip/TooltipTrigger.astro +10 -0
- package/dist/src/components/tooltip/index.ts +16 -0
- package/dist/src/components/video/Video.astro +120 -0
- package/dist/src/components/video/index.ts +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
type Props = HTMLAttributes<"div"> & {
|
|
6
|
+
/**
|
|
7
|
+
* Whether the collapsible panel is initially open
|
|
8
|
+
*/
|
|
9
|
+
defaultOpen?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the component should ignore user interaction
|
|
12
|
+
*/
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const collapsible = tv({ base: "starwind-collapsible" });
|
|
17
|
+
|
|
18
|
+
const { defaultOpen = false, disabled = false, class: className, ...rest } = Astro.props;
|
|
19
|
+
|
|
20
|
+
// Keys owned by the runtime that should not be spread from rest
|
|
21
|
+
const runtimeOwnedKeys = ["id", "data-state", "data-disabled", "class"] as const;
|
|
22
|
+
const sanitizedRest = Object.fromEntries(
|
|
23
|
+
Object.entries(rest).filter(
|
|
24
|
+
([key]) => !runtimeOwnedKeys.includes(key as (typeof runtimeOwnedKeys)[number]),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Compute final values, preferring consumer-provided values
|
|
29
|
+
const finalDataState =
|
|
30
|
+
(rest as Record<string, unknown>)["data-state"] ?? (defaultOpen ? "open" : "closed");
|
|
31
|
+
const finalDataDisabled =
|
|
32
|
+
(rest as Record<string, unknown>)["data-disabled"] ?? (disabled ? "" : undefined);
|
|
33
|
+
const finalId = (rest as Record<string, unknown>)["id"] as string | undefined;
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
{...sanitizedRest}
|
|
38
|
+
id={finalId}
|
|
39
|
+
class={collapsible({ class: className })}
|
|
40
|
+
data-state={finalDataState}
|
|
41
|
+
data-disabled={finalDataDisabled}
|
|
42
|
+
data-slot="collapsible"
|
|
43
|
+
>
|
|
44
|
+
<slot />
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
type CollapsibleState = "open" | "closed";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handles the functionality of a collapsible component.
|
|
52
|
+
* Manages open/close state and accessibility attributes.
|
|
53
|
+
*/
|
|
54
|
+
class CollapsibleHandler {
|
|
55
|
+
private root: HTMLElement;
|
|
56
|
+
private trigger: HTMLElement | null;
|
|
57
|
+
private content: HTMLElement | null;
|
|
58
|
+
private collapsibleId: string;
|
|
59
|
+
|
|
60
|
+
constructor(root: HTMLElement, idx: number) {
|
|
61
|
+
this.root = root;
|
|
62
|
+
this.collapsibleId = `starwind-collapsible-${idx}`;
|
|
63
|
+
|
|
64
|
+
this.trigger = root.querySelector<HTMLElement>(".starwind-collapsible-trigger");
|
|
65
|
+
this.content = root.querySelector<HTMLElement>(".starwind-collapsible-content");
|
|
66
|
+
|
|
67
|
+
if (this.trigger && this.content) {
|
|
68
|
+
this.setupAccessibility();
|
|
69
|
+
this.setupEventListeners();
|
|
70
|
+
this.setInitialState();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private setupAccessibility(): void {
|
|
75
|
+
if (!this.trigger || !this.content) return;
|
|
76
|
+
|
|
77
|
+
const triggerId = `${this.collapsibleId}-trigger`;
|
|
78
|
+
const contentId = `${this.collapsibleId}-content`;
|
|
79
|
+
|
|
80
|
+
// Use "set if missing" semantics for IDs
|
|
81
|
+
if (!this.trigger.id) this.trigger.id = triggerId;
|
|
82
|
+
if (!this.content.id) this.content.id = contentId;
|
|
83
|
+
|
|
84
|
+
// Use actual IDs (consumer-provided or generated) for ARIA
|
|
85
|
+
this.trigger.setAttribute("aria-controls", this.content.id);
|
|
86
|
+
this.content.setAttribute("aria-labelledby", this.trigger.id);
|
|
87
|
+
this.content.setAttribute("role", "region");
|
|
88
|
+
|
|
89
|
+
this.updateAriaExpanded();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private setInitialState(): void {
|
|
93
|
+
const isOpen = this.root.dataset.state === "open";
|
|
94
|
+
this.setState(isOpen);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private setupEventListeners(): void {
|
|
98
|
+
if (!this.trigger) return;
|
|
99
|
+
|
|
100
|
+
this.trigger.addEventListener("click", () => {
|
|
101
|
+
if (this.root.dataset.disabled !== undefined) return;
|
|
102
|
+
this.toggle();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private toggle(): void {
|
|
107
|
+
const isOpen = this.root.dataset.state === "open";
|
|
108
|
+
this.setState(!isOpen);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private setState(isOpen: boolean): void {
|
|
112
|
+
const state: CollapsibleState = isOpen ? "open" : "closed";
|
|
113
|
+
|
|
114
|
+
this.root.dataset.state = state;
|
|
115
|
+
|
|
116
|
+
if (this.trigger) {
|
|
117
|
+
this.trigger.dataset.state = state;
|
|
118
|
+
if (isOpen) {
|
|
119
|
+
this.trigger.dataset.panelOpen = "";
|
|
120
|
+
} else {
|
|
121
|
+
delete this.trigger.dataset.panelOpen;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.content) {
|
|
126
|
+
this.content.dataset.state = state;
|
|
127
|
+
if (isOpen) {
|
|
128
|
+
this.content.hidden = false;
|
|
129
|
+
} else {
|
|
130
|
+
this.content.hidden = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.updateAriaExpanded();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private updateAriaExpanded(): void {
|
|
138
|
+
if (!this.trigger) return;
|
|
139
|
+
const isOpen = this.root.dataset.state === "open";
|
|
140
|
+
this.trigger.setAttribute("aria-expanded", isOpen.toString());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const collapsibleInstances = new WeakMap<HTMLElement, CollapsibleHandler>();
|
|
145
|
+
let collapsibleCounter = 0;
|
|
146
|
+
|
|
147
|
+
const setupCollapsibles = () => {
|
|
148
|
+
document.querySelectorAll<HTMLElement>(".starwind-collapsible").forEach((collapsible) => {
|
|
149
|
+
if (!collapsibleInstances.has(collapsible)) {
|
|
150
|
+
collapsibleInstances.set(
|
|
151
|
+
collapsible,
|
|
152
|
+
new CollapsibleHandler(collapsible, collapsibleCounter++),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
setupCollapsibles();
|
|
159
|
+
document.addEventListener("astro:after-swap", setupCollapsibles);
|
|
160
|
+
document.addEventListener("starwind:init", setupCollapsibles);
|
|
161
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
type Props = HTMLAttributes<"div">;
|
|
6
|
+
|
|
7
|
+
export const collapsibleContent = tv({
|
|
8
|
+
base: "starwind-collapsible-content",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const { class: className, ...rest } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
class={collapsibleContent({ class: className })}
|
|
16
|
+
data-state="closed"
|
|
17
|
+
data-slot="collapsible-content"
|
|
18
|
+
hidden
|
|
19
|
+
{...rest}
|
|
20
|
+
>
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
type Props = HTMLAttributes<"button"> & {
|
|
6
|
+
/**
|
|
7
|
+
* When true, renders as a wrapper div instead of a button,
|
|
8
|
+
* allowing a custom trigger element as a child
|
|
9
|
+
*/
|
|
10
|
+
asChild?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const collapsibleTrigger = tv({
|
|
14
|
+
base: "starwind-collapsible-trigger",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const { class: className, asChild = false, ...rest } = Astro.props;
|
|
18
|
+
|
|
19
|
+
let hasChildren = false;
|
|
20
|
+
if (Astro.slots.has("default")) {
|
|
21
|
+
hasChildren = true;
|
|
22
|
+
}
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
asChild && hasChildren ? (
|
|
27
|
+
<div
|
|
28
|
+
class={collapsibleTrigger({ class: className })}
|
|
29
|
+
data-slot="collapsible-trigger"
|
|
30
|
+
data-as-child
|
|
31
|
+
>
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
) : (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class={collapsibleTrigger({ class: className })}
|
|
38
|
+
data-slot="collapsible-trigger"
|
|
39
|
+
{...rest}
|
|
40
|
+
>
|
|
41
|
+
<slot />
|
|
42
|
+
</button>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Collapsible, { collapsible } from "./Collapsible.astro";
|
|
2
|
+
import CollapsibleContent, { collapsibleContent } from "./CollapsibleContent.astro";
|
|
3
|
+
import CollapsibleTrigger, { collapsibleTrigger } from "./CollapsibleTrigger.astro";
|
|
4
|
+
|
|
5
|
+
const CollapsibleVariants = { collapsible, collapsibleContent, collapsibleTrigger };
|
|
6
|
+
|
|
7
|
+
export { Collapsible, CollapsibleContent, CollapsibleTrigger, CollapsibleVariants };
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
Root: Collapsible,
|
|
11
|
+
Content: CollapsibleContent,
|
|
12
|
+
Trigger: CollapsibleTrigger,
|
|
13
|
+
};
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<"div">;
|
|
5
|
+
|
|
6
|
+
const { class: className, ...rest } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class:list={["starwind-dialog", className]} data-slot="dialog" {...rest}>
|
|
10
|
+
<slot />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
// Store instances in a WeakMap to avoid memory leaks
|
|
15
|
+
const dialogInstances = new WeakMap<HTMLElement, DialogHandler>();
|
|
16
|
+
let dialogCounter = 0;
|
|
17
|
+
|
|
18
|
+
class DialogHandler {
|
|
19
|
+
private triggers: HTMLButtonElement[] = [];
|
|
20
|
+
private dialog: HTMLDialogElement | null = null;
|
|
21
|
+
private closeButtons: HTMLButtonElement[] = [];
|
|
22
|
+
private backdrop: HTMLElement | null = null;
|
|
23
|
+
private dialogWrapper: HTMLElement;
|
|
24
|
+
private dialogId: string;
|
|
25
|
+
/**
|
|
26
|
+
* The duration of the animation in milliseconds. This is used to calculate the
|
|
27
|
+
* duration of close animation before hiding the dialog and backdrop
|
|
28
|
+
*/
|
|
29
|
+
private animationDuration: number;
|
|
30
|
+
private parentDialog: DialogHandler | null = null;
|
|
31
|
+
private nestedOpenCount: number = 0;
|
|
32
|
+
private isNested: boolean = false;
|
|
33
|
+
private _closeTimeout: number | null = null;
|
|
34
|
+
private _isClosing: boolean = false;
|
|
35
|
+
|
|
36
|
+
constructor(dialogWrapper: HTMLElement, dialogNumber: number) {
|
|
37
|
+
this.dialogWrapper = dialogWrapper;
|
|
38
|
+
this.dialog = dialogWrapper.querySelector("dialog");
|
|
39
|
+
this.backdrop = dialogWrapper.querySelector(".starwind-dialog-backdrop");
|
|
40
|
+
if (!this.dialog || !this.backdrop) {
|
|
41
|
+
throw new Error("Dialog: dialog or backdrop not found");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// if no ID was provided for the wrapper, generate one
|
|
45
|
+
if (dialogWrapper.id) {
|
|
46
|
+
this.dialogId = dialogWrapper.id;
|
|
47
|
+
} else {
|
|
48
|
+
this.dialogId = `starwind-dialog${dialogNumber}`;
|
|
49
|
+
dialogWrapper.id = this.dialogId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// animationDuration is set with inline styles through passed prop to DialogContent
|
|
53
|
+
// if no animationDuration, check data-close-duration
|
|
54
|
+
const animationDurationString = this.dialog.style.animationDuration;
|
|
55
|
+
if (animationDurationString.endsWith("ms")) {
|
|
56
|
+
this.animationDuration = parseFloat(animationDurationString);
|
|
57
|
+
} else if (animationDurationString.endsWith("s")) {
|
|
58
|
+
// using something like @playform/compress might optimize to use "s" instead of "ms"
|
|
59
|
+
this.animationDuration = parseFloat(animationDurationString) * 1000;
|
|
60
|
+
} else {
|
|
61
|
+
this.animationDuration = this.dialog.dataset.closeDuration
|
|
62
|
+
? parseFloat(this.dialog.dataset.closeDuration)
|
|
63
|
+
: 200;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Find internal triggers and handle them
|
|
67
|
+
const internalTriggers = dialogWrapper.querySelectorAll(".starwind-dialog-trigger");
|
|
68
|
+
internalTriggers.forEach((triggerElement) => {
|
|
69
|
+
const tempTrigger = triggerElement as HTMLElement;
|
|
70
|
+
let trigger: HTMLButtonElement;
|
|
71
|
+
|
|
72
|
+
if (tempTrigger?.hasAttribute("data-as-child")) {
|
|
73
|
+
trigger = tempTrigger.firstElementChild as HTMLButtonElement;
|
|
74
|
+
} else {
|
|
75
|
+
trigger = tempTrigger as HTMLButtonElement;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (trigger) {
|
|
79
|
+
this.triggers.push(trigger);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Find external triggers that target this dialog
|
|
84
|
+
this.findExternalTriggers();
|
|
85
|
+
|
|
86
|
+
// if closeButtons are set with asChild, swap the wrapper with its first child
|
|
87
|
+
const tempCloseButtons = dialogWrapper.querySelectorAll(
|
|
88
|
+
".starwind-dialog-close",
|
|
89
|
+
) as NodeListOf<HTMLElement>;
|
|
90
|
+
tempCloseButtons.forEach((button: HTMLElement) => {
|
|
91
|
+
if (button.hasAttribute("data-as-child")) {
|
|
92
|
+
const childElement = button.firstElementChild;
|
|
93
|
+
if (childElement) {
|
|
94
|
+
childElement.classList.add("starwind-dialog-close");
|
|
95
|
+
button.parentNode?.replaceChild(childElement, button);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return button;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Convert NodeList to Array for consistency with triggers
|
|
102
|
+
this.closeButtons = Array.from(
|
|
103
|
+
dialogWrapper.querySelectorAll(".starwind-dialog-close"),
|
|
104
|
+
) as HTMLButtonElement[];
|
|
105
|
+
|
|
106
|
+
// if essential elements are not there, exit
|
|
107
|
+
if (!this.dialog || !this.backdrop) return;
|
|
108
|
+
|
|
109
|
+
this.setupAccessibility(dialogNumber);
|
|
110
|
+
this.setupEvents();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private setupAccessibility(dialogNumber: number): void {
|
|
114
|
+
// get the first heading element in the dialog
|
|
115
|
+
const firstHeading = this.dialog?.querySelector("h1, h2, h3, h4, h5, h6");
|
|
116
|
+
if (firstHeading) {
|
|
117
|
+
// create a unique ID for the heading
|
|
118
|
+
firstHeading.id = `starwind-dialog${dialogNumber}-heading`;
|
|
119
|
+
// set the aria-labelledby attribute to the first heading element
|
|
120
|
+
this.dialog?.setAttribute("aria-labelledby", firstHeading.id);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find all external triggers that target this dialog
|
|
126
|
+
*/
|
|
127
|
+
private findExternalTriggers(): void {
|
|
128
|
+
const externalTriggers = document.querySelectorAll(
|
|
129
|
+
`.starwind-dialog-trigger[data-dialog-for="${this.dialogId}"]`,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
externalTriggers.forEach((triggerElement) => {
|
|
133
|
+
// Skip if this is an internal trigger we already processed
|
|
134
|
+
const dialogWrapper = triggerElement.closest(".starwind-dialog");
|
|
135
|
+
if (dialogWrapper && dialogWrapper.id === this.dialogId) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let trigger: HTMLButtonElement;
|
|
140
|
+
if (triggerElement.hasAttribute("data-as-child")) {
|
|
141
|
+
trigger = triggerElement.firstElementChild as HTMLButtonElement;
|
|
142
|
+
} else {
|
|
143
|
+
trigger = triggerElement as HTMLButtonElement;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (trigger && !this.triggers.includes(trigger)) {
|
|
147
|
+
this.triggers.push(trigger);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private setupEvents(): void {
|
|
153
|
+
if (!this.dialog) return;
|
|
154
|
+
|
|
155
|
+
// Add programmatic open/close via custom events on the wrapper
|
|
156
|
+
this.dialogWrapper.addEventListener("dialog:open", () => this.open());
|
|
157
|
+
this.dialogWrapper.addEventListener("dialog:close", () => this.close());
|
|
158
|
+
this.dialogWrapper.addEventListener("dialog:toggle", () => {
|
|
159
|
+
if (this.dialog?.open) {
|
|
160
|
+
this.close();
|
|
161
|
+
} else {
|
|
162
|
+
this.open();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Add click listeners to all triggers
|
|
167
|
+
this.triggers.forEach((trigger) => {
|
|
168
|
+
trigger.addEventListener("click", () => {
|
|
169
|
+
this.open();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Add click handlers to all close buttons
|
|
174
|
+
this.closeButtons?.forEach((button) => {
|
|
175
|
+
button.addEventListener("click", () => {
|
|
176
|
+
// Only close if this is the topmost dialog
|
|
177
|
+
const openDialogs = document.querySelectorAll("dialog[open]");
|
|
178
|
+
if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
|
|
179
|
+
this.close();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Close on click outside
|
|
185
|
+
this.dialog.addEventListener("click", (e) => {
|
|
186
|
+
if (!this.dialog) return;
|
|
187
|
+
const dialogDimensions = this.dialog.getBoundingClientRect();
|
|
188
|
+
const clickedInDialog =
|
|
189
|
+
e.clientX >= dialogDimensions.left &&
|
|
190
|
+
e.clientX <= dialogDimensions.right &&
|
|
191
|
+
e.clientY >= dialogDimensions.top &&
|
|
192
|
+
e.clientY <= dialogDimensions.bottom;
|
|
193
|
+
|
|
194
|
+
if (!clickedInDialog) {
|
|
195
|
+
// Only close if this is the topmost dialog
|
|
196
|
+
const openDialogs = document.querySelectorAll("dialog[open]");
|
|
197
|
+
if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
|
|
198
|
+
this.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Handle escape key
|
|
204
|
+
this.dialog.addEventListener("keydown", (e) => {
|
|
205
|
+
if (e.key === "Escape") {
|
|
206
|
+
// prevent default dialog closing behavior so we can add closing animation
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
// Only close if this is the topmost dialog
|
|
209
|
+
const openDialogs = document.querySelectorAll("dialog[open]");
|
|
210
|
+
if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
|
|
211
|
+
this.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Intercept form submissions to handle dialog close
|
|
217
|
+
const forms = this.dialog.querySelectorAll("form");
|
|
218
|
+
forms.forEach((form) => {
|
|
219
|
+
form.addEventListener("submit", (e) => {
|
|
220
|
+
/**
|
|
221
|
+
* Default form.method = "dialog" submissions cause the dialog to close
|
|
222
|
+
* Default form.method = "post" submissions do not close the dialog
|
|
223
|
+
* Here we intercept the form submission and manage the dialog closing if method = "dialog"
|
|
224
|
+
* so we can add closing animation
|
|
225
|
+
* Normal form event listeners for "submit" will still get the form data
|
|
226
|
+
*/
|
|
227
|
+
if (form.method === "dialog") {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
// Only close if this is the topmost dialog
|
|
230
|
+
const openDialogs = document.querySelectorAll("dialog[open]");
|
|
231
|
+
if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
|
|
232
|
+
this.close();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private open(): void {
|
|
240
|
+
if (!this.dialog || !this.backdrop) return;
|
|
241
|
+
// Return early if already open
|
|
242
|
+
if (this.dialog.open && !this._isClosing) return;
|
|
243
|
+
|
|
244
|
+
// Cancel any pending close
|
|
245
|
+
if (this._closeTimeout !== null) {
|
|
246
|
+
clearTimeout(this._closeTimeout);
|
|
247
|
+
this._closeTimeout = null;
|
|
248
|
+
}
|
|
249
|
+
this._isClosing = false;
|
|
250
|
+
|
|
251
|
+
// Reset nested count when opening (in case it wasn't properly reset)
|
|
252
|
+
this.nestedOpenCount = 0;
|
|
253
|
+
this.updateNestedState();
|
|
254
|
+
|
|
255
|
+
this.dialog.showModal();
|
|
256
|
+
document.body.classList.add("overflow-hidden");
|
|
257
|
+
|
|
258
|
+
// For nested dialogs, hide the backdrop and notify parent
|
|
259
|
+
if (this.isNested && this.parentDialog) {
|
|
260
|
+
this.backdrop.classList.add("hidden");
|
|
261
|
+
this.parentDialog.onNestedDialogOpen();
|
|
262
|
+
} else {
|
|
263
|
+
this.backdrop.classList.remove("hidden");
|
|
264
|
+
this.backdrop.dataset.state = "open";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.dialog.dataset.state = "open";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private close(): void {
|
|
271
|
+
if (!this.dialog || !this.backdrop) return;
|
|
272
|
+
// Return early if already closed or closing
|
|
273
|
+
if (!this.dialog.open || this._isClosing) return;
|
|
274
|
+
|
|
275
|
+
// Clear any existing close timeout
|
|
276
|
+
if (this._closeTimeout !== null) {
|
|
277
|
+
clearTimeout(this._closeTimeout);
|
|
278
|
+
this._closeTimeout = null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this._isClosing = true;
|
|
282
|
+
this.dialog.dataset.state = "closed";
|
|
283
|
+
|
|
284
|
+
// Notify parent dialog that nested dialog is closing
|
|
285
|
+
if (this.isNested && this.parentDialog) {
|
|
286
|
+
this.parentDialog.onNestedDialogClose();
|
|
287
|
+
} else {
|
|
288
|
+
this.backdrop.dataset.state = "closed";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Wait for animation to finish before hiding backdrop
|
|
292
|
+
this._closeTimeout = window.setTimeout(() => {
|
|
293
|
+
if (!this.isNested) {
|
|
294
|
+
this.backdrop?.classList.add("hidden");
|
|
295
|
+
}
|
|
296
|
+
this.dialog?.close();
|
|
297
|
+
this._isClosing = false;
|
|
298
|
+
this._closeTimeout = null;
|
|
299
|
+
const stillOpen = document.querySelectorAll("dialog[open]").length;
|
|
300
|
+
if (stillOpen === 0) {
|
|
301
|
+
document.body.classList.remove("overflow-hidden");
|
|
302
|
+
}
|
|
303
|
+
}, this.animationDuration);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Called by child dialogs when they open
|
|
308
|
+
*/
|
|
309
|
+
public onNestedDialogOpen(): void {
|
|
310
|
+
this.nestedOpenCount++;
|
|
311
|
+
this.updateNestedState();
|
|
312
|
+
// Propagate up the chain to grandparent dialogs
|
|
313
|
+
if (this.parentDialog) {
|
|
314
|
+
this.parentDialog.onNestedDialogOpen();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Called by child dialogs when they close
|
|
320
|
+
*/
|
|
321
|
+
public onNestedDialogClose(): void {
|
|
322
|
+
this.nestedOpenCount = Math.max(0, this.nestedOpenCount - 1);
|
|
323
|
+
this.updateNestedState();
|
|
324
|
+
// Propagate up the chain to grandparent dialogs
|
|
325
|
+
if (this.parentDialog) {
|
|
326
|
+
this.parentDialog.onNestedDialogClose();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Updates the nested dialog state attributes
|
|
332
|
+
*/
|
|
333
|
+
private updateNestedState(): void {
|
|
334
|
+
if (!this.dialog) return;
|
|
335
|
+
|
|
336
|
+
if (this.nestedOpenCount > 0) {
|
|
337
|
+
this.dialog.setAttribute("data-nested-dialog-open", "");
|
|
338
|
+
this.dialog.style.setProperty("--nested-dialogs", String(this.nestedOpenCount));
|
|
339
|
+
} else {
|
|
340
|
+
this.dialog.removeAttribute("data-nested-dialog-open");
|
|
341
|
+
this.dialog.style.removeProperty("--nested-dialogs");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Links this dialog to its parent dialog (called after all dialogs are initialized)
|
|
347
|
+
*/
|
|
348
|
+
public linkParentDialog(): void {
|
|
349
|
+
const parentDialogWrapper = this.dialogWrapper.parentElement?.closest(".starwind-dialog");
|
|
350
|
+
if (parentDialogWrapper) {
|
|
351
|
+
this.isNested = true;
|
|
352
|
+
const parentInstance = dialogInstances.get(parentDialogWrapper as HTMLElement);
|
|
353
|
+
if (parentInstance) {
|
|
354
|
+
this.parentDialog = parentInstance;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Initialize all dialogs
|
|
361
|
+
const setupDialogs = () => {
|
|
362
|
+
// First pass: create all dialog instances
|
|
363
|
+
document.querySelectorAll(".starwind-dialog").forEach((dialogWrapper) => {
|
|
364
|
+
const wrapper = dialogWrapper as HTMLElement;
|
|
365
|
+
if (!dialogInstances.has(wrapper)) {
|
|
366
|
+
dialogInstances.set(wrapper, new DialogHandler(wrapper, dialogCounter++));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Second pass: link parent dialogs (now that all instances exist)
|
|
371
|
+
document.querySelectorAll(".starwind-dialog").forEach((dialogWrapper) => {
|
|
372
|
+
const wrapper = dialogWrapper as HTMLElement;
|
|
373
|
+
const instance = dialogInstances.get(wrapper);
|
|
374
|
+
if (instance) {
|
|
375
|
+
instance.linkParentDialog();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
setupDialogs();
|
|
381
|
+
document.addEventListener("astro:after-swap", setupDialogs);
|
|
382
|
+
document.addEventListener("starwind:init", setupDialogs);
|
|
383
|
+
</script>
|
|
384
|
+
|
|
385
|
+
<style>
|
|
386
|
+
.overflow-hidden {
|
|
387
|
+
overflow: hidden;
|
|
388
|
+
}
|
|
389
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<"button"> & {
|
|
5
|
+
/**
|
|
6
|
+
* When true, the component will render its child element instead of a button
|
|
7
|
+
*/
|
|
8
|
+
asChild?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const { class: className, asChild = false, ...rest } = Astro.props;
|
|
12
|
+
|
|
13
|
+
// Get the first child element if asChild is true
|
|
14
|
+
let hasChildren = false;
|
|
15
|
+
if (Astro.slots.has("default")) {
|
|
16
|
+
hasChildren = true;
|
|
17
|
+
}
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
asChild && hasChildren ? (
|
|
22
|
+
<div class="starwind-dialog-close" data-slot="dialog-close" data-as-child>
|
|
23
|
+
<slot />
|
|
24
|
+
</div>
|
|
25
|
+
) : (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class:list={["starwind-dialog-close", className]}
|
|
29
|
+
data-slot="dialog-close"
|
|
30
|
+
{...rest}
|
|
31
|
+
>
|
|
32
|
+
<slot>Demo close button</slot>
|
|
33
|
+
</button>
|
|
34
|
+
)
|
|
35
|
+
}
|