@starwind-ui/core 0.0.1 → 0.1.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.
Files changed (70) hide show
  1. package/dist/index.js +86 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/components/accordion/Accordion.astro +248 -0
  4. package/dist/src/components/accordion/AccordionContent.astro +28 -0
  5. package/dist/src/components/accordion/AccordionItem.astro +25 -0
  6. package/dist/src/components/accordion/AccordionTrigger.astro +26 -0
  7. package/dist/src/components/accordion/index.ts +13 -0
  8. package/dist/src/components/alert/Alert.astro +44 -0
  9. package/dist/src/components/alert/AlertDescription.astro +11 -0
  10. package/dist/src/components/alert/AlertTitle.astro +17 -0
  11. package/dist/src/components/alert/index.ts +11 -0
  12. package/dist/src/components/avatar/Avatar.astro +44 -0
  13. package/dist/src/components/avatar/AvatarFallback.astro +16 -0
  14. package/dist/src/components/avatar/AvatarImage.astro +48 -0
  15. package/dist/src/components/avatar/index.ts +11 -0
  16. package/dist/src/components/card/Card.astro +14 -0
  17. package/dist/src/components/card/CardContent.astro +11 -0
  18. package/dist/src/components/card/CardDescription.astro +11 -0
  19. package/dist/src/components/card/CardFooter.astro +11 -0
  20. package/dist/src/components/card/CardHeader.astro +11 -0
  21. package/dist/src/components/card/CardTitle.astro +11 -0
  22. package/dist/src/components/card/index.ts +17 -0
  23. package/dist/src/components/checkbox/Checkbox.astro +105 -0
  24. package/dist/src/components/checkbox/index.ts +5 -0
  25. package/dist/src/components/dialog/Dialog.astro +175 -0
  26. package/dist/src/components/dialog/DialogClose.astro +30 -0
  27. package/dist/src/components/dialog/DialogContent.astro +57 -0
  28. package/dist/src/components/dialog/DialogDescription.astro +11 -0
  29. package/dist/src/components/dialog/DialogFooter.astro +11 -0
  30. package/dist/src/components/dialog/DialogHeader.astro +11 -0
  31. package/dist/src/components/dialog/DialogTitle.astro +16 -0
  32. package/dist/src/components/dialog/DialogTrigger.astro +35 -0
  33. package/dist/src/components/dialog/index.ts +30 -0
  34. package/dist/src/components/input/Input.astro +26 -0
  35. package/dist/src/components/input/index.ts +5 -0
  36. package/dist/src/components/label/Label.astro +25 -0
  37. package/dist/src/components/label/index.ts +5 -0
  38. package/dist/src/components/pagination/Pagination.astro +18 -0
  39. package/dist/src/components/pagination/PaginationContent.astro +13 -0
  40. package/dist/src/components/pagination/PaginationEllipsis.astro +15 -0
  41. package/dist/src/components/pagination/PaginationItem.astro +13 -0
  42. package/dist/src/components/pagination/PaginationLink.astro +50 -0
  43. package/dist/src/components/pagination/PaginationNext.astro +23 -0
  44. package/dist/src/components/pagination/PaginationPrevious.astro +23 -0
  45. package/dist/src/components/pagination/index.ts +27 -0
  46. package/dist/src/components/select/Select.astro +452 -0
  47. package/dist/src/components/select/SelectContent.astro +57 -0
  48. package/dist/src/components/select/SelectGroup.astro +10 -0
  49. package/dist/src/components/select/SelectItem.astro +41 -0
  50. package/dist/src/components/select/SelectLabel.astro +11 -0
  51. package/dist/src/components/select/SelectSeparator.astro +9 -0
  52. package/dist/src/components/select/SelectTrigger.astro +40 -0
  53. package/dist/src/components/select/SelectTypes.ts +7 -0
  54. package/dist/src/components/select/SelectValue.astro +16 -0
  55. package/dist/src/components/select/index.ts +30 -0
  56. package/dist/src/components/switch/Switch.astro +189 -0
  57. package/dist/src/components/switch/SwitchTypes.ts +6 -0
  58. package/dist/src/components/switch/index.ts +6 -0
  59. package/dist/src/components/tabs/Tabs.astro +246 -0
  60. package/dist/src/components/tabs/TabsContent.astro +22 -0
  61. package/dist/src/components/tabs/TabsList.astro +19 -0
  62. package/dist/src/components/tabs/TabsTrigger.astro +27 -0
  63. package/dist/src/components/tabs/index.ts +13 -0
  64. package/dist/src/components/textarea/Textarea.astro +25 -0
  65. package/dist/src/components/textarea/index.ts +5 -0
  66. package/dist/src/components/tooltip/Tooltip.astro +233 -0
  67. package/dist/src/components/tooltip/TooltipContent.astro +76 -0
  68. package/dist/src/components/tooltip/TooltipTrigger.astro +11 -0
  69. package/dist/src/components/tooltip/index.ts +11 -0
  70. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ ---
