@starwind-ui/core 1.4.1 → 1.5.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.
package/dist/index.js CHANGED
@@ -10,10 +10,12 @@ var registry_default = {
10
10
  { name: "alert", type: "component", version: "1.1.1", dependencies: [] },
11
11
  { name: "avatar", type: "component", version: "1.1.1", dependencies: [] },
12
12
  { name: "badge", type: "component", version: "1.1.1", dependencies: [] },
13
+ { name: "breadcrumb", type: "component", version: "1.0.0", dependencies: [] },
13
14
  { name: "button", type: "component", version: "2.0.1", dependencies: [] },
14
15
  { name: "card", type: "component", version: "1.1.0", dependencies: [] },
15
16
  { name: "checkbox", type: "component", version: "1.2.0", dependencies: [] },
16
17
  { name: "dialog", type: "component", version: "1.1.1", dependencies: [] },
18
+ { name: "dropdown", type: "component", version: "1.0.1", dependencies: [] },
17
19
  { name: "input", type: "component", version: "1.1.1", dependencies: [] },
18
20
  { name: "label", type: "component", version: "1.1.1", dependencies: [] },
19
21
  { name: "pagination", type: "component", version: "2.0.1", dependencies: [] },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/registry.json"],"sourcesContent":["import { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport componentRegistry from \"./registry.json\" with { type: \"json\" };\n\n/**\n * Component metadata interface describing a Starwind UI component\n */\nexport interface ComponentMeta {\n\tname: string;\n\tversion: string;\n\ttype: \"component\";\n\tdependencies: string[];\n}\n\n/**\n * Registry interface containing all available components\n */\nexport interface Registry {\n\tcomponents: ComponentMeta[];\n}\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\n\n/**\n * Get the absolute path to a component file\n * @param {string} componentName - The name of the component\n * @param {string} fileName - The name of the file within the component\n * @returns {string} The absolute path to the component file\n */\nexport const getComponentPath = (componentName: string, fileName: string): string => {\n\t// In production (when installed as a dependency), the components will be in dist/src/components\n\t// In development, they will be in src/components\n\tconst componentsDir = __dirname.includes(\"dist\") ? \"src/components\" : \"src/components\";\n\treturn join(__dirname, componentsDir, componentName, fileName);\n};\n\n/**\n * Map of all components and their metadata from registry\n */\nexport const registry = componentRegistry.components as ComponentMeta[];\n","{\n\t\"$schema\": \"https://starwind.dev/registry-schema.json\",\n\t\"components\": [\n\t\t{ \"name\": \"accordion\", \"type\": \"component\", \"version\": \"1.1.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"alert\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"avatar\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"badge\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"button\", \"type\": \"component\", \"version\": \"2.0.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"card\", \"type\": \"component\", \"version\": \"1.1.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"checkbox\", \"type\": \"component\", \"version\": \"1.2.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"dialog\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"input\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"label\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"pagination\", \"type\": \"component\", \"version\": \"2.0.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"select\", \"type\": \"component\", \"version\": \"1.2.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"switch\", \"type\": \"component\", \"version\": \"1.1.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"tabs\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"textarea\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"tooltip\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] }\n\t]\n}\n"],"mappings":";AAAA,SAAS,YAAY;AACrB,SAAS,qBAAqB;;;ACD9B;AAAA,EACC,SAAW;AAAA,EACX,YAAc;AAAA,IACb,EAAE,MAAQ,aAAa,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IACnF,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,QAAQ,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC9E,EAAE,MAAQ,YAAY,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAClF,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,cAAc,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IACpF,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,QAAQ,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC9E,EAAE,MAAQ,YAAY,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAClF,EAAE,MAAQ,WAAW,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,EAClF;AACD;;;ADCA,IAAM,YAAY,cAAc,IAAI,IAAI,KAAK,YAAY,GAAG,CAAC;AAQtD,IAAM,mBAAmB,CAAC,eAAuB,aAA6B;AAGpF,QAAM,gBAAgB,UAAU,SAAS,MAAM,IAAI,mBAAmB;AACtE,SAAO,KAAK,WAAW,eAAe,eAAe,QAAQ;AAC9D;AAKO,IAAM,WAAW,iBAAkB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/registry.json"],"sourcesContent":["import { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport componentRegistry from \"./registry.json\" with { type: \"json\" };\n\n/**\n * Component metadata interface describing a Starwind UI component\n */\nexport interface ComponentMeta {\n\tname: string;\n\tversion: string;\n\ttype: \"component\";\n\tdependencies: string[];\n}\n\n/**\n * Registry interface containing all available components\n */\nexport interface Registry {\n\tcomponents: ComponentMeta[];\n}\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\n\n/**\n * Get the absolute path to a component file\n * @param {string} componentName - The name of the component\n * @param {string} fileName - The name of the file within the component\n * @returns {string} The absolute path to the component file\n */\nexport const getComponentPath = (componentName: string, fileName: string): string => {\n\t// In production (when installed as a dependency), the components will be in dist/src/components\n\t// In development, they will be in src/components\n\tconst componentsDir = __dirname.includes(\"dist\") ? \"src/components\" : \"src/components\";\n\treturn join(__dirname, componentsDir, componentName, fileName);\n};\n\n/**\n * Map of all components and their metadata from registry\n */\nexport const registry = componentRegistry.components as ComponentMeta[];\n","{\n\t\"$schema\": \"https://starwind.dev/registry-schema.json\",\n\t\"components\": [\n\t\t{ \"name\": \"accordion\", \"type\": \"component\", \"version\": \"1.1.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"alert\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"avatar\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"badge\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"breadcrumb\", \"type\": \"component\", \"version\": \"1.0.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"button\", \"type\": \"component\", \"version\": \"2.0.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"card\", \"type\": \"component\", \"version\": \"1.1.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"checkbox\", \"type\": \"component\", \"version\": \"1.2.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"dialog\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"dropdown\", \"type\": \"component\", \"version\": \"1.0.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"input\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"label\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"pagination\", \"type\": \"component\", \"version\": \"2.0.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"select\", \"type\": \"component\", \"version\": \"1.2.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"switch\", \"type\": \"component\", \"version\": \"1.1.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"tabs\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"textarea\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] },\n\t\t{ \"name\": \"tooltip\", \"type\": \"component\", \"version\": \"1.1.1\", \"dependencies\": [] }\n\t]\n}\n"],"mappings":";AAAA,SAAS,YAAY;AACrB,SAAS,qBAAqB;;;ACD9B;AAAA,EACC,SAAW;AAAA,EACX,YAAc;AAAA,IACb,EAAE,MAAQ,aAAa,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IACnF,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,cAAc,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IACpF,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,QAAQ,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC9E,EAAE,MAAQ,YAAY,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAClF,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,YAAY,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAClF,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,EAAE,MAAQ,cAAc,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IACpF,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,UAAU,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAChF,EAAE,MAAQ,QAAQ,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC9E,EAAE,MAAQ,YAAY,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAClF,EAAE,MAAQ,WAAW,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,EAClF;AACD;;;ADDA,IAAM,YAAY,cAAc,IAAI,IAAI,KAAK,YAAY,GAAG,CAAC;AAQtD,IAAM,mBAAmB,CAAC,eAAuB,aAA6B;AAGpF,QAAM,gBAAgB,UAAU,SAAS,MAAM,IAAI,mBAAmB;AACtE,SAAO,KAAK,WAAW,eAAe,eAAe,QAAQ;AAC9D;AAKO,IAAM,WAAW,iBAAkB;","names":[]}
