@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
@@ -3,78 +3,78 @@ import type { HTMLAttributes } from "astro/types";
3
3
  import { tv } from "tailwind-variants";
4
4
 
5
5
  type Props = HTMLAttributes<"div"> & {
6
- /**
7
- * Side of the dropdown
8
- * @default bottom
9
- */
10
- side?: "top" | "bottom";
11
- /**
12
- * Alignment of the dropdown
13
- * @default start
14
- */
15
- align?: "start" | "center" | "end";
16
- /**
17
- * Offset distance in pixels
18
- * @default 4
19
- */
20
- sideOffset?: number;
21
- /**
22
- * Open and close animation duration in milliseconds
23
- * @default 150
24
- */
25
- animationDuration?: number;
6
+ /**
7
+ * Side of the dropdown
8
+ * @default bottom
9
+ */
10
+ side?: "top" | "bottom";
11
+ /**
12
+ * Alignment of the dropdown
13
+ * @default start
14
+ */
15
+ align?: "start" | "center" | "end";
16
+ /**
17
+ * Offset distance in pixels
18
+ * @default 4
19
+ */
20
+ sideOffset?: number;
21
+ /**
22
+ * Open and close animation duration in milliseconds
23
+ * @default 150
24
+ */
25
+ animationDuration?: number;
26
26
  };
27
27
 
28
28
  const dropdownContent = tv({
29
- base: [
30
- "starwind-dropdown-content",
31
- "bg-popover text-popover-foreground z-50 min-w-[9rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
32
- "data-[state=open]:animate-in fade-in zoom-in-95",
33
- "data-[state=closed]:animate-out data-[state=closed]:fill-mode-forwards fade-out zoom-out-95",
34
- "absolute will-change-transform",
35
- ],
36
- variants: {
37
- side: {
38
- bottom: "slide-in-from-top-2 slide-out-to-top-2 top-full",
39
- top: "slide-in-from-bottom-2 slide-out-to-bottom-2 bottom-full",
40
- },
41
- align: {
42
- start: "slide-in-from-left-1 slide-out-to-left-1 left-0",
43
- center: "left-1/2 -translate-x-1/2",
44
- end: "slide-in-from-right-1 slide-out-to-right-1 right-0",
45
- },
46
- },
47
- defaultVariants: {
48
- side: "bottom",
49
- align: "start",
50
- },
29
+ base: [
30
+ "starwind-dropdown-content",
31
+ "bg-popover text-popover-foreground z-50 min-w-[9rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
32
+ "data-[state=open]:animate-in fade-in zoom-in-95",
33
+ "data-[state=closed]:animate-out data-[state=closed]:fill-mode-forwards fade-out zoom-out-95",
34
+ "absolute will-change-transform",
35
+ ],
36
+ variants: {
37
+ side: {
38
+ bottom: "slide-in-from-top-2 slide-out-to-top-2 top-full",
39
+ top: "slide-in-from-bottom-2 slide-out-to-bottom-2 bottom-full",
40
+ },
41
+ align: {
42
+ start: "slide-in-from-left-1 slide-out-to-left-1 left-0",
43
+ center: "left-1/2 -translate-x-1/2",
44
+ end: "slide-in-from-right-1 slide-out-to-right-1 right-0",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ side: "bottom",
49
+ align: "start",
50
+ },
51
51
  });
52
52
 
53
53
  const {
54
- class: className,
55
- side = "bottom",
56
- align = "start",
57
- sideOffset = 4,
58
- animationDuration = 150,
59
- ...rest
54
+ class: className,
55
+ side = "bottom",
56
+ align = "start",
57
+ sideOffset = 4,
58
+ animationDuration = 150,
59
+ ...rest
60
60
  } = Astro.props;
61
61
  ---
62
62
 
63
63
  <div
64
- class={dropdownContent({ side, align, class: className })}
65
- role="menu"
66
- data-side={side}
67
- data-align={align}
68
- data-state="closed"
69
- tabindex="-1"
70
- aria-orientation="vertical"
71
- style={{
72
- display: "none",
73
- animationDuration: `${animationDuration}ms`,
74
- marginTop: side === "bottom" ? `${sideOffset}px` : undefined,
75
- marginBottom: side === "top" ? `${sideOffset}px` : undefined,
76
- }}
77
- {...rest}
64
+ class={dropdownContent({ side, align, class: className })}
65
+ role="menu"
66
+ data-side={side}
67
+ data-align={align}
68
+ data-state="closed"
69
+ tabindex="-1"
70
+ aria-orientation="vertical"
71
+ style={{
72
+ display: "none",
73
+ animationDuration: `${animationDuration}ms`,
74
+ marginTop: side === "bottom" ? `${sideOffset}px` : undefined,
75
+ marginBottom: side === "top" ? `${sideOffset}px` : undefined,
76
+ }}
77
+ {...rest}
78
78
  >
79
- <slot />
79
+ <slot />
80
80
  </div>
@@ -3,45 +3,45 @@ import type { HTMLTag, Polymorphic } from "astro/types";
3
3
  import { tv } from "tailwind-variants";
4
4
 
5
5
  type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }> & {
6
- /**
7
- * Whether the item is inset (has left padding)
8
- */
9
- inset?: boolean;
10
- /**
11
- * Whether the item is disabled
12
- */
13
- disabled?: boolean;
6
+ /**
7
+ * Whether the item is inset (has left padding)
8
+ */
9
+ inset?: boolean;
10
+ /**
11
+ * Whether the item is disabled
12
+ */
13
+ disabled?: boolean;
14
14
  };
