@starwind-ui/core 1.13.0 → 1.15.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 (47) hide show
  1. package/dist/index.js +28 -5
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/components/badge/Badge.astro +8 -2
  4. package/dist/src/components/button/Button.astro +8 -7
  5. package/dist/src/components/collapsible/Collapsible.astro +161 -0
  6. package/dist/src/components/collapsible/CollapsibleContent.astro +22 -0
  7. package/dist/src/components/collapsible/CollapsibleTrigger.astro +44 -0
  8. package/dist/src/components/collapsible/index.ts +13 -0
  9. package/dist/src/components/dialog/Dialog.astro +35 -1
  10. package/dist/src/components/input-otp/InputOtp.astro +319 -0
  11. package/dist/src/components/input-otp/InputOtpGroup.astro +16 -0
  12. package/dist/src/components/input-otp/InputOtpSeparator.astro +25 -0
  13. package/dist/src/components/input-otp/InputOtpSlot.astro +48 -0
  14. package/dist/src/components/input-otp/InputOtpTypes.ts +6 -0
  15. package/dist/src/components/input-otp/index.ts +33 -0
  16. package/dist/src/components/prose/Prose.astro +617 -0
  17. package/dist/src/components/prose/index.ts +9 -0
  18. package/dist/src/components/select/Select.astro +3 -0
  19. package/dist/src/components/sidebar/Sidebar.astro +213 -0
  20. package/dist/src/components/sidebar/SidebarContent.astro +24 -0
  21. package/dist/src/components/sidebar/SidebarFooter.astro +21 -0
  22. package/dist/src/components/sidebar/SidebarGroup.astro +21 -0
  23. package/dist/src/components/sidebar/SidebarGroupContent.astro +21 -0
  24. package/dist/src/components/sidebar/SidebarGroupLabel.astro +52 -0
  25. package/dist/src/components/sidebar/SidebarHeader.astro +21 -0
  26. package/dist/src/components/sidebar/SidebarInput.astro +22 -0
  27. package/dist/src/components/sidebar/SidebarInset.astro +21 -0
  28. package/dist/src/components/sidebar/SidebarMenu.astro +21 -0
  29. package/dist/src/components/sidebar/SidebarMenuAction.astro +59 -0
  30. package/dist/src/components/sidebar/SidebarMenuBadge.astro +30 -0
  31. package/dist/src/components/sidebar/SidebarMenuButton.astro +129 -0
  32. package/dist/src/components/sidebar/SidebarMenuItem.astro +21 -0
  33. package/dist/src/components/sidebar/SidebarMenuSkeleton.astro +40 -0
  34. package/dist/src/components/sidebar/SidebarMenuSub.astro +24 -0
  35. package/dist/src/components/sidebar/SidebarMenuSubButton.astro +49 -0
  36. package/dist/src/components/sidebar/SidebarMenuSubItem.astro +16 -0
  37. package/dist/src/components/sidebar/SidebarProvider.astro +213 -0
  38. package/dist/src/components/sidebar/SidebarRail.astro +71 -0
  39. package/dist/src/components/sidebar/SidebarSeparator.astro +22 -0
  40. package/dist/src/components/sidebar/SidebarTrigger.astro +66 -0
  41. package/dist/src/components/sidebar/index.ts +103 -0
  42. package/dist/src/components/theme-toggle/ThemeToggle.astro +208 -0
  43. package/dist/src/components/theme-toggle/index.ts +7 -0
  44. package/dist/src/components/toggle/Toggle.astro +1 -1
  45. package/dist/src/components/tooltip/Tooltip.astro +80 -37
  46. package/dist/src/components/tooltip/TooltipContent.astro +9 -34
  47. package/package.json +1 -1
