@starwind-ui/core 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14,14 +14,15 @@ var registry_default = {
14
14
  { name: "button", type: "component", version: "2.0.1", dependencies: [] },
15
15
  { name: "card", type: "component", version: "1.1.0", dependencies: [] },
16
16
  { name: "checkbox", type: "component", version: "1.2.0", dependencies: [] },
17
- { name: "dialog", type: "component", version: "1.1.1", dependencies: [] },
18
- { name: "dropdown", type: "component", version: "1.0.0", dependencies: [] },
17
+ { name: "dialog", type: "component", version: "1.1.2", dependencies: [] },
18
+ { name: "dropdown", type: "component", version: "1.0.2", dependencies: [] },
19
19
  { name: "input", type: "component", version: "1.1.1", dependencies: [] },
20
20
  { name: "label", type: "component", version: "1.1.1", dependencies: [] },
21
21
  { name: "pagination", type: "component", version: "2.0.1", dependencies: [] },
22
- { name: "select", type: "component", version: "1.2.0", dependencies: [] },
22
+ { name: "select", type: "component", version: "1.3.0", dependencies: [] },
23
23
  { name: "switch", type: "component", version: "1.1.0", dependencies: [] },
24
- { name: "tabs", type: "component", version: "1.1.1", dependencies: [] },
24
+ { name: "table", type: "component", version: "1.0.0", dependencies: [] },
25
+ { name: "tabs", type: "component", version: "1.2.0", dependencies: [] },
25
26
  { name: "textarea", type: "component", version: "1.1.1", dependencies: [] },
26
27
  { name: "tooltip", type: "component", version: "1.1.1", dependencies: [] }
27
28
  ]
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\": \"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.0\", \"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":[]}
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.2\", \"dependencies\": [] },\n\t\t{ \"name\": \"dropdown\", \"type\": \"component\", \"version\": \"1.0.2\", \"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.3.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"switch\", \"type\": \"component\", \"version\": \"1.1.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"table\", \"type\": \"component\", \"version\": \"1.0.0\", \"dependencies\": [] },\n\t\t{ \"name\": \"tabs\", \"type\": \"component\", \"version\": \"1.2.0\", \"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,SAAS,MAAQ,aAAa,SAAW,SAAS,cAAgB,CAAC,EAAE;AAAA,IAC/E,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;;;ADFA,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":[]}
@@ -90,7 +90,11 @@ const { class: className, ...rest } = Astro.props;
90
90
  // Add click handlers to all close buttons
91
91
  this.closeButtons?.forEach((button) => {
92
92
  button.addEventListener("click", () => {
93
- this.close();
93
+ // Only close if this is the topmost dialog
94
+ const openDialogs = document.querySelectorAll("dialog[open]");
95
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
96
+ this.close();
97
+ }
94
98
  });
95
99
  });
96
100
 
@@ -104,7 +108,11 @@ const { class: className, ...rest } = Astro.props;
104
108
  e.clientY <= dialogDimensions.bottom;
105
109
 
106
110
  if (!clickedInDialog) {
107
- this.close();
111
+ // Only close if this is the topmost dialog
112
+ const openDialogs = document.querySelectorAll("dialog[open]");
113
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
114
+ this.close();
115
+ }
108
116
  }
109
117
  });
110
118
 
@@ -113,7 +121,11 @@ const { class: className, ...rest } = Astro.props;
113
121
  if (e.key === "Escape") {
114
122
  // prevent default dialog closing behavior so we can add closing animation
115
123
  e.preventDefault();
116
- this.close();
124
+ // Only close if this is the topmost dialog
125
+ const openDialogs = document.querySelectorAll("dialog[open]");
126
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
127
+ this.close();
128
+ }
117
129
  }
118
130
  });
119
131
 
@@ -130,7 +142,11 @@ const { class: className, ...rest } = Astro.props;
130
142
  */
131
143
  if (form.method === "dialog") {
132
144
  e.preventDefault();
133
- this.close();
145
+ // Only close if this is the topmost dialog
146
+ const openDialogs = document.querySelectorAll("dialog[open]");
147
+ if (openDialogs.length > 0 && openDialogs[openDialogs.length - 1] === this.dialog) {
148
+ this.close();
149
+ }
134
150
  }
135
151
  });
136
152
  });
@@ -2,7 +2,6 @@
2
2
  import type { HTMLAttributes } from "astro/types";