15
15
 
16
16
  const dropdownItem = tv({
17
- base: [
18
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 transition-colors outline-none select-none",
19
- "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
20
- "[&>svg]:size-4 [&>svg]:shrink-0",
21
- ],
22
- variants: {
23
- inset: {
24
- true: "pl-8",
25
- },
26
- disabled: {
27
- true: "pointer-events-none opacity-50",
28
- },
29
- },
30
- defaultVariants: {
31
- inset: false,
32
- disabled: false,
33
- },
17
+ base: [
18
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 transition-colors outline-none select-none",
19
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
20
+ "[&>svg]:size-4 [&>svg]:shrink-0",
21
+ ],
22
+ variants: {
23
+ inset: {
24
+ true: "pl-8",
25
+ },
26
+ disabled: {
27
+ true: "pointer-events-none opacity-50",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ inset: false,
32
+ disabled: false,
33
+ },
34
34
  });
35
35
 
36
36
  const { class: className, inset = false, disabled = false, as: Tag = "div", ...rest } = Astro.props;
37
37
  ---
38
38
 
39
39
  <Tag
40
- class={dropdownItem({ inset, disabled, class: className })}
41
- role="menuitem"
42
- tabindex={disabled ? "-1" : "0"}
43
- data-disabled={disabled ? "true" : undefined}
44
- {...rest}
40
+ class={dropdownItem({ inset, disabled, class: className })}
41
+ role="menuitem"
42
+ tabindex={disabled ? "-1" : "0"}
43
+ data-disabled={disabled ? "true" : undefined}
44
+ {...rest}
45
45
  >
46
- <slot />
46
+ <slot />
47
47
  </Tag>
@@ -3,27 +3,27 @@ import type { HTMLAttributes } from "astro/types";
3
3
  import { tv } from "tailwind-variants";
4
4
 
5
5
  type Props = HTMLAttributes<"div"> & {
6
- /**
7
- * Whether the label is inset (has left padding)
8
- */
9
- inset?: boolean;
6
+ /**
7
+ * Whether the label is inset (has left padding)
8
+ */
9
+ inset?: boolean;
10
10
  };
11
11
 
12
12
  const dropdownLabel = tv({
13
- base: ["px-2 py-1.5 font-semibold"],
14
- variants: {
15
- inset: {
16
- true: "pl-8",
17
- },
18
- },
19
- defaultVariants: {
20
- inset: false,
21
- },
13
+ base: ["px-2 py-1.5 font-semibold"],
14
+ variants: {
15
+ inset: {
16
+ true: "pl-8",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ inset: false,
21
+ },
22
22
  });
23
23
 
24
24
  const { class: className, inset = false, ...rest } = Astro.props;
25
25
  ---
26
26
 
27
27
  <div class={dropdownLabel({ inset, class: className })} {...rest}>
28
- <slot />
28
+ <slot />
29
29
  </div>
@@ -5,16 +5,16 @@ import { tv } from "tailwind-variants";
5
5
  type Props = HTMLAttributes<"div">;
6
6
 
7
7
  const dropdownSeparator = tv({
8
- base: "bg-muted -mx-1 my-1 h-px",
8
+ base: "bg-muted -mx-1 my-1 h-px",
9
9
  });
10
10
 
11
11
  const { class: className, ...rest } = Astro.props;
12
12
  ---
13
13
 
14
14
  <div
15
- class={dropdownSeparator({ class: className })}
16
- role="separator"
17
- aria-orientation="horizontal"
18
- {...rest}
15
+ class={dropdownSeparator({ class: className })}
16
+ role="separator"
17
+ aria-orientation="horizontal"
18
+ {...rest}
19
19
  >
20
20
  </div>
@@ -3,18 +3,18 @@ import type { HTMLAttributes } from "astro/types";
3
3
  import { tv } from "tailwind-variants";
4
4
 
5
5
  type Props = Omit<HTMLAttributes<"button">, "role" | "type"> & {
6
- /**
7
- * When true, the component will render its child element with a simple wrapper instead of a button component
8
- */
9
- asChild?: boolean;
6
+ /**
7
+ * When true, the component will render its child element with a simple wrapper instead of a button component
8
+ */
9
+ asChild?: boolean;
10
10
  };
11
11
 
12
12
  const dropdownTrigger = tv({
13
- base: [
14
- "starwind-dropdown-trigger",
15
- "inline-flex items-center justify-center",
16
- "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
17
- ],
13
+ base: [
14
+ "starwind-dropdown-trigger",
15
+ "inline-flex items-center justify-center",
16
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
17
+ ],
18
18
  });
19
19
 
20
20
  const { class: className, asChild = false, ...rest } = Astro.props;
@@ -22,25 +22,25 @@ const { class: className, asChild = false, ...rest } = Astro.props;
22
22
  // Get the first child element if asChild is true
23
23
  let hasChildren = false;
24
24
  if (Astro.slots.has("default")) {
25
- hasChildren = true;
25
+ hasChildren = true;
26
26
  }
27
27
  ---
28
28
 
29
29
  {
30
- asChild && hasChildren ? (
31
- <div class={`starwind-dropdown-trigger ${className}`} data-as-child>
32
- <slot />
33
- </div>
34
- ) : (
35
- <button
36
- class={dropdownTrigger({ class: className })}
37
- type="button"
38
- aria-haspopup="true"
39
- aria-expanded="false"
40
- data-state="closed"
41
- {...rest}
42
- >
43
- <slot />
44
- </button>
45
- )
30
+ asChild && hasChildren ? (
31
+ <div class={`starwind-dropdown-trigger ${className}`} data-as-child>
32
+ <slot />
33
+ </div>
34
+ ) : (
35
+ <button
36
+ class={dropdownTrigger({ class: className })}
37
+ type="button"
38
+ aria-haspopup="true"
39
+ aria-expanded="false"
40
+ data-state="closed"
41
+ {...rest}
42
+ >
43
+ <slot />
44
+ </button>
45
+ )
46
46
  }
@@ -6,19 +6,19 @@ import DropdownSeparator from "./DropdownSeparator.astro";
6
6
  import DropdownTrigger from "./DropdownTrigger.astro";
7
7
 
8
8
  export {
9
- Dropdown,
10
- DropdownTrigger,
11
- DropdownContent,
12
- DropdownItem,
13
- DropdownLabel,
14
- DropdownSeparator,
9
+ Dropdown,
10
+ DropdownContent,
11
+ DropdownItem,
12
+ DropdownLabel,
13
+ DropdownSeparator,
14
+ DropdownTrigger,
15
15
  };
16
16
 
17
17
  export default {
18
- Root: Dropdown,
19
- Trigger: DropdownTrigger,
20
- Content: DropdownContent,
21
- Item: DropdownItem,
22
- Label: DropdownLabel,
23
- Separator: DropdownSeparator,
18
+ Root: Dropdown,
19
+ Trigger: DropdownTrigger,
20
+ Content: DropdownContent,
21
+ Item: DropdownItem,
22
+ Label: DropdownLabel,
23
+ Separator: DropdownSeparator,
24
24
  };
@@ -0,0 +1,232 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ import DropzoneFilesList from "./DropzoneFilesList.astro";
6
+ import DropzoneLoadingIndicator from "./DropzoneLoadingIndicator.astro";
7
+ import DropzoneUploadIndicator from "./DropzoneUploadIndicator.astro";
8
+
9
+ type Props = HTMLAttributes<"input"> & {
10
+ /**
11
+ * Whether to show the loading indicator initially
12
+ */
13
+ isUploading?: boolean;
14
+ };
15
+
16
+ // extract id separately so it can be used in the label. The input will get the same id with "-input" suffix.
17
+ const { class: className, id, isUploading = false, ...rest } = Astro.props as Props;
18
+
19
+ const root = tv({
20
+ base: [
21
+ "starwind-dropzone",
22
+ "relative flex w-full flex-col items-center justify-center gap-1 rounded-lg px-6 py-12",
23
+ "bg-background text-muted-foreground border-input cursor-pointer border border-dashed text-center text-sm",
24
+ "starwind-transition-colors data-[is-uploading=false]:hover:bg-muted/50 data-[drag-active=true]:bg-muted/50",
25
+ "focus-visible:outline-outline focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2",
26
+ ],
27
+ });
28
+ ---
29
+
30
+ <label
31
+ id={id}
32
+ tabindex="0"
33
+ class={root({ class: className })}
34
+ data-drag-active="false"
35
+ data-has-files="false"
36
+ data-is-uploading={String(isUploading)}
37
+ >
38
+ <slot>
39
+ <DropzoneUploadIndicator />
40
+ <DropzoneLoadingIndicator />
41
+ <DropzoneFilesList />
42
+ </slot>
43
+
44
+ <input tabindex="-1" type="file" class="sr-only" {...rest} />
45
+ </label>
46
+
47
+ <script>
48
+ const fileSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-file"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" /></svg>`;
49
+ class FileInputHandler {
50
+ private label: HTMLLabelElement;
51
+ private input: HTMLInputElement;
52
+ private uploadIndicator: HTMLElement | null;
53
+ private loadingIndicator: HTMLElement | null;
54
+ private filesList: HTMLElement | null;
55
+ private files: File[] = [];
56
+ private hasFiles: boolean = false;
57
+ private isUploading: boolean = false;
58
+ private observer: MutationObserver | null = null;
59
+
60
+ constructor(label: HTMLLabelElement, idx: number) {
61
+ this.label = label;
62
+ const maybeInput = label.querySelector<HTMLInputElement>('input[type="file"]');
63
+ if (!maybeInput) {
64
+ throw new Error("No file input found inside starwind-dropzone");
65
+ }
66
+ this.input = maybeInput;
67
+ this.uploadIndicator = label.querySelector(".starwind-upload-indicator");
68
+ this.loadingIndicator = label.querySelector(".starwind-loading-indicator");
69
+ this.filesList = label.querySelector(".starwind-files-list");
70
+
71
+ // generate ID
72
+ if (this.label.id) {
73
+ const inputId = this.label.id + "-input";
74
+ this.input.id = inputId;
75
+ this.label.htmlFor = inputId;
76
+ } else {
77
+ const generatedId = `starwind-dropzone-${idx}`;
78
+ this.input.id = generatedId;
79
+ this.label.htmlFor = generatedId;
80
+ }
81
+
82
+ // Setup event handlers
83
+ this.setupEvents();
84
+ }
85
+
86
+ private setActive(active: boolean) {
87
+ this.label.dataset.dragActive = String(active);
88
+ }
89
+
90
+ // Set up mutation observer to watch for external attribute changes
91
+ private observeAttributeChanges() {
92
+ this.observer = new MutationObserver((mutations) => {
93
+ mutations.forEach((mutation) => {
94
+ if (mutation.type === "attributes" && mutation.attributeName === "data-is-uploading") {
95
+ this.handleAttributeChange();
96
+ }
97
+ });
98
+ });
99
+
100
+ this.observer.observe(this.label, {
101
+ attributes: true,
102
+ attributeFilter: ["data-is-uploading"],
103
+ });
104
+ }
105
+
106
+ // Handle attribute changes from external sources
107
+ private handleAttributeChange() {
108
+ const isUploading = this.label.dataset.isUploading === "true";
109
+
110
+ // Only update internal state if it's different
111
+ if (isUploading !== this.isUploading) {
112
+ this.isUploading = isUploading;
113
+
114
+ // Update UI
115
+ if (this.uploadIndicator && this.loadingIndicator) {
116
+ if (isUploading) {
117
+ this.uploadIndicator.classList.add("hidden");
118
+ this.loadingIndicator.classList.remove("hidden");
119
+ } else {
120
+ this.loadingIndicator.classList.add("hidden");
121
+ this.uploadIndicator.classList.remove("hidden");
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ private updateFilesList() {
128
+ if (!this.filesList) return;
129
+
130
+ const hasFiles = this.files.length > 0;
131
+ this.label.dataset.hasFiles = String(hasFiles);
132
+
133
+ if (hasFiles) {
134
+ this.filesList.classList.remove("invisible");
135
+
136
+ // Clear previous files
137
+ this.filesList.innerHTML = "";
138
+
139
+ // Create file list
140
+ this.files.forEach((file) => {
141
+ const fileItem = document.createElement("div");
142
+
143
+ // Insert SVG directly
144
+ fileItem.innerHTML = fileSvg;
145
+
146
+ const fileText = document.createElement("span");
147
+ fileText.textContent = file.name;
148
+
149
+ // Append text after the SVG
150
+ fileItem.appendChild(fileText);
151
+
152
+ // Use non-null assertion since we already checked at the beginning of the method
153
+ this.filesList!.appendChild(fileItem);
154
+ });
155
+ } else {
156
+ this.filesList.classList.add("invisible");
157
+ }
158
+ }
159
+
160
+ private handleFiles(files: FileList | null) {
161
+ if (!files || files.length === 0) return;
162
+
163
+ this.files = Array.from(files);
164
+ this.updateFilesList();
165
+ }
166
+
167
+ private setupEvents() {
168
+ this.label.addEventListener("dragover", (e) => {
169
+ e.preventDefault();
170
+ this.setActive(true);
171
+ });
172
+
173
+ this.label.addEventListener("dragleave", () => {
174
+ this.setActive(false);
175
+ });
176
+
177
+ this.label.addEventListener("drop", (e) => {
178
+ e.preventDefault();
179
+ this.setActive(false);
180
+
181
+ const { files } = e.dataTransfer || {};
182
+ if (files && files.length) {
183
+ const dt = new DataTransfer();
184
+ Array.from(files).forEach((file) => dt.items.add(file));
185
+ this.input.files = dt.files;
186
+ this.input.dispatchEvent(new Event("change", { bubbles: true }));
187
+
188
+ this.handleFiles(files);
189
+ }
190
+ });
191
+
192
+ // Handle file selection from dialog
193
+ this.input.addEventListener("change", () => {
194
+ this.handleFiles(this.input.files);
195
+ });
196
+
197
+ // Add keyboard accessibility for Enter and Space keys
198
+ this.label.addEventListener("keydown", (e) => {
199
+ // Handle Enter (13) and Space (32) keys
200
+ if (e.key === "Enter" || e.key === " ") {
201
+ e.preventDefault();
202
+ this.input.click(); // Trigger the native file dialog
203
+ }
204
+ });
205
+
206
+ if (this.loadingIndicator) {
207
+ // Watch for external changes to data-is-uploading attribute
208
+ this.observeAttributeChanges();
209
+ // Initialize based on current attribute values
210
+ this.handleAttributeChange();
211
+ }
212
+ }
213
+ }
214
+
215
+ // Store instances in a WeakMap to avoid memory leaks
216
+ const fileInputInstances = new WeakMap<HTMLElement, FileInputHandler>();
217
+
218
+ const setupFileInputs = () => {
219
+ document.querySelectorAll<HTMLLabelElement>(".starwind-dropzone").forEach((label, idx) => {
220
+ if (!fileInputInstances.has(label)) {
221
+ try {
222
+ fileInputInstances.set(label, new FileInputHandler(label, idx));
223
+ } catch {
224
+ /* ignore labels without inputs */
225
+ }
226
+ }
227
+ });
228
+ };
229
+
230
+ setupFileInputs();
231
+ document.addEventListener("astro:after-swap", setupFileInputs);
232
+ </script>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"div">;
6
+
7
+ const { class: className, ...rest } = Astro.props;
8
+
9
+ const filesList = tv({
10
+ base: [
11
+ "starwind-files-list",
12
+ "mt-1 -mb-8 min-h-8",
13
+ "bg-muted invisible flex flex-col items-center justify-center gap-1 rounded-md px-2 py-1 text-sm",
14
+ "[&_div]:flex [&_div]:items-center [&_div]:gap-1 [&_svg]:size-3.5",
15
+ ],
16
+ });
17
+ ---
18
+
19
+ <div
20
+ class={filesList({ class: className })}
21
+ aria-live="polite"
22
+ aria-label="Uploaded files"
23
+ {...rest}
24
+ >
25
+ </div>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import LoaderIcon from "@tabler/icons/outline/loader-2.svg";
3
+ ---
4
+
5
+ <div class="starwind-loading-indicator hidden">
6
+ <slot>
7
+ <LoaderIcon class="mx-auto size-10 animate-spin" aria-hidden="true" />
8
+ <p class="mt-1 text-sm">Uploading file(s)...</p>
9
+ </slot>
10
+ </div>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import CloudUploadIcon from "@tabler/icons/outline/cloud-upload.svg";
3
+ ---
4
+
5
+ <div class="starwind-upload-indicator">
6
+ <slot>
7
+ <CloudUploadIcon class="mx-auto size-10" aria-hidden="true" />
8
+ <p class="mt-1 text-sm">Click to upload or drag and drop</p>
9
+ </slot>
10
+ </div>