@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.
- package/dist/index.js +86 -2
- package/dist/index.js.map +1 -1
- package/dist/src/components/accordion/Accordion.astro +248 -0
- package/dist/src/components/accordion/AccordionContent.astro +28 -0
- package/dist/src/components/accordion/AccordionItem.astro +25 -0
- package/dist/src/components/accordion/AccordionTrigger.astro +26 -0
- package/dist/src/components/accordion/index.ts +13 -0
- package/dist/src/components/alert/Alert.astro +44 -0
- package/dist/src/components/alert/AlertDescription.astro +11 -0
- package/dist/src/components/alert/AlertTitle.astro +17 -0
- package/dist/src/components/alert/index.ts +11 -0
- package/dist/src/components/avatar/Avatar.astro +44 -0
- package/dist/src/components/avatar/AvatarFallback.astro +16 -0
- package/dist/src/components/avatar/AvatarImage.astro +48 -0
- package/dist/src/components/avatar/index.ts +11 -0
- package/dist/src/components/card/Card.astro +14 -0
- package/dist/src/components/card/CardContent.astro +11 -0
- package/dist/src/components/card/CardDescription.astro +11 -0
- package/dist/src/components/card/CardFooter.astro +11 -0
- package/dist/src/components/card/CardHeader.astro +11 -0
- package/dist/src/components/card/CardTitle.astro +11 -0
- package/dist/src/components/card/index.ts +17 -0
- package/dist/src/components/checkbox/Checkbox.astro +105 -0
- package/dist/src/components/checkbox/index.ts +5 -0
- package/dist/src/components/dialog/Dialog.astro +175 -0
- package/dist/src/components/dialog/DialogClose.astro +30 -0
- package/dist/src/components/dialog/DialogContent.astro +57 -0
- package/dist/src/components/dialog/DialogDescription.astro +11 -0
- package/dist/src/components/dialog/DialogFooter.astro +11 -0
- package/dist/src/components/dialog/DialogHeader.astro +11 -0
- package/dist/src/components/dialog/DialogTitle.astro +16 -0
- package/dist/src/components/dialog/DialogTrigger.astro +35 -0
- package/dist/src/components/dialog/index.ts +30 -0
- package/dist/src/components/input/Input.astro +26 -0
- package/dist/src/components/input/index.ts +5 -0
- package/dist/src/components/label/Label.astro +25 -0
- package/dist/src/components/label/index.ts +5 -0
- package/dist/src/components/pagination/Pagination.astro +18 -0
- package/dist/src/components/pagination/PaginationContent.astro +13 -0
- package/dist/src/components/pagination/PaginationEllipsis.astro +15 -0
- package/dist/src/components/pagination/PaginationItem.astro +13 -0
- package/dist/src/components/pagination/PaginationLink.astro +50 -0
- package/dist/src/components/pagination/PaginationNext.astro +23 -0
- package/dist/src/components/pagination/PaginationPrevious.astro +23 -0
- package/dist/src/components/pagination/index.ts +27 -0
- package/dist/src/components/select/Select.astro +452 -0
- package/dist/src/components/select/SelectContent.astro +57 -0
- package/dist/src/components/select/SelectGroup.astro +10 -0
- package/dist/src/components/select/SelectItem.astro +41 -0
- package/dist/src/components/select/SelectLabel.astro +11 -0
- package/dist/src/components/select/SelectSeparator.astro +9 -0
- package/dist/src/components/select/SelectTrigger.astro +40 -0
- package/dist/src/components/select/SelectTypes.ts +7 -0
- package/dist/src/components/select/SelectValue.astro +16 -0
- package/dist/src/components/select/index.ts +30 -0
- package/dist/src/components/switch/Switch.astro +189 -0
- package/dist/src/components/switch/SwitchTypes.ts +6 -0
- package/dist/src/components/switch/index.ts +6 -0
- package/dist/src/components/tabs/Tabs.astro +246 -0
- package/dist/src/components/tabs/TabsContent.astro +22 -0
- package/dist/src/components/tabs/TabsList.astro +19 -0
- package/dist/src/components/tabs/TabsTrigger.astro +27 -0
- package/dist/src/components/tabs/index.ts +13 -0
- package/dist/src/components/textarea/Textarea.astro +25 -0
- package/dist/src/components/textarea/index.ts +5 -0
- package/dist/src/components/tooltip/Tooltip.astro +233 -0
- package/dist/src/components/tooltip/TooltipContent.astro +76 -0
- package/dist/src/components/tooltip/TooltipTrigger.astro +11 -0
- package/dist/src/components/tooltip/index.ts +11 -0
- 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,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,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,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
|
+
};
|