3
3
 
4
4
  type Props = HTMLAttributes<"div"> & {
5
- name?: string;
6
5
  /**
7
6
  * When true, the dropdown will open on hover in addition to click
8
7
  */
@@ -16,12 +15,11 @@ type Props = HTMLAttributes<"div"> & {
16
15
  children: any;
17
16
  };
18
17
 
19
- const { class: className, name, openOnHover = false, closeDelay = 200, ...rest } = Astro.props;
18
+ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Astro.props;
20
19
  ---
21
20
 
22
21
  <div
23
22
  class:list={["starwind-dropdown", "relative", className]}
24
- data-name={name}
25
23
  data-open-on-hover={openOnHover ? "true" : undefined}
26
24
  data-close-delay={closeDelay}
27
25
  {...rest}
@@ -37,6 +35,7 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
37
35
  private items: HTMLElement[] = [];
38
36
  private currentFocusIndex: number = -1;
39
37
  private isOpen: boolean = false;
38
+ private isClosing: boolean = false;
40
39
  private animationDuration = 150;
41
40
  private openOnHover: boolean;
42
41
  private closeDelay: number;
@@ -117,9 +116,24 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
117
116
  }
118
117
  });
119
118
 
120
- // Close dropdown when clicking outside
119
+ // Close dropdown when clicking outside for mouse
121
120
  document.addEventListener("pointerdown", (e) => {
122
121
  if (this.isOpen && !this.dropdown.contains(e.target as Node)) {
122
+ // only call handler if it's the left button (mousedown gets triggered by all mouse buttons)
123
+ // but not when the control key is pressed (avoiding MacOS right click); also not for touch
124
+ // devices because that would open the menu on scroll. (pen devices behave as touch on iOS).
125
+ if (e.button === 0 && e.ctrlKey === false && e.pointerType === "mouse") {
126
+ this.closeDropdown();
127
+ }
128
+ }
129
+ });
130
+
131
+ // Handle click outside select content to close for mobile
132
+ document.addEventListener("click", (e) => {
133
+ if (
134
+ !(this.trigger?.contains(e.target as Node) || this.content?.contains(e.target as Node)) &&
135
+ this.isOpen
136
+ ) {
123
137
  this.closeDropdown();
124
138
  }
125
139
  });
@@ -142,6 +156,7 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
142
156
  if (item && !(item as HTMLElement).hasAttribute("data-disabled")) {
143
157
  // Close the dropdown after item selection
144
158
  this.closeDropdown();
159
+ console.log("click closing");
145
160
  }
146
161
  });
147
162
 
@@ -162,8 +177,9 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
162
177
  });
163
178
 