@@ -0,0 +1,11 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ type Props = HTMLAttributes<"nav">;
5
+
6
+ const { class: className, ...rest } = Astro.props;
7
+ ---
8
+
9
+ <nav aria-label="breadcrumb" class={className} {...rest}>
10
+ <slot />
11
+ </nav>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import Dots from "@tabler/icons/outline/dots.svg";
3
+ import type { HTMLAttributes } from "astro/types";
4
+ import { tv } from "tailwind-variants";
5
+
6
+ type Props = HTMLAttributes<"span">;
7
+
8
+ const breadcrumbEllipsis = tv({ base: "flex size-6 items-center justify-center [&>svg]:size-4" });
9
+
10
+ const { class: className, ...rest } = Astro.props;
11
+ ---
12
+
13
+ <span
14
+ role="presentation"
15
+ aria-hidden="true"
16
+ class={breadcrumbEllipsis({ class: className })}
17
+ {...rest}
18
+ >
19
+ <Dots />
20
+ <span class="sr-only">More</span>
21
+ </span>
@@ -0,0 +1,14 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"li">;
6
+
7
+ const breadcrumbItem = tv({ base: "inline-flex items-center gap-1.5" });
8
+
9
+ const { class: className, ...rest } = Astro.props;
10
+ ---
11
+
12
+ <li class={breadcrumbItem({ class: className })} {...rest}>
13
+ <slot />
14
+ </li>
@@ -0,0 +1,20 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"a"> & {
6
+ asChild?: boolean;
7
+ };
8
+
9
+ const breadcrumbLink = tv({ base: "hover:text-foreground transition-colors" });
10
+
11
+ const { class: className, asChild = false, ...rest } = Astro.props;
12
+ ---
13
+
14
+ {asChild ? (
15
+ <slot />
16
+ ) : (
17
+ <a class={breadcrumbLink({ class: className })} {...rest}>
18
+ <slot />
19
+ </a>
20
+ )}
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"ol">;
6
+
7
+ const breadcrumbList = tv({
8
+ base: "text-muted-foreground flex flex-wrap items-center gap-1.5 break-words sm:gap-2",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <ol class={breadcrumbList({ class: className })} {...rest}>
15
+ <slot />
16
+ </ol>
@@ -0,0 +1,20 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"span">;
6
+
7
+ const breadcrumbPage = tv({ base: "text-foreground font-normal" });
8
+
9
+ const { class: className, ...rest } = Astro.props;
10
+ ---
11
+
12
+ <span
13
+ role="link"
14
+ aria-disabled="true"
15
+ aria-current="page"
16
+ class={breadcrumbPage({ class: className })}
17
+ {...rest}
18
+ >
19
+ <slot />
20
+ </span>
@@ -0,0 +1,22 @@
1
+ ---
2
+ import ChevronRight from "@tabler/icons/outline/chevron-right.svg";
3
+ import type { HTMLAttributes } from "astro/types";
4
+ import { tv } from "tailwind-variants";
5
+
6
+ type Props = HTMLAttributes<"li">;
7
+
8
+ const breadcrumbSeparator = tv({ base: "[&>svg]:size-4" });
9
+
10
+ const { class: className, ...rest } = Astro.props;
11
+ ---
12
+
13
+ <li
14
+ role="presentation"
15
+ aria-hidden="true"
16
+ class={breadcrumbSeparator({ class: className })}
17
+ {...rest}
18
+ >
19
+ <slot>
20
+ <ChevronRight />
21
+ </slot>
22
+ </li>
@@ -0,0 +1,27 @@
1
+ import Breadcrumb from "./Breadcrumb.astro";
2
+ import BreadcrumbList from "./BreadcrumbList.astro";
3
+ import BreadcrumbEllipsis from "./BreadcrumbEllipsis.astro";
4
+ import BreadcrumbItem from "./BreadcrumbItem.astro";
5
+ import BreadcrumbLink from "./BreadcrumbLink.astro";
6
+ import BreadcrumbSeparator from "./BreadcrumbSeparator.astro";
7
+ import BreadcrumbPage from "./BreadcrumbPage.astro";
8
+
9
+ export {
10
+ Breadcrumb,
11
+ BreadcrumbList,
12
+ BreadcrumbEllipsis,
13
+ BreadcrumbItem,
14
+ BreadcrumbLink,
15
+ BreadcrumbSeparator,
16
+ BreadcrumbPage,
17
+ };
18
+
19
+ export default {
20
+ Root: Breadcrumb,
21
+ List: BreadcrumbList,
22
+ Ellipsis: BreadcrumbEllipsis,
23
+ Item: BreadcrumbItem,
24
+ Link: BreadcrumbLink,
25
+ Separator: BreadcrumbSeparator,
26
+ Page: BreadcrumbPage,
27
+ };
@@ -0,0 +1,355 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ type Props = HTMLAttributes<"div"> & {
5
+ /**
6
+ * When true, the dropdown will open on hover in addition to click
7
+ */
8
+ openOnHover?: boolean;
9
+ /**
10
+ * Time in milliseconds to wait before closing when hover open is enabled
11
+ * @default 200
12
+ */
13
+ closeDelay?: number;
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ children: any;
16
+ };
17
+
18
+ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Astro.props;
19
+ ---
20
+
21
+ <div
22
+ class:list={["starwind-dropdown", "relative", className]}
23
+ data-open-on-hover={openOnHover ? "true" : undefined}
24
+ data-close-delay={closeDelay}
25
+ {...rest}
26
+ >
27
+ <slot />
28
+ </div>
29
+
30
+ <script>
31
+ class DropdownHandler {
32
+ private dropdown: HTMLElement;
33
+ private trigger: HTMLButtonElement | null;
34
+ private content: HTMLElement | null;
35
+ private items: HTMLElement[] = [];
36
+ private currentFocusIndex: number = -1;
37
+ private isOpen: boolean = false;
38
+ private animationDuration = 150;
39
+ private openOnHover: boolean;
40
+ private closeDelay: number;
41
+ private closeTimerRef: number | null = null;
42
+
43
+ constructor(dropdown: HTMLElement, dropdownIdx: number) {
44
+ this.dropdown = dropdown;
45
+ this.openOnHover = dropdown.getAttribute("data-open-on-hover") === "true";
46
+ this.closeDelay = parseInt(dropdown.getAttribute("data-close-delay") || "200");
47
+
48
+ // Get the temporary trigger element
49
+ const tempTrigger = dropdown.querySelector(".starwind-dropdown-trigger") as HTMLElement;
50
+
51
+ // if trigger is set with asChild, use the first child element for trigger button
52
+ if (tempTrigger?.hasAttribute("data-as-child")) {
53
+ this.trigger = tempTrigger.firstElementChild as HTMLButtonElement;
54
+ } else {
55
+ this.trigger = tempTrigger as HTMLButtonElement;
56
+ }
57
+
58
+ this.content = dropdown.querySelector(".starwind-dropdown-content");
59
+
60
+ if (!this.trigger || !this.content) return;
61
+
62
+ // Get animation duration from inline styles if available
63
+ const animationDurationString = this.content.style.animationDuration;
64
+ if (animationDurationString.endsWith("ms")) {
65
+ this.animationDuration = parseFloat(animationDurationString);
66
+ } else if (animationDurationString.endsWith("s")) {
67
+ this.animationDuration = parseFloat(animationDurationString) * 1000;
68
+ }
69
+
70
+ this.init(dropdownIdx);
71
+ }
72
+
73
+ private init(dropdownIdx: number) {
74
+ this.setupAccessibility(dropdownIdx);
75
+ this.setupEvents();
76
+ }
77
+
78
+ private setupAccessibility(dropdownIdx: number) {
79
+ if (!this.trigger || !this.content) return;
80
+
81
+ // Generate unique IDs for accessibility
82
+ this.trigger.id = `starwind-dropdown${dropdownIdx}-trigger`;
83
+ this.content.id = `starwind-dropdown${dropdownIdx}-content`;
84
+
85
+ // Set up additional ARIA attributes
86
+ this.trigger.setAttribute("aria-controls", this.content.id);
87
+ this.content.setAttribute("aria-labelledby", this.trigger.id);
88
+ }
89
+
90
+ private setupEvents() {
91
+ if (!this.trigger || !this.content) return;
92
+
93
+ // Handle trigger click
94
+ this.trigger.addEventListener("click", (e) => {
95
+ e.preventDefault();
96
+ this.toggleDropdown();
97
+ });
98
+
99
+ // Handle keyboard navigation
100
+ this.trigger.addEventListener("keydown", (e) => {
101
+ if (e.key === "Enter" || e.key === " ") {
102
+ e.preventDefault();
103
+ this.toggleDropdown();
104
+ } else if (e.key === "Escape" && this.isOpen) {
105
+ e.preventDefault();
106
+ this.closeDropdown();
107
+ } else if (this.isOpen && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
108
+ e.preventDefault();
109
+ this.updateDropdownItems();
110
+ if (e.key === "ArrowDown") {
111
+ this.focusItem(0); // Focus first item when opening with arrow down
112
+ } else {
113
+ this.focusItem(this.items.length - 1); // Focus last item when opening with arrow up
114
+ }
115
+ }
116
+ });
117
+
118
+ // Close dropdown when clicking outside for mouse
119
+ document.addEventListener("pointerdown", (e) => {
120
+ if (this.isOpen && !this.dropdown.contains(e.target as Node)) {
121
+ // only call handler if it's the left button (mousedown gets triggered by all mouse buttons)
122
+ // but not when the control key is pressed (avoiding MacOS right click); also not for touch
123
+ // devices because that would open the menu on scroll. (pen devices behave as touch on iOS).
124
+ if (e.button === 0 && e.ctrlKey === false && e.pointerType === "mouse") {
125
+ this.closeDropdown();
126
+ }
127
+ }
128
+ });
129
+
130
+ // Handle click outside select content to close for mobile
131
+ document.addEventListener("click", (e) => {
132
+ if (
133
+ !(this.trigger?.contains(e.target as Node) || this.content?.contains(e.target as Node)) &&
134
+ this.isOpen
135
+ ) {
136
+ this.closeDropdown();
137
+ }
138
+ });
139
+
140
+ // Handle keyboard navigation and item selection within dropdown
141
+ this.content.addEventListener("keydown", (e) => {
142
+ if (e.key === "Escape") {
143
+ e.preventDefault();
144
+ this.closeDropdown();
145
+ this.trigger?.focus();
146
+ } else if (this.isOpen) {
147
+ this.handleMenuKeydown(e);
148
+ }
149
+ });
150
+
151
+ // Handle item selection
152
+ this.content.addEventListener("click", (e) => {
153
+ const target = e.target as HTMLElement;
154
+ const item = target.closest('[role="menuitem"]');
155
+ if (item && !(item as HTMLElement).hasAttribute("data-disabled")) {
156
+ // Close the dropdown after item selection
157
+ this.closeDropdown();
158
+ console.log("click closing");
159
+ }
160
+ });
161
+
162
+ // Handle hover on dropdown items
163
+ this.content.addEventListener("mouseover", (e) => {
164
+ const target = e.target as HTMLElement;
165
+ const menuItem = target.closest('[role="menuitem"]');
166
+ if (menuItem && menuItem instanceof HTMLElement && this.isOpen === true) {
167
+ // Update items list before focusing to ensure the index is correct
168
+ this.updateDropdownItems();
169
+
170
+ // Focus the item when hovering
171
+ menuItem.focus();
172
+
173
+ // Update the current focus index
174
+ this.currentFocusIndex = this.items.indexOf(menuItem);
175
+ }
176
+ });
177
+
178
+ if (this.openOnHover) {
179
+ this.trigger.addEventListener("pointerenter", (e) => {
180
+ if (e.pointerType !== "mouse") return;
181
+ if (!this.isOpen) {
182
+ this.openDropdown();
183
+ } else {
184
+ // If the dropdown is already open, make sure to clear any close timer
185
+ this.clearCloseTimer();
186
+ }
187
+ });
188
+
189
+ this.dropdown.addEventListener("pointerleave", (e) => {
190
+ if (e.pointerType !== "mouse") return;
191
+ if (this.isOpen) {
192
+ this.closeDropdownDelayed();
193
+ }
194
+ });
195
+
196
+ this.content.addEventListener("pointerenter", (e) => {
197
+ if (e.pointerType !== "mouse") return;
198
+ // If the user moves the mouse to the content, cancel the close timer
199
+ this.clearCloseTimer();
200
+ });
201
+ }
202
+ }
203
+
204
+ private handleMenuKeydown(e: KeyboardEvent) {
205
+ // Make sure we've got an updated list of menu items
206
+ this.updateDropdownItems();
207
+
208
+ // Skip if no items
209
+ if (this.items.length === 0) return;
210
+
211
+ const currentIdx = this.currentFocusIndex;
212
+
213
+ switch (e.key) {
214
+ case "ArrowDown":
215
+ e.preventDefault();
216
+ this.focusItem(currentIdx === -1 ? 0 : currentIdx + 1);
217
+ break;
218
+ case "ArrowUp":
219
+ e.preventDefault();
220
+ this.focusItem(currentIdx === -1 ? this.items.length - 1 : currentIdx - 1);
221
+ break;
222
+ case "Home":
223
+ e.preventDefault();
224
+ this.focusItem(0);
225
+ break;
226
+ case "End":
227
+ e.preventDefault();
228
+ this.focusItem(this.items.length - 1);
229
+ break;
230
+ case "Enter":
231
+ case " ":
232
+ if (currentIdx !== -1) {
233
+ e.preventDefault();
234
+ this.items[currentIdx].click();
235
+ }
236
+ break;
237
+ }
238
+ }
239
+
240
+ private updateDropdownItems() {
241
+ if (!this.content) return;
242
+ // Get all interactive menuitem elements
243
+ this.items = Array.from(
244
+ this.content.querySelectorAll('[role="menuitem"]:not([data-disabled="true"])'),
245
+ ) as HTMLElement[];
246
+ }
247
+
248
+ private focusItem(idx: number) {
249
+ // Ensure the index wraps around properly
250
+ const targetIdx = (idx + this.items.length) % this.items.length;
251
+
252
+ if (this.items[targetIdx]) {
253
+ this.items[targetIdx].focus();
254
+ this.currentFocusIndex = targetIdx;
255
+ }
256
+ }
257
+
258
+ private toggleDropdown() {
259
+ if (this.isOpen) {
260
+ this.closeDropdown();
261
+ } else {
262
+ this.openDropdown();
263
+ }
264
+ }
265
+
266
+ private openDropdown() {
267
+ if (!this.content || !this.trigger || this.trigger.disabled) return;
268
+
269
+ this.isOpen = true;
270
+ this.content.setAttribute("data-state", "open");
271
+ this.trigger.setAttribute("aria-expanded", "true");
272
+ this.content.style.removeProperty("display");
273
+
274
+ // Update the list of dropdown items
275
+ this.updateDropdownItems();
276
+
277
+ // Reset focus index when opening
278
+ this.currentFocusIndex = -1;
279
+
280
+ this.positionContent();
281
+ }
282
+
283
+ private closeDropdown() {
284
+ if (!this.content || !this.trigger) return;
285
+
286
+ this.isOpen = false;
287
+ this.content.setAttribute("data-state", "closed");
288
+
289
+ // Set focus back on trigger
290
+ requestAnimationFrame(() => {
291
+ if (!this.trigger) return;
292
+ this.trigger.focus();
293
+ });
294
+
295
+ // Give the content time to animate before hiding
296
+ setTimeout(() => {
297
+ if (!this.content) return;
298
+ this.content.style.display = "none";
299
+ }, this.animationDuration - 10);
300
+
301
+ this.trigger.setAttribute("aria-expanded", "false");
302
+
303
+ // Reset focus index when closing
304
+ this.currentFocusIndex = -1;
305
+ }
306
+
307
+ private closeDropdownDelayed() {
308
+ if (!this.content || !this.trigger) return;
309
+
310
+ // Clear any existing close timer
311
+ this.clearCloseTimer();
312
+
313
+ // Set a new timer to close the dropdown after the delay
314
+ this.closeTimerRef = window.setTimeout(() => {
315
+ if (this.isOpen) {
316
+ this.closeDropdown();
317
+ }
318
+ this.closeTimerRef = null;
319
+ }, this.closeDelay);
320
+ }
321
+
322
+ private clearCloseTimer() {
323
+ if (this.closeTimerRef !== null) {
324
+ window.clearTimeout(this.closeTimerRef);
325
+ this.closeTimerRef = null;
326
+ }
327
+ }
328
+
329
+ private positionContent() {
330
+ if (!this.content || !this.trigger) return;
331
+
332
+ // Set content width to match trigger width
333
+ this.content.style.width = "var(--starwind-dropdown-trigger-width)";
334
+ this.content.style.setProperty(
335
+ "--starwind-dropdown-trigger-width",
336
+ `${this.trigger.offsetWidth}px`,
337
+ );
338
+ }
339
+ }
340
+
341
+ // Store instances in a WeakMap to avoid memory leaks
342
+ const dropdownInstances = new WeakMap<HTMLElement, DropdownHandler>();
343
+
344
+ // Initialize dropdowns
345
+ const initDropdowns = () => {
346
+ document.querySelectorAll(".starwind-dropdown").forEach((dropdown, idx) => {
347
+ if (dropdown instanceof HTMLElement && !dropdownInstances.has(dropdown)) {
348
+ dropdownInstances.set(dropdown, new DropdownHandler(dropdown, idx));
349
+ }
350
+ });
351
+ };
352
+
353
+ initDropdowns();
354
+ document.addEventListener("astro:after-swap", initDropdowns);
355
+ </script>
@@ -0,0 +1,80 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
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;
26
+ };
27
+
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
+ "animate-in fade-in-0 zoom-in-95",
33
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
34
+ "absolute will-change-transform",
35
+ ],
36
+ variants: {
37
+ side: {
38
+ bottom: "slide-in-from-top-2 data-[state=closed]:slide-out-to-top-2 top-full",
39
+ top: "slide-in-from-bottom-2 data-[state=closed]:slide-out-to-bottom-2 bottom-full",
40
+ },
41
+ align: {
42
+ start: "slide-in-from-left-1 data-[state=closed]:slide-out-to-left-1 left-0",
43
+ center: "left-1/2 -translate-x-1/2",
44
+ end: "slide-in-from-right-1 data-[state=closed]:slide-out-to-right-1 right-0",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ side: "bottom",
49
+ align: "start",
50
+ },
51
+ });
52
+
53
+ const {
54
+ class: className,
55
+ side = "bottom",
56
+ align = "start",
57
+ sideOffset = 4,
58
+ animationDuration = 150,
59
+ ...rest
60
+ } = Astro.props;
61
+ ---
62
+
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}
78
+ >
79
+ <slot />
80
+ </div>
@@ -0,0 +1,47 @@
1
+ ---
2
+ import type { HTMLTag, Polymorphic } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
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;
14
+ };
15
+
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
+ },
34
+ });
35
+
36
+ const { class: className, inset = false, disabled = false, as: Tag = "div", ...rest } = Astro.props;
37
+ ---
38
+
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}
45
+ >
46
+ <slot />
47
+ </Tag>
@@ -0,0 +1,29 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"div"> & {
6
+ /**
7
+ * Whether the label is inset (has left padding)
8
+ */
9
+ inset?: boolean;
10
+ };
11
+
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
+ },
22
+ });
23
+
24
+ const { class: className, inset = false, ...rest } = Astro.props;
25
+ ---
26
+
27
+ <div class={dropdownLabel({ inset, class: className })} {...rest}>
28
+ <slot />
29
+ </div>
@@ -0,0 +1,20 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"div">;
6
+
7
+ const dropdownSeparator = tv({
8
+ base: "bg-muted -mx-1 my-1 h-px",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <div
15
+ class={dropdownSeparator({ class: className })}
16
+ role="separator"
17
+ aria-orientation="horizontal"
18
+ {...rest}
19
+ >
20
+ </div>
@@ -0,0 +1,47 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
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;
10
+ };
11
+
12
+ const dropdownTrigger = tv({
13
+ base: [
14
+ "starwind-dropdown-trigger",
15
+ "inline-flex items-center justify-center",
16
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
17
+ ],
18
+ });
19
+
20
+ const { class: className, asChild = false, ...rest } = Astro.props;
21
+
22
+ // Get the first child element if asChild is true
23
+ let hasChildren = false;
24
+ if (Astro.slots.has("default")) {
25
+ hasChildren = true;
26
+ }
27
+ ---
28
+
29
+ {
30
+ asChild && hasChildren ? (
31
+ <div class="starwind-dropdown-trigger" data-as-child>
32
+ <slot />
33
+ </div>
34
+ ) : (
35
+ <button
36
+ class={dropdownTrigger({ class: className })}
37
+ type="button"
38
+ role="button"
39
+ aria-haspopup="true"
40
+ aria-expanded="false"
41
+ data-state="closed"
42
+ {...rest}
43
+ >
44
+ <slot />
45
+ </button>
46
+ )
47
+ }
@@ -0,0 +1,24 @@
1
+ import Dropdown from "./Dropdown.astro";
2
+ import DropdownTrigger from "./DropdownTrigger.astro";
3
+ import DropdownContent from "./DropdownContent.astro";
4
+ import DropdownItem from "./DropdownItem.astro";
5
+ import DropdownLabel from "./DropdownLabel.astro";
6
+ import DropdownSeparator from "./DropdownSeparator.astro";
7
+
8
+ export {
9
+ Dropdown,
10
+ DropdownTrigger,
11
+ DropdownContent,
12
+ DropdownItem,
13
+ DropdownLabel,
14
+ DropdownSeparator,
15
+ };
16
+
17
+ export default {
18
+ Root: Dropdown,
19
+ Trigger: DropdownTrigger,
20
+ Content: DropdownContent,
21
+ Item: DropdownItem,
22
+ Label: DropdownLabel,
23
+ Separator: DropdownSeparator,
24
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starwind-ui/core",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "Starwind UI core components and registry",
5
5
  "license": "MIT",
6
6
  "author": {