2
+ import Check from "@tabler/icons/outline/check.svg";
3
+
4
+ import type { HTMLAttributes } from "astro/types";
5
+
6
+ type Props = HTMLAttributes<"div"> & {
7
+ /**
8
+ * The value associated with this select item
9
+ */
10
+ value: string;
11
+ /**
12
+ * Whether this select item is disabled and cannot be selected
13
+ */
14
+ disabled?: boolean;
15
+ };
16
+
17
+ const { class: className, value, disabled, ...rest } = Astro.props;
18
+ ---
19
+
20
+ <div
21
+ class:list={[
22
+ "relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 outline-none select-none",
23
+ "focus:bg-accent focus:text-accent-foreground",
24
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
25
+ "not-aria-selected:[&_svg]:hidden aria-selected:[&_svg]:flex",
26
+ className,
27
+ ]}
28
+ data-value={value}
29
+ data-disabled={disabled}
30
+ aria-selected="false"
31
+ role="option"
32
+ tabindex="0"
33
+ {...rest}
34
+ >
35
+ <span class="absolute left-2 size-3.5 items-center justify-center">
36
+ <Check class="size-4" />
37
+ </span>
38
+ <span>
39
+ <slot />
40
+ </span>
41
+ </div>
@@ -0,0 +1,11 @@
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={["py-1.5 pr-2 pl-8 font-semibold", className]} {...rest}>
10
+ <slot />
11
+ </div>
@@ -0,0 +1,9 @@
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={["bg-muted -mx-1 my-1 h-px", className]} {...rest}></div>
@@ -0,0 +1,40 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ import ChevronDown from "@tabler/icons/outline/chevron-down.svg";
5
+
6
+ type Props = Omit<HTMLAttributes<"button">, "role" | "type"> & {
7
+ /**
8
+ * The content to be rendered inside the select trigger
9
+ */
10
+ children: any;
11
+ /**
12
+ * Whether the select field is required in a form context
13
+ */
14
+ required?: boolean;
15
+ };
16
+
17
+ const { class: className, required = false, ...rest } = Astro.props;
18
+ ---
19
+
20
+ <button
21
+ class:list={[
22
+ "starwind-select-trigger",
23
+ "border-input bg-background text-foreground ring-offset-background flex h-11 items-center justify-between rounded-md border px-3 py-2",
24
+ "focus:outline-outline focus:outline-2 focus:outline-offset-2",
25
+ "disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
26
+ className,
27
+ ]}
28
+ type="button"
29
+ role="combobox"
30
+ aria-label="Select field"
31
+ aria-expanded="false"
32
+ aria-haspopup="listbox"
33
+ aria-autocomplete="none"
34
+ aria-required={required ? "true" : "false"}
35
+ data-state="closed"
36
+ {...rest}
37
+ >
38
+ <slot />
39
+ <ChevronDown class="size-4 opacity-50" />
40
+ </button>
@@ -0,0 +1,7 @@
1
+ export interface SelectChangeEvent extends CustomEvent {
2
+ detail: {
3
+ value: string;
4
+ selectId: string;
5
+ label: string;
6
+ };
7
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ type Props = HTMLAttributes<"span"> & {
5
+ /**
6
+ * The text to display when no value is selected
7
+ */
8
+ placeholder?: string;
9
+ };
10
+
11
+ const { placeholder = "select", ...rest } = Astro.props;
12
+ ---
13
+
14
+ <span class="pointer-events-none" {...rest}>
15
+ {placeholder}
16
+ </span>
@@ -0,0 +1,30 @@
1
+ import Select from "./Select.astro";
2
+ import SelectContent from "./SelectContent.astro";
3
+ import SelectGroup from "./SelectGroup.astro";
4
+ import SelectItem from "./SelectItem.astro";
5
+ import SelectLabel from "./SelectLabel.astro";
6
+ import SelectSeparator from "./SelectSeparator.astro";
7
+ import SelectTrigger from "./SelectTrigger.astro";
8
+ import SelectValue from "./SelectValue.astro";
9
+
10
+ export {
11
+ Select,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ SelectContent,
15
+ SelectGroup,
16
+ SelectLabel,
17
+ SelectItem,
18
+ SelectSeparator,
19
+ };
20
+
21
+ export default {
22
+ Root: Select,
23
+ Trigger: SelectTrigger,
24
+ Value: SelectValue,
25
+ Content: SelectContent,
26
+ Group: SelectGroup,
27
+ Label: SelectLabel,
28
+ Item: SelectItem,
29
+ Separator: SelectSeparator,
30
+ };
@@ -0,0 +1,189 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ type Props = Omit<HTMLAttributes<"button">, "role" | "type" | "aria-checked"> & {
5
+ /**
6
+ * Unique identifier for the switch component.
7
+ */
8
+ id: string;
9
+ /**
10
+ * Optional text label to display alongside the switch.
11
+ */
12
+ label?: string;
13
+ /**
14
+ * Controls the checked state of the switch.
15
+ */
16
+ checked?: boolean;
17
+ /**
18
+ * Custom padding in pixels to apply around the switch.
19
+ */
20
+ padding?: number;
21
+ /**
22
+ * Size variant of the switch component.
23
+ */
24
+ size?: "sm" | "md" | "lg";
25
+ /**
26
+ * Visual style variant of the switch.
27
+ * @default "default"
28
+ */
29
+ variant?: "default" | "primary" | "secondary" | "info" | "success" | "warning" | "error";
30
+ };
31
+
32
+ const {
33
+ id,
34
+ label,
35
+ checked = false,
36
+ padding,
37
+ size = "md",
38
+ variant = "default",
39
+ class: className,
40
+ ...rest
41
+ } = Astro.props;
42
+
43
+ // if no specific padding is set, base it off of size
44
+ let newPadding = padding;
45
+ if (!padding) {
46
+ newPadding = size === "sm" ? 2.5 : size === "lg" ? 4 : 3;
47
+ }
48
+
49
+ const sizeMultiplier = size === "sm" ? 4 : size === "lg" ? 6 : 5;
50
+
51
+ const toggleSizeClass = {
52
+ sm: "size-4",
53
+ md: "size-5",
54
+ lg: "size-6",
55
+ };
56
+
57
+ let ariaLabel;
58
+ if (rest["aria-label"]) {
59
+ ariaLabel = rest["aria-label"];
60
+ delete rest["aria-label"];
61
+ } else if (label) {
62
+ ariaLabel = label;
63
+ } else {
64
+ ariaLabel = "switch";
65
+ }
66
+ ---
67
+
68
+ <div class="starwind-switch flex items-center">
69
+ <button
70
+ type="button"
71
+ id={id}
72
+ role="switch"
73
+ aria-checked={checked ? "true" : "false"}
74
+ aria-label={ariaLabel}
75
+ class:list={[
76
+ "starwind-transition-colors border-input bg-muted inline-flex h-(--height) w-(--width) items-center rounded-full border",
77
+ "group peer ring-offset-background focus-visible:outline-2 focus-visible:outline-offset-2",
78
+ "not-disabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
79
+ {
80
+ "aria-checked:border-primary focus:outline-primary": variant === "primary",
81
+ "aria-checked:border-secondary focus:outline-secondary": variant === "secondary",
82
+ "aria-checked:border-foreground focus:outline-outline": variant === "default",
83
+ "aria-checked:border-info focus:outline-info": variant === "info",
84
+ "aria-checked:border-success focus:outline-success": variant === "success",
85
+ "aria-checked:border-warning focus:outline-warning": variant === "warning",
86
+ "aria-checked:border-error focus:outline-error": variant === "error",
87
+ },
88
+ className,
89
+ ]}
90
+ style={{
91
+ "--padding": `${newPadding}px`,
92
+ "--height": `calc((var(--spacing) * ${sizeMultiplier}) + (var(--padding) * 2))`,
93
+ "--width": `calc((var(--spacing) * ${sizeMultiplier} * 2) + (var(--padding) * 3))`,
94
+ "--border-offset": "1px",
95
+ }}
96
+ {...rest}
97
+ >
98
+ <span
99
+ class:list={[
100
+ "bg-foreground inline-block transform rounded-full transition-transform",
101
+ "group-aria-checked:translate-x-(--translation) group-aria-[checked=false]:translate-x-[calc(var(--padding)-var(--border-offset))]",
102
+ toggleSizeClass[size],
103
+ ]}
104
+ style={{
105
+ "--translation": `calc((var(--spacing) * ${sizeMultiplier}) + (var(--padding) * 2) - var(--border-offset))`,
106
+ }}></span>
107
+ </button>
108
+ {
109
+ label && (
110
+ <label
111
+ for={id}
112
+ class:list={[
113
+ "text-foreground ml-2 font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
114
+ {
115
+ "text-sm": size === "sm",
116
+ "text-base": size === "md",
117
+ "text-lg": size === "lg",
118
+ },
119
+ ]}
120
+ >
121
+ {label}
122
+ </label>
123
+ )
124
+ }
125
+ </div>
126
+
127
+ <script>
128
+ import type { SwitchChangeEvent } from "./SwitchTypes";
129
+
130
+ class SwitchHandler {
131
+ private switchButton: HTMLButtonElement;
132
+
133
+ constructor(switchButton: HTMLButtonElement) {
134
+ this.switchButton = switchButton;
135
+ this.setupEventListeners();
136
+ }
137
+
138
+ private setupEventListeners(): void {
139
+ this.switchButton.addEventListener("click", () => this.handleStateChange());
140
+ this.switchButton.addEventListener("keydown", (event) => this.handleKeyDown(event));
141
+ }
142
+
143
+ private handleStateChange(): void {
144
+ if (this.switchButton.disabled) return;
145
+
146
+ const isChecked = this.switchButton.getAttribute("aria-checked") === "true";
147
+ const newState = !isChecked;
148
+
149
+ this.switchButton.setAttribute("aria-checked", newState.toString());
150
+
151
+ // Dispatch custom event with the new state
152
+ const event = new CustomEvent<SwitchChangeEvent["detail"]>("starwind-switch:change", {
153
+ detail: {
154
+ checked: newState,
155
+ switchId: this.switchButton.id,
156
+ },
157
+ bubbles: true,
158
+ cancelable: true,
159
+ });
160
+
161
+ this.switchButton.dispatchEvent(event);
162
+ }
163
+
164
+ private handleKeyDown(event: KeyboardEvent): void {
165
+ if (this.switchButton.disabled) return;
166
+
167
+ if (event.key === " " || event.key === "Enter") {
168
+ event.preventDefault();
169
+ this.handleStateChange();
170
+ }
171
+ }
172
+ }
173
+
174
+ // Store instances in a WeakMap to avoid memory leaks
175
+ const switchInstances = new WeakMap<HTMLButtonElement, SwitchHandler>();
176
+
177
+ const setupSwitches = () => {
178
+ document
179
+ .querySelectorAll<HTMLButtonElement>('.starwind-switch button[role="switch"]')
180
+ .forEach((switchButton) => {
181
+ if (!switchInstances.has(switchButton)) {
182
+ switchInstances.set(switchButton, new SwitchHandler(switchButton));
183
+ }
184
+ });
185
+ };
186
+
187
+ setupSwitches();
188
+ document.addEventListener("astro:after-swap", setupSwitches);
189
+ </script>
@@ -0,0 +1,6 @@
1
+ export interface SwitchChangeEvent extends CustomEvent {
2
+ detail: {
3
+ checked: boolean;
4
+ switchId: string;
5
+ };
6
+ }
@@ -0,0 +1,6 @@
1
+ import Switch from "./Switch.astro";
2
+ import type { SwitchChangeEvent } from "./SwitchTypes";
3
+
4
+ export { Switch, type SwitchChangeEvent };
5
+
6
+ export default Switch;
@@ -0,0 +1,246 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ interface Props extends HTMLAttributes<"div"> {
5
+ defaultValue?: string;
6
+ syncKey?: string;
7
+ }
8
+
9
+ const { defaultValue, syncKey, class: className, ...rest } = Astro.props;
10
+ ---
11
+
12
+ <div
13
+ class:list={["starwind-tabs", className]}
14
+ data-default-value={defaultValue}
15
+ data-sync-key={syncKey}
16
+ {...rest}
17
+ >
18
+ <slot />
19
+ </div>
20
+
21
+ <script>
22
+ type TabValue = string;
23
+
24
+ interface TabsSyncEventDetail {
25
+ value: TabValue;
26
+ }
27
+
28
+ interface TabsSyncEvent extends CustomEvent<TabsSyncEventDetail> {
29
+ type: `starwind-tabs-sync:${string}`;
30
+ }
31
+
32
+ class TabsHandler {
33
+ private tabs: HTMLElement;
34
+ private triggers: HTMLButtonElement[];
35
+ private contents: HTMLElement[];
36
+ private currentTabIndex: number = 0;
37
+ private tabsId: string;
38
+ private syncKey?: string;
39
+ private storageKey: string;
40
+ private valueToTriggerMap: Map<string, HTMLButtonElement>;
41
+ private valueToContentMap: Map<string, HTMLElement>;
42
+
43
+ constructor(tabs: HTMLElement, idx: number) {
44
+ this.tabs = tabs;
45
+ this.triggers = Array.from(tabs.querySelectorAll("[data-tabs-trigger]"));
46
+ this.contents = Array.from(tabs.querySelectorAll("[data-tabs-content]"));
47
+ this.tabsId = `starwind-tabs${idx}`;
48
+ this.syncKey = tabs.dataset.syncKey;
49
+ this.storageKey = this.syncKey
50
+ ? `starwind-tabs-${this.syncKey}`
51
+ : `starwind-tabs-${this.tabsId}`;
52
+
53
+ // Create maps for faster lookups
54
+ this.valueToTriggerMap = new Map(
55
+ this.triggers.map((trigger) => [trigger.getAttribute("data-value") ?? "", trigger]),
56
+ );
57
+ this.valueToContentMap = new Map(
58
+ this.contents.map((content) => [content.getAttribute("data-value") ?? "", content]),
59
+ );
60
+
61
+ this.setupIds();
62
+ this.initializeTab();
63
+ this.addEventListeners();
64
+
65
+ if (this.syncKey) {
66
+ this.setupSyncListener();
67
+ }
68
+ }
69
+
70
+ private initializeTab(): void {
71
+ const value = this.syncKey
72
+ ? (localStorage.getItem(this.storageKey) ?? this.tabs.dataset.defaultValue)
73
+ : this.tabs.dataset.defaultValue;
74
+
75
+ if (value) {
76
+ this.showTab(value);
77
+ this.currentTabIndex = this.triggers.findIndex(
78
+ (trigger) => trigger.getAttribute("data-value") === value,
79
+ );
80
+ this.setTabIndex();
81
+ }
82
+ }
83
+
84
+ private setupSyncListener(): void {
85
+ document.addEventListener(`starwind-tabs-sync:${this.syncKey}`, ((e: TabsSyncEvent) => {
86
+ const value = e.detail.value;
87
+ const trigger = this.valueToTriggerMap.get(value);
88
+ const index = trigger ? this.triggers.indexOf(trigger) : -1;
89
+
90
+ if (index !== -1) {
91
+ this.showTab(value);
92
+ this.currentTabIndex = index;
93
+ this.setTabIndex();
94
+ }
95
+ }) as EventListener);
96
+ }
97
+
98
+ private setupIds(): void {
99
+ this.triggers.forEach((trigger, idx) => {
100
+ const triggerId = `${this.tabsId}-t${idx}`;
101
+ const contentId = `${this.tabsId}-c${idx}`;
102
+ const value = trigger.getAttribute("data-value");
103
+
104
+ trigger.id = triggerId;
105
+
106
+ if (value) {
107
+ trigger.setAttribute("aria-controls", contentId);
108
+ const content = this.valueToContentMap.get(value);
109
+ if (content) {
110
+ content.id = contentId;
111
+ content.setAttribute("aria-labelledby", triggerId);
112
+ }
113
+ }
114
+ });
115
+ }
116
+
117
+ private setTabIndex(): void {
118
+ this.triggers.forEach((trigger, index) => {
119
+ trigger.setAttribute("tabindex", index === this.currentTabIndex ? "0" : "-1");
120
+ });
121
+ }
122
+
123
+ private dispatchSyncEvent(value: TabValue): void {
124
+ if (!this.syncKey) return;
125
+
126
+ document.dispatchEvent(
127
+ new CustomEvent(`starwind-tabs-sync:${this.syncKey}`, {
128
+ detail: { value },
129
+ }),
130
+ );
131
+
132
+ localStorage.setItem(this.storageKey, value);
133
+ }
134
+
135
+ private handleKeyNavigation = (e: KeyboardEvent, trigger: HTMLElement): void => {
136
+ const key = e.key;
137
+ let newIndex = this.currentTabIndex;
138
+
139
+ switch (key) {
140
+ case "ArrowRight": {
141
+ for (let i = 1; i < this.triggers.length; i++) {
142
+ const index = (this.currentTabIndex + i) % this.triggers.length;
143
+ if (!this.triggers[index].disabled) {
144
+ newIndex = index;
145
+ break;
146
+ }
147
+ }
148
+ break;
149
+ }
150
+ case "ArrowLeft": {
151
+ for (let i = 1; i < this.triggers.length; i++) {
152
+ const index = (this.currentTabIndex - i + this.triggers.length) % this.triggers.length;
153
+ if (!this.triggers[index].disabled) {
154
+ newIndex = index;
155
+ break;
156
+ }
157
+ }
158
+ break;
159
+ }
160
+ case "Home": {
161
+ for (let i = 0; i < this.triggers.length; i++) {
162
+ if (!this.triggers[i].disabled) {
163
+ newIndex = i;
164
+ break;
165
+ }
166
+ }
167
+ break;
168
+ }
169
+ case "End": {
170
+ for (let i = this.triggers.length - 1; i >= 0; i--) {
171
+ if (!this.triggers[i].disabled) {
172
+ newIndex = i;
173
+ break;
174
+ }
175
+ }
176
+ break;
177
+ }
178
+ default:
179
+ return;
180
+ }
181
+
182
+ e.preventDefault();
183
+ const newTrigger = this.triggers[newIndex];
184
+ const value = newTrigger.getAttribute("data-value");
185
+ if (value) {
186
+ this.showTab(value);
187
+ this.currentTabIndex = newIndex;
188
+ this.setTabIndex();
189
+ newTrigger.focus();
190
+ this.dispatchSyncEvent(value);
191
+ }
192
+ };
193
+
194
+ private handleClick = (trigger: HTMLElement, index: number): void => {
195
+ const value = trigger.getAttribute("data-value");
196
+ if (value) {
197
+ this.showTab(value);
198
+ this.currentTabIndex = index;
199
+ this.setTabIndex();
200
+ trigger.focus();
201
+ this.dispatchSyncEvent(value);
202
+ }
203
+ };
204
+
205
+ private addEventListeners(): void {
206
+ this.triggers.forEach((trigger, index) => {
207
+ trigger.addEventListener("click", () => this.handleClick(trigger, index));
208
+ trigger.addEventListener("keydown", (e) => this.handleKeyNavigation(e, trigger));
209
+ });
210
+ }
211
+
212
+ private showTab(value: TabValue): void {
213
+ const trigger = this.valueToTriggerMap.get(value);
214
+ const content = this.valueToContentMap.get(value);
215
+
216
+ if (!trigger || !content) return;
217
+
218
+ // Update all triggers and contents
219
+ this.triggers.forEach((t) => {
220
+ const isActive = t === trigger;
221
+ t.setAttribute("data-state", isActive ? "active" : "inactive");
222
+ t.setAttribute("aria-selected", isActive.toString());
223
+ });
224
+
225
+ this.contents.forEach((c) => {
226
+ const isActive = c === content;
227
+ c.setAttribute("data-state", isActive ? "active" : "inactive");
228
+ c.hidden = !isActive;
229
+ });
230
+ }
231
+ }
232
+
233
+ // Store instances in a WeakMap to avoid memory leaks
234
+ const tabInstances = new WeakMap<HTMLElement, TabsHandler>();
235
+
236
+ const setupTabs = () => {
237
+ document.querySelectorAll<HTMLElement>(".starwind-tabs").forEach((tabs, idx) => {
238
+ if (!tabInstances.has(tabs)) {
239
+ tabInstances.set(tabs, new TabsHandler(tabs, idx));
240
+ }
241
+ });
242
+ };
243
+
244
+ setupTabs();
245
+ document.addEventListener("astro:after-swap", setupTabs);
246
+ </script>
@@ -0,0 +1,22 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ interface Props extends Omit<HTMLAttributes<"div">, "id" | "role" | "tabindex" | "hidden"> {
5
+ value: string;
6
+ }
7
+
8
+ const { value, class: className, ...rest } = Astro.props;
9
+ ---
10
+
11
+ <div
12
+ class:list={["mt-2 focus-visible:outline-2 focus-visible:outline-offset-2", className]}
13
+ data-tabs-content
14
+ data-value={value}
15
+ data-state="inactive"
16
+ role="tabpanel"
17
+ tabindex="0"
18
+ hidden
19
+ {...rest}
20
+ >
21
+ <slot />
22
+ </div>
@@ -0,0 +1,19 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ type Props = Omit<HTMLAttributes<"div">, "role">;
5
+
6
+ const { class: className, ...rest } = Astro.props;
7
+ ---
8
+
9
+ <div
10
+ class:list={[
11
+ "bg-muted text-muted-foreground inline-flex w-full items-center justify-center rounded-md p-1",
12
+ className,
13
+ ]}
14
+ data-tabs-list
15
+ role="tablist"
16
+ {...rest}
17
+ >
18
+ <slot />
19
+ </div>
@@ -0,0 +1,27 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ interface Props extends Omit<HTMLAttributes<"button">, "type" | "id" | "role"> {
5
+ value: string;
6
+ }
7
+
8
+ const { value, class: className, ...rest } = Astro.props;
9
+ ---
10
+
11
+ <button
12
+ class:list={[
13
+ "starwind-transition-colors inline-flex grow items-center justify-center rounded-sm px-3 py-1.5 font-medium whitespace-nowrap",
14
+ "data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
15
+ "focus-visible:outline-outline focus-visible:outline-2 focus-visible:outline-offset-2",
16
+ "disabled:pointer-events-none disabled:opacity-50",
17
+ className,
18
+ ]}
19
+ data-tabs-trigger
20
+ data-value={value}
21
+ data-state="inactive"
22
+ role="tab"
23
+ aria-selected="false"
24
+ {...rest}
25
+ >
26
+ <slot />
27
+ </button>
@@ -0,0 +1,13 @@
1
+ import Tabs from "./Tabs.astro";
2
+ import TabsContent from "./TabsContent.astro";
3
+ import TabsList from "./TabsList.astro";
4
+ import TabsTrigger from "./TabsTrigger.astro";
5
+
6
+ export { Tabs, TabsContent, TabsList, TabsTrigger };
7
+
8
+ export default {
9
+ Root: Tabs,
10
+ Content: TabsContent,
11
+ List: TabsList,
12
+ Trigger: TabsTrigger,
13
+ };