164
179
  if (this.openOnHover) {
165
- // Use mouseenter instead of mouseover to avoid flickering when moving between child elements
166
- this.trigger.addEventListener("mouseenter", () => {
180
+ this.trigger.addEventListener("pointerenter", (e) => {
181
+ if (e.pointerType !== "mouse") return;
182
+ if (this.isClosing) return;
167
183
  if (!this.isOpen) {
168
184
  this.openDropdown();
169
185
  } else {
@@ -172,20 +188,18 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
172
188
  }
173
189
  });
174
190
 
175
- // Use mouseleave instead of mouseout to only trigger when leaving the entire dropdown
176
- this.dropdown.addEventListener("mouseleave", () => {
191
+ this.dropdown.addEventListener("pointerleave", (e) => {
192
+ if (e.pointerType !== "mouse") return;
177
193
  if (this.isOpen) {
178
194
  this.closeDropdownDelayed();
179
195
  }
180
196
  });
181
197
 
182
- // When content is available, also add mouseenter to it to cancel close timer
183
- if (this.content) {
184
- this.content.addEventListener("mouseenter", () => {
185
- // If the user moves the mouse to the content, cancel the close timer
186
- this.clearCloseTimer();
187
- });
188
- }
198
+ this.content.addEventListener("pointerenter", (e) => {
199
+ if (e.pointerType !== "mouse") return;
200
+ // If the user moves the mouse to the content, cancel the close timer
201
+ this.clearCloseTimer();
202
+ });
189
203
  }
190
204
  }
191
205
 
@@ -252,6 +266,7 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
252
266
  }
253
267
 
254
268
  private openDropdown() {
269
+ if (this.isClosing) return;
255
270
  if (!this.content || !this.trigger || this.trigger.disabled) return;
256
271
 
257
272
  this.isOpen = true;
@@ -271,6 +286,7 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
271
286
  private closeDropdown() {
272
287
  if (!this.content || !this.trigger) return;
273
288
 
289
+ this.isClosing = true;
274
290
  this.isOpen = false;
275
291
  this.content.setAttribute("data-state", "closed");
276
292
 
@@ -284,6 +300,7 @@ const { class: className, name, openOnHover = false, closeDelay = 200, ...rest }
284
300
  setTimeout(() => {
285
301
  if (!this.content) return;
286
302
  this.content.style.display = "none";
303
+ this.isClosing = false;
287
304
  }, this.animationDuration - 10);
288
305
 
289
306
  this.trigger.setAttribute("aria-expanded", "false");
@@ -13,7 +13,7 @@ const dropdownTrigger = tv({
13
13
  base: [
14
14
  "starwind-dropdown-trigger",
15
15
  "inline-flex items-center justify-center",
16
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
16
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
17
17
  ],
18
18
  });
19
19
 
@@ -28,14 +28,13 @@ if (Astro.slots.has("default")) {
28
28
 
29
29
  {
30
30
  asChild && hasChildren ? (
31
- <div class="starwind-dropdown-trigger" data-as-child>
31
+ <div class={`starwind-dropdown-trigger ${className}`} data-as-child>
32
32
  <slot />
33
33
  </div>
34
34
  ) : (
35
35
  <button
36
36
  class={dropdownTrigger({ class: className })}
37
37
  type="button"
38
- role="button"
39
38
  aria-haspopup="true"
40
39
  aria-expanded="false"
41
40
  data-state="closed"
@@ -2,15 +2,27 @@
2
2
  import type { HTMLAttributes } from "astro/types";
3
3
 
4
4
  type Props = HTMLAttributes<"div"> & {
5
+ /**
6
+ * The name of the select field for form handling
7
+ */
5
8
  name?: string;
9
+ /**
10
+ * The value of the item that should be selected by default
11
+ */
12
+ defaultValue?: string;
6
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
14
  children: any;
8
15
  };
9
16
 
10
- const { class: className, name, ...rest } = Astro.props;
17
+ const { class: className, name, defaultValue, ...rest } = Astro.props;
11
18
  ---
12
19
 
13
- <div class:list={["starwind-select", "relative", className]} data-name={name} {...rest}>
20
+ <div
21
+ class:list={["starwind-select", "relative", className]}
22
+ data-name={name}
23
+ data-value={defaultValue}
24
+ {...rest}
25
+ >
14
26
  <slot />
15
27
  </div>
16
28
 
@@ -50,6 +62,7 @@ const { class: className, name, ...rest } = Astro.props;
50
62
  this.setupAccessibility(selectIdx);
51
63
  this.setupEvents();
52
64
  this.setupSelectField();
65
+ this.setInitialState();
53
66
  }
54
67
 
55
68
  private setupSelectField() {
@@ -398,6 +411,21 @@ const { class: className, name, ...rest } = Astro.props;
398
411
  this.closeSelect();
399
412
  }
400
413
 
414
+ /**
415
+ * Sets the initial state based on the default value attribute
416
+ */
417
+ private setInitialState(): void {
418
+ const defaultValue = this.select.dataset.value;
419
+ if (defaultValue) {
420
+ const item = this.content?.querySelector(`[data-value="${defaultValue}"]`);
421
+
422
+ if (item && item instanceof HTMLElement) {
423
+ this.handleSelection(item);
424
+ this.selectedItem = item;
425
+ }
426
+ }
427
+ }
428
+
401
429
  /**
402
430
  * TODO: add position logic to avoid collisions with window boundary
403
431
  * It will need to switch to top or bottom depending on space available
@@ -8,6 +8,11 @@ type Props = HTMLAttributes<"div"> & {
8
8
  * @default bottom
9
9
  */
10
10
  side?: "top" | "bottom";
11
+ /**
12
+ * Alignment of the dropdown
13
+ * @default start
14
+ */
15
+ align?: "start" | "center" | "end";
11
16
  /**
12
17
  * Offset distance in pixels
13
18
  * @default 4
@@ -25,17 +30,20 @@ const selectContent = tv({
25
30
  "starwind-select-content",
26
31
  "bg-popover text-popover-foreground absolute z-50 min-w-[8rem] rounded-md border shadow-md",
27
32
  "fade-in-0 zoom-in-95 animate-in overflow-hidden will-change-transform",
28
- "data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:animate-out",
29
- "left-0",
33
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
30
34
  ],
31
35
  variants: {
32
36
  side: {
33
- bottom:
34
- "data-[side=bottom]:slide-in-from-top-4 data-[side=bottom]:top-(--select-content-offset)",
35
- top: "data-[side=top]:slide-in-from-bottom-4 data-[side=top]:bottom-(--select-content-offset)",
37
+ bottom: "slide-in-from-top-2 data-[state=closed]:slide-out-to-top-2 top-full",
38
+ top: "slide-in-from-bottom-2 data-[state=closed]:slide-out-to-bottom-2 bottom-full",
39
+ },
40
+ align: {
41
+ start: "slide-in-from-left-1 data-[state=closed]:slide-out-to-left-1 left-0",
42
+ center: "left-1/2 -translate-x-1/2",
43
+ end: "slide-in-from-right-1 data-[state=closed]:slide-out-to-right-1 right-0",
36
44
  },
37
45
  },
38
- defaultVariants: { side: "bottom" },
46
+ defaultVariants: { side: "bottom", align: "start" },
39
47
  });
40
48
 
41
49
  const selectContentInner = tv({
@@ -45,6 +53,7 @@ const selectContentInner = tv({
45
53
  const {
46
54
  class: className,
47
55
  side = "bottom",
56
+ align = "start",
48
57
  sideOffset = 4,
49
58
  animationDuration = 150,
50
59
  ...rest
@@ -52,16 +61,18 @@ const {
52
61
  ---
53
62
 
54
63
  <div
55
- class={selectContent({ side, class: className })}
64
+ class={selectContent({ side, align, class: className })}
56
65
  role="listbox"
57
66
  data-side={side}
67
+ data-align={align}
58
68
  data-state="closed"
59
69
  tabindex="-1"
60
70
  style={{
61
- "--select-content-offset": `calc(100% + ${sideOffset}px)`,
62
71
  // hide the content initially. Script will remove this
63
72
  display: "none",
64
73
  animationDuration: `${animationDuration}ms`,
74
+ marginTop: side === "bottom" ? `${sideOffset}px` : undefined,
75
+ marginBottom: side === "top" ? `${sideOffset}px` : undefined,
65
76
  }}
66
77
  {...rest}
67
78
  >
@@ -0,0 +1,18 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"table"> & VariantProps<typeof table>;
6
+
7
+ const table = tv({
8
+ base: "w-full caption-bottom text-sm",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <div data-table-container class="relative w-full overflow-x-auto">
15
+ <table data-table class={table({ class: className })} {...rest} role="table">
16
+ <slot />
17
+ </table>
18
+ </div>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"tbody"> & VariantProps<typeof tableBody>;
6
+
7
+ const tableBody = tv({
8
+ base: "[&_tr:last-child]:border-0",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <tbody data-table-body class={tableBody({ class: className })} {...rest}>
15
+ <slot />
16
+ </tbody>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"caption"> & VariantProps<typeof tableCaption>;
6
+
7
+ const tableCaption = tv({
8
+ base: "text-muted-foreground mt-4 text-sm",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <caption data-table-caption class={tableCaption({ class: className })} {...rest}>
15
+ <slot />
16
+ </caption>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"td"> & VariantProps<typeof tableCell>;
6
+
7
+ const tableCell = tv({
8
+ base: "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <td data-table-cell class={tableCell({ class: className })} {...rest}>
15
+ <slot />
16
+ </td>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"tfoot"> & VariantProps<typeof tableFoot>;
6
+
7
+ const tableFoot = tv({
8
+ base: "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <tfoot data-table-foot class={tableFoot({ class: className })} {...rest}>
15
+ <slot />
16
+ </tfoot>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"th"> & VariantProps<typeof tableHead>;
6
+
7
+ const tableHead = tv({
8
+ base: "text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <th data-table-head class={tableHead({ class: className })} {...rest} role="columnheader">
15
+ <slot />
16
+ </th>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"thead"> & VariantProps<typeof tableHeader>;
6
+
7
+ const tableHeader = tv({
8
+ base: "[&_tr]:border-b",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <thead data-table-header class={tableHeader({ class: className })} {...rest}>
15
+ <slot />
16
+ </thead>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"tr"> & VariantProps<typeof tableRow>;
6
+
7
+ const tableRow = tv({
8
+ base: "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <tr data-table-row class={tableRow({ class: className })} {...rest} role="row">
15
+ <slot />
16
+ </tr>
@@ -0,0 +1,21 @@
1
+ import Table from "./Table.astro";
2
+ import TableBody from "./TableBody.astro";
3
+ import TableCaption from "./TableCaption.astro";
4
+ import TableCell from "./TableCell.astro";
5
+ import TableFoot from "./TableFoot.astro";
6
+ import TableHeader from "./TableHeader.astro";
7
+ import TableHead from "./TableHead.astro";
8
+ import TableRow from "./TableRow.astro";
9
+
10
+ export { Table, TableBody, TableCaption, TableCell, TableFoot, TableHead, TableHeader, TableRow };
11
+
12
+ export default {
13
+ Root: Table,
14
+ Body: TableBody,
15
+ Caption: TableCaption,
16
+ Cell: TableCell,
17
+ Foot: TableFoot,
18
+ Head: TableHead,
19
+ Header: TableHeader,
20
+ Row: TableRow,
21
+ };
@@ -42,11 +42,13 @@ const { defaultValue, syncKey, class: className, ...rest } = Astro.props;
42
42
  private storageKey: string;
43
43
  private valueToTriggerMap: Map<string, HTMLButtonElement>;
44
44
  private valueToContentMap: Map<string, HTMLElement>;
45
+ private parentHandler: TabsHandler | null = null;
45
46
 
46
- constructor(tabs: HTMLElement, idx: number) {
47
+ constructor(tabs: HTMLElement, idx: number, parentHandler: TabsHandler | null = null) {
47
48
  this.tabs = tabs;
48
- this.triggers = Array.from(tabs.querySelectorAll("[data-tabs-trigger]"));
49
- this.contents = Array.from(tabs.querySelectorAll("[data-tabs-content]"));
49
+ this.parentHandler = parentHandler;
50
+ this.triggers = Array.from(tabs.querySelectorAll(":scope > [data-tabs-list] > [data-tabs-trigger]"));
51
+ this.contents = Array.from(tabs.querySelectorAll(":scope > [data-tabs-content]"));
50
52
  this.tabsId = `starwind-tabs${idx}`;
51
53
  this.syncKey = tabs.dataset.syncKey;
52
54
  this.storageKey = this.syncKey
@@ -228,6 +230,20 @@ const { defaultValue, syncKey, class: className, ...rest } = Astro.props;
228
230
  c.setAttribute("data-state", isActive ? "active" : "inactive");
229
231
  c.hidden = !isActive;
230
232
  });
233
+
234
+ // Initialize any nested tabs in the active content
235
+ if (content.hasAttribute("data-state") && content.getAttribute("data-state") === "active") {
236
+ const nestedTabs = content.querySelectorAll<HTMLElement>(".starwind-tabs");
237
+
238
+ nestedTabs.forEach((nestedTab, nestedIdx) => {
239
+ // Skip tabs that already have instances
240
+ if (!tabInstances.has(nestedTab)) {
241
+ const uniqueIdx = 1000 + nestedIdx;
242
+ const handler = new TabsHandler(nestedTab, uniqueIdx, this);
243
+ tabInstances.set(nestedTab, handler);
244
+ }
245
+ });
246
+ }
231
247
  }
232
248
  }
233
249
 
@@ -235,8 +251,11 @@ const { defaultValue, syncKey, class: className, ...rest } = Astro.props;
235
251
  const tabInstances = new WeakMap<HTMLElement, TabsHandler>();
236
252
 
237
253
  const setupTabs = () => {
254
+ // First handle top-level tabs
238
255
  document.querySelectorAll<HTMLElement>(".starwind-tabs").forEach((tabs, idx) => {
239
- if (!tabInstances.has(tabs)) {
256
+ // Skip tabs that are nested within other tab contents
257
+ const isNested = !!tabs.closest("[data-tabs-content]");
258
+ if (!isNested && !tabInstances.has(tabs)) {
240
259
  tabInstances.set(tabs, new TabsHandler(tabs, idx));
241
260
  }
242
261
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starwind-ui/core",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Starwind UI core components and registry",
5
5
  "license": "MIT",
6
6
  "author": {