@@ -0,0 +1,319 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"div"> & {
6
+ maxLength?: number;
7
+ value?: string;
8
+ defaultValue?: string;
9
+ disabled?: boolean;
10
+ pattern?: RegExp | string;
11
+ name?: string;
12
+ id?: string;
13
+ required?: boolean;
14
+ };
15
+
16
+ export const inputOtp = tv({
17
+ base: "starwind-input-otp flex items-center gap-2 outline-none has-disabled:opacity-50",
18
+ });
19
+
20
+ const {
21
+ maxLength = 6,
22
+ value,
23
+ defaultValue,
24
+ disabled = false,
25
+ pattern,
26
+ name,
27
+ id,
28
+ required,
29
+ class: className,
30
+ ...rest
31
+ } = Astro.props;
32
+
33
+ let patternStr = "\\d";
34
+ if (pattern) {
35
+ const stripAnchorsRegex = /^\^|\$$/g;
36
+ if (pattern instanceof RegExp) {
37
+ patternStr = pattern.source.replace(stripAnchorsRegex, "");
38
+ } else {
39
+ patternStr = pattern.replace(stripAnchorsRegex, "");
40
+ }
41
+ }
42
+
43
+ const numericPatterns = ["\\d", "[0-9]", "\\d+", "[0-9]+"];
44
+ const isNumericOnly = numericPatterns.includes(patternStr);
45
+ const inputMode = isNumericOnly ? "numeric" : "text";
46
+ ---
47
+
48
+ <div
49
+ class={inputOtp({ class: className })}
50
+ data-max-length={maxLength}
51
+ data-value={value || defaultValue || ""}
52
+ data-disabled={disabled}
53
+ data-pattern={patternStr}
54
+ data-slot="input-otp"
55
+ tabindex={disabled ? "-1" : "0"}
56
+ {...rest}
57
+ >
58
+ <input
59
+ autocomplete="one-time-code"
60
+ inputmode={inputMode}
61
+ tabindex="-1"
62
+ name={name}
63
+ id={id}
64
+ class="sr-only"
65
+ required={required}
66
+ maxlength={maxLength.toString()}
67
+ value={value || defaultValue || ""}
68
+ disabled={disabled}
69
+ data-otp-hidden-input
70
+ />
71
+ <slot />
72
+ </div>
73
+
74
+ <script>
75
+ import type { InputOtpChangeEvent } from "./InputOtpTypes";
76
+
77
+ class InputOtpHandler {
78
+ private container: HTMLElement;
79
+ private slots: HTMLElement[];
80
+ private hiddenInput: HTMLInputElement | null;
81
+ private maxLength: number;
82
+ private containerId: string;
83
+ private pattern: RegExp;
84
+ private observer: MutationObserver | null = null;
85
+ private currentIndex: number = 0;
86
+ private values: string[] = [];
87
+
88
+ constructor(container: HTMLElement, idx: number) {
89
+ this.container = container;
90
+ this.containerId = container.id || `starwind-input-otp-${idx}`;
91
+ if (!container.id) {
92
+ container.id = this.containerId;
93
+ }
94
+ this.maxLength = parseInt(container.dataset.maxLength || "6", 10);
95
+ const patternStr = container.dataset.pattern || "\\d";
96
+ this.pattern = new RegExp(`^${patternStr}$`);
97
+ this.slots = Array.from(container.querySelectorAll<HTMLElement>("[data-otp-slot]"));
98
+ this.hiddenInput = container.querySelector<HTMLInputElement>("input[data-otp-hidden-input]");
99
+ this.values = new Array(this.slots.length).fill("");
100
+ this.init();
101
+ }
102
+
103
+ private init() {
104
+ if (this.slots.length === 0) return;
105
+
106
+ const isDisabled = this.container.dataset.disabled === "true";
107
+ if (isDisabled) {
108
+ this.container.setAttribute("tabindex", "-1");
109
+ return;
110
+ }
111
+
112
+ const initialValue = this.container.dataset.value || "";
113
+ if (initialValue) {
114
+ this.setValue(initialValue);
115
+ }
116
+
117
+ this.setupEventListeners();
118
+ this.setupValueSync();
119
+ this.updateSlots();
120
+ }
121
+
122
+ private setupValueSync() {
123
+ this.observer = new MutationObserver(() => {
124
+ const containerValue = this.container.dataset.value || "";
125
+ const currentValue = this.getValue();
126
+ if (containerValue !== currentValue) {
127
+ this.setValue(containerValue);
128
+ }
129
+ });
130
+
131
+ this.observer.observe(this.container, {
132
+ attributes: true,
133
+ attributeFilter: ["data-value"],
134
+ });
135
+ }
136
+
137
+ private setupEventListeners() {
138
+ this.container.addEventListener("keydown", (e) => this.handleKeyDown(e));
139
+ this.container.addEventListener("focus", () => this.handleContainerFocus());
140
+ this.container.addEventListener("blur", () => this.handleContainerBlur());
141
+ this.container.addEventListener("paste", (e) => this.handlePaste(e));
142
+ this.container.addEventListener("click", (e) => this.handleClick(e));
143
+ }
144
+
145
+ private handleKeyDown(e: KeyboardEvent) {
146
+ const isDisabled = this.container.dataset.disabled === "true";
147
+ if (isDisabled) return;
148
+
149
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
150
+ e.preventDefault();
151
+ const char = e.key;
152
+ if (this.pattern.test(char)) {
153
+ this.setSlotValue(this.currentIndex, char);
154
+ if (this.currentIndex < this.slots.length - 1) {
155
+ this.currentIndex++;
156
+ }
157
+ this.updateSlots();
158
+ this.updateValue();
159
+ }
160
+ } else if (e.key === "Backspace") {
161
+ e.preventDefault();
162
+ if (this.values[this.currentIndex]) {
163
+ this.setSlotValue(this.currentIndex, "");
164
+ } else if (this.currentIndex > 0) {
165
+ this.currentIndex--;
166
+ this.setSlotValue(this.currentIndex, "");
167
+ }
168
+ this.updateSlots();
169
+ this.updateValue();
170
+ } else if (e.key === "ArrowLeft") {
171
+ e.preventDefault();
172
+ if (this.currentIndex > 0) {
173
+ this.currentIndex--;
174
+ this.updateSlots();
175
+ }
176
+ } else if (e.key === "ArrowRight") {
177
+ e.preventDefault();
178
+ if (this.currentIndex < this.slots.length - 1) {
179
+ this.currentIndex++;
180
+ this.updateSlots();
181
+ }
182
+ } else if (e.key === "Delete") {
183
+ e.preventDefault();
184
+ this.setSlotValue(this.currentIndex, "");
185
+ this.updateSlots();
186
+ this.updateValue();
187
+ }
188
+ }
189
+
190
+ private handlePaste(e: ClipboardEvent) {
191
+ const isDisabled = this.container.dataset.disabled === "true";
192
+ if (isDisabled) return;
193
+
194
+ e.preventDefault();
195
+ const pastedData = e.clipboardData?.getData("text") || "";
196
+ const matchedChars = this.filterByPattern(pastedData).slice(0, this.maxLength);
197
+
198
+ if (matchedChars.length === 0) return;
199
+
200
+ for (let i = 0; i < matchedChars.length && this.currentIndex + i < this.slots.length; i++) {
201
+ this.setSlotValue(this.currentIndex + i, matchedChars[i]);
202
+ }
203
+
204
+ this.currentIndex = Math.min(this.currentIndex + matchedChars.length, this.slots.length - 1);
205
+ this.updateSlots();
206
+ this.updateValue();
207
+ }
208
+
209
+ private handleClick(e: MouseEvent) {
210
+ const isDisabled = this.container.dataset.disabled === "true";
211
+ if (isDisabled) return;
212
+
213
+ const slot = (e.target as HTMLElement).closest<HTMLElement>("[data-otp-slot]");
214
+ if (slot) {
215
+ const index = this.slots.indexOf(slot);
216
+ if (index !== -1) {
217
+ this.currentIndex = index;
218
+ this.updateSlots();
219
+ this.container.focus();
220
+ }
221
+ }
222
+ }
223
+
224
+ private handleContainerFocus() {
225
+ this.updateSlots();
226
+ }
227
+
228
+ private handleContainerBlur() {
229
+ this.updateSlots();
230
+ }
231
+
232
+ private setSlotValue(index: number, value: string) {
233
+ if (index >= 0 && index < this.values.length) {
234
+ this.values[index] = value;
235
+ }
236
+ }
237
+
238
+ private updateSlots() {
239
+ const isFocused = document.activeElement === this.container;
240
+
241
+ this.slots.forEach((slot, index) => {
242
+ const charElement = slot.querySelector("[data-otp-char]");
243
+ const caretElement = slot.querySelector("[data-otp-caret]");
244
+ const char = this.values[index] || "";
245
+
246
+ if (charElement) {
247
+ charElement.textContent = char;
248
+ }
249
+
250
+ const isActive = isFocused && index === this.currentIndex;
251
+ slot.setAttribute("data-active", isActive ? "true" : "false");
252
+
253
+ if (caretElement) {
254
+ if (isActive && !char) {
255
+ caretElement.classList.remove("hidden");
256
+ caretElement.classList.add("flex");
257
+ } else {
258
+ caretElement.classList.add("hidden");
259
+ caretElement.classList.remove("flex");
260
+ }
261
+ }
262
+ });
263
+ }
264
+
265
+ private filterByPattern(value: string): string[] {
266
+ return value.split("").filter((char) => this.pattern.test(char));
267
+ }
268
+
269
+ private setValue(value: string) {
270
+ const matchedChars = this.filterByPattern(value).slice(0, this.maxLength);
271
+ for (let i = 0; i < this.slots.length; i++) {
272
+ this.values[i] = matchedChars[i] || "";
273
+ }
274
+ this.currentIndex = Math.min(matchedChars.length, this.slots.length - 1);
275
+ this.updateSlots();
276
+ }
277
+
278
+ private updateValue() {
279
+ const value = this.values.join("");
280
+ this.container.dataset.value = value;
281
+
282
+ if (this.hiddenInput) {
283
+ this.hiddenInput.value = value;
284
+ }
285
+
286
+ this.dispatchChangeEvent(value);
287
+ }
288
+
289
+ public getValue(): string {
290
+ return this.values.join("");
291
+ }
292
+
293
+ private dispatchChangeEvent(value: string) {
294
+ const event = new CustomEvent<InputOtpChangeEvent["detail"]>("starwind-input-otp:change", {
295
+ bubbles: true,
296
+ detail: {
297
+ value,
298
+ inputOtpId: this.containerId,
299
+ },
300
+ });
301
+ this.container.dispatchEvent(event);
302
+ }
303
+ }
304
+
305
+ const inputOtpInstances = new WeakMap<HTMLElement, InputOtpHandler>();
306
+ let inputOtpCounter = 0;
307
+
308
+ const setupInputOtps = () => {
309
+ document.querySelectorAll<HTMLElement>(".starwind-input-otp").forEach((container) => {
310
+ if (!inputOtpInstances.has(container)) {
311
+ inputOtpInstances.set(container, new InputOtpHandler(container, inputOtpCounter++));
312
+ }
313
+ });
314
+ };
315
+
316
+ setupInputOtps();
317
+ document.addEventListener("astro:after-swap", setupInputOtps);
318
+ document.addEventListener("starwind:init", setupInputOtps);
319
+ </script>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"div">;
6
+
7
+ export const inputOtpGroup = tv({
8
+ base: "flex items-center",
9
+ });
10
+
11
+ const { class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <div class={inputOtpGroup({ class: className })} data-slot="input-otp-group" {...rest}>
15
+ <slot />
16
+ </div>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import MinusIcon from "@tabler/icons/outline/minus.svg";
3
+ import type { HTMLAttributes } from "astro/types";
4
+ import { tv } from "tailwind-variants";
5
+
6
+ type Props = HTMLAttributes<"div">;
7
+
8
+ export const inputOtpSeparator = tv({
9
+ base: "text-muted-foreground",
10
+ });
11
+
12
+ const { class: className, ...rest } = Astro.props;
13
+ ---
14
+
15
+ <div
16
+ aria-hidden="true"
17
+ class={inputOtpSeparator({ class: className })}
18
+ data-slot="input-otp-separator"
19
+ role="separator"
20
+ {...rest}
21
+ >
22
+ <slot name="icon">
23
+ <MinusIcon class="size-6" />
24
+ </slot>
25
+ </div>
@@ -0,0 +1,48 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"div"> &
6
+ VariantProps<typeof inputOtpSlot> & {
7
+ index?: number;
8
+ };
9
+
10
+ export const inputOtpSlot = tv({
11
+ base: [
12
+ "border-input dark:bg-input/30 text-foreground border bg-transparent text-center shadow-xs",
13
+ "relative flex items-center justify-center border-y border-r text-sm transition-all outline-none",
14
+ "first:rounded-l-md first:border-l last:rounded-r-md disabled:cursor-not-allowed disabled:opacity-50",
15
+ "data-[active=true]:border-outline data-[active=true]:ring-outline/50 data-[active=true]:z-10 data-[active=true]:ring-3",
16
+ "data-[active=true]:aria-invalid:ring-error/40",
17
+ "aria-invalid:border-error data-[active=true]:aria-invalid:border-error",
18
+ ],
19
+ variants: {
20
+ size: {
21
+ sm: "size-9 text-sm",
22
+ md: "size-11 text-base",
23
+ lg: "size-12 text-lg",
24
+ },
25
+ },
26
+ defaultVariants: { size: "md" },
27
+ });
28
+
29
+ const { size, index, class: className, ...rest } = Astro.props;
30
+ ---
31
+
32
+ <div
33
+ class={inputOtpSlot({ size, class: className })}
34
+ data-otp-slot
35
+ data-index={index}
36
+ data-slot="input-otp-slot"
37
+ {...rest}
38
+ >
39
+ <span data-otp-char></span>
40
+ <div
41
+ class="pointer-events-none absolute inset-0 hidden items-center justify-center"
42
+ data-otp-caret
43
+ >
44
+ <slot name="caret">
45
+ <div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
46
+ </slot>
47
+ </div>
48
+ </div>
@@ -0,0 +1,6 @@
1
+ export interface InputOtpChangeEvent extends CustomEvent {
2
+ detail: {
3
+ value: string;
4
+ inputOtpId?: string;
5
+ };
6
+ }
@@ -0,0 +1,33 @@
1
+ import InputOtp, { inputOtp } from "./InputOtp.astro";
2
+ import InputOtpGroup, { inputOtpGroup } from "./InputOtpGroup.astro";
3
+ import InputOtpSeparator, { inputOtpSeparator } from "./InputOtpSeparator.astro";
4
+ import InputOtpSlot, { inputOtpSlot } from "./InputOtpSlot.astro";
5
+ import type { InputOtpChangeEvent } from "./InputOtpTypes";
6
+
7
+ const REGEXP_ONLY_DIGITS_AND_CHARS = /^[A-Za-z0-9]+$/;
8
+ const REGEXP_ONLY_DIGITS = /^[0-9]+$/;
9
+
10
+ const InputOtpVariants = {
11
+ inputOtp,
12
+ inputOtpGroup,
13
+ inputOtpSeparator,
14
+ inputOtpSlot,
15
+ };
16
+
17
+ export {
18
+ InputOtp,
19
+ type InputOtpChangeEvent,
20
+ InputOtpGroup,
21
+ InputOtpSeparator,
22
+ InputOtpSlot,
23
+ InputOtpVariants,
24
+ REGEXP_ONLY_DIGITS,
25
+ REGEXP_ONLY_DIGITS_AND_CHARS,
26
+ };
27
+
28
+ export default {
29
+ Root: InputOtp,
30
+ Group: InputOtpGroup,
31
+ Separator: InputOtpSeparator,
32
+ Slot: InputOtpSlot,
33
+ };