@moontra/moonui 3.0.0 → 3.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.d.mts +78 -12
- package/dist/index.d.ts +78 -12
- package/dist/index.global.js +67 -391
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +775 -5387
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +687 -5341
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.global.js +1 -14
- package/package.json +8 -1
- package/src/components/ui/__tests__/carousel.test.tsx +291 -0
- package/src/components/ui/__tests__/input-otp.test.tsx +228 -0
- package/src/components/ui/carousel.tsx +324 -0
- package/src/components/ui/index.ts +32 -0
- package/src/components/ui/input-otp.tsx +140 -0
- package/tailwind-preset.js +6 -0
- package/src/use-paddle.ts +0 -138
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import {
|
|
4
|
+
InputOTP,
|
|
5
|
+
InputOTPGroup,
|
|
6
|
+
InputOTPSlot,
|
|
7
|
+
InputOTPSeparator,
|
|
8
|
+
inputOTPSlotVariants,
|
|
9
|
+
REGEXP_ONLY_DIGITS,
|
|
10
|
+
} from '../input-otp'
|
|
11
|
+
|
|
12
|
+
// Kontrollü test bileşeni — gerçek input-otp kütüphanesiyle uçtan uca çalışır
|
|
13
|
+
const ControlledOTP = ({
|
|
14
|
+
onChange,
|
|
15
|
+
maxLength = 6,
|
|
16
|
+
...props
|
|
17
|
+
}: {
|
|
18
|
+
onChange?: (value: string) => void
|
|
19
|
+
maxLength?: number
|
|
20
|
+
} & Partial<React.ComponentProps<typeof InputOTP>>) => {
|
|
21
|
+
const [value, setValue] = React.useState('')
|
|
22
|
+
return (
|
|
23
|
+
<InputOTP
|
|
24
|
+
maxLength={maxLength}
|
|
25
|
+
value={value}
|
|
26
|
+
onChange={(newValue) => {
|
|
27
|
+
setValue(newValue)
|
|
28
|
+
onChange?.(newValue)
|
|
29
|
+
}}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<InputOTPGroup data-testid="otp-group">
|
|
33
|
+
{Array.from({ length: maxLength }, (_, i) => (
|
|
34
|
+
<InputOTPSlot key={i} index={i} data-testid={`otp-slot-${i}`} />
|
|
35
|
+
))}
|
|
36
|
+
</InputOTPGroup>
|
|
37
|
+
</InputOTP>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('InputOTP Components', () => {
|
|
42
|
+
describe('InputOTP Root', () => {
|
|
43
|
+
it('renders a textbox with the given maxLength', () => {
|
|
44
|
+
render(<ControlledOTP maxLength={6} />)
|
|
45
|
+
|
|
46
|
+
const input = screen.getByRole('textbox')
|
|
47
|
+
expect(input).toBeInTheDocument()
|
|
48
|
+
expect(input).toHaveAttribute('maxlength', '6')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('applies moonui-theme to the container', () => {
|
|
52
|
+
const { container } = render(<ControlledOTP />)
|
|
53
|
+
|
|
54
|
+
expect(container.querySelector('.moonui-theme')).toBeInTheDocument()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('merges custom containerClassName', () => {
|
|
58
|
+
const { container } = render(
|
|
59
|
+
<ControlledOTP containerClassName="custom-container" />
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const otpContainer = container.querySelector('.custom-container')
|
|
63
|
+
expect(otpContainer).toBeInTheDocument()
|
|
64
|
+
expect(otpContainer).toHaveClass('flex', 'items-center', 'gap-2')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('forwards ref to the underlying input', () => {
|
|
68
|
+
const ref = React.createRef<HTMLInputElement>()
|
|
69
|
+
render(
|
|
70
|
+
<InputOTP ref={ref} maxLength={4}>
|
|
71
|
+
<InputOTPGroup>
|
|
72
|
+
<InputOTPSlot index={0} />
|
|
73
|
+
</InputOTPGroup>
|
|
74
|
+
</InputOTP>
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('maintains displayName', () => {
|
|
81
|
+
expect(InputOTP.displayName).toBe('InputOTP')
|
|
82
|
+
expect(InputOTPGroup.displayName).toBe('InputOTPGroup')
|
|
83
|
+
expect(InputOTPSlot.displayName).toBe('InputOTPSlot')
|
|
84
|
+
expect(InputOTPSeparator.displayName).toBe('InputOTPSeparator')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('passes through aria-label to the input', () => {
|
|
88
|
+
render(<ControlledOTP aria-label="One-time password" />)
|
|
89
|
+
|
|
90
|
+
expect(screen.getByRole('textbox')).toHaveAttribute(
|
|
91
|
+
'aria-label',
|
|
92
|
+
'One-time password'
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('disables the input when disabled prop is set', () => {
|
|
97
|
+
render(
|
|
98
|
+
<InputOTP maxLength={4} disabled>
|
|
99
|
+
<InputOTPGroup>
|
|
100
|
+
<InputOTPSlot index={0} />
|
|
101
|
+
</InputOTPGroup>
|
|
102
|
+
</InputOTP>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
expect(screen.getByRole('textbox')).toBeDisabled()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('Value Entry', () => {
|
|
110
|
+
it('calls onChange and renders characters into slots when typing', () => {
|
|
111
|
+
const handleChange = jest.fn()
|
|
112
|
+
render(<ControlledOTP onChange={handleChange} />)
|
|
113
|
+
|
|
114
|
+
const input = screen.getByRole('textbox')
|
|
115
|
+
fireEvent.change(input, { target: { value: '123' } })
|
|
116
|
+
|
|
117
|
+
expect(handleChange).toHaveBeenLastCalledWith('123')
|
|
118
|
+
expect(screen.getByTestId('otp-slot-0')).toHaveTextContent('1')
|
|
119
|
+
expect(screen.getByTestId('otp-slot-1')).toHaveTextContent('2')
|
|
120
|
+
expect(screen.getByTestId('otp-slot-2')).toHaveTextContent('3')
|
|
121
|
+
expect(screen.getByTestId('otp-slot-3')).toHaveTextContent('')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('supports pasting a full code at once', () => {
|
|
125
|
+
const handleChange = jest.fn()
|
|
126
|
+
render(<ControlledOTP onChange={handleChange} />)
|
|
127
|
+
|
|
128
|
+
// Paste davranışı: input değeri tek seferde tamamen değişir
|
|
129
|
+
const input = screen.getByRole('textbox')
|
|
130
|
+
fireEvent.change(input, { target: { value: '123456' } })
|
|
131
|
+
|
|
132
|
+
expect(handleChange).toHaveBeenLastCalledWith('123456')
|
|
133
|
+
expect(screen.getByTestId('otp-slot-5')).toHaveTextContent('6')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('does not exceed maxLength', () => {
|
|
137
|
+
const handleChange = jest.fn()
|
|
138
|
+
render(<ControlledOTP maxLength={4} onChange={handleChange} />)
|
|
139
|
+
|
|
140
|
+
const input = screen.getByRole('textbox')
|
|
141
|
+
fireEvent.change(input, { target: { value: '12345' } })
|
|
142
|
+
|
|
143
|
+
// input-otp maxLength üstünü keser veya reddeder — 5 karakterlik değer asla yayılmaz
|
|
144
|
+
expect(handleChange).not.toHaveBeenCalledWith('12345')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('rejects non-matching characters when pattern is digits-only', () => {
|
|
148
|
+
const handleChange = jest.fn()
|
|
149
|
+
render(<ControlledOTP pattern={REGEXP_ONLY_DIGITS} onChange={handleChange} />)
|
|
150
|
+
|
|
151
|
+
const input = screen.getByRole('textbox')
|
|
152
|
+
fireEvent.change(input, { target: { value: 'abc' } })
|
|
153
|
+
|
|
154
|
+
expect(handleChange).not.toHaveBeenCalled()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('InputOTPSlot', () => {
|
|
159
|
+
it('renders base styles with token-based border classes', () => {
|
|
160
|
+
render(<ControlledOTP />)
|
|
161
|
+
|
|
162
|
+
const slot = screen.getByTestId('otp-slot-0')
|
|
163
|
+
expect(slot).toHaveClass('border-y', 'border-r', 'border-input')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('applies custom className', () => {
|
|
167
|
+
render(
|
|
168
|
+
<InputOTP maxLength={2}>
|
|
169
|
+
<InputOTPGroup>
|
|
170
|
+
<InputOTPSlot index={0} className="custom-slot" data-testid="slot" />
|
|
171
|
+
</InputOTPGroup>
|
|
172
|
+
</InputOTP>
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
expect(screen.getByTestId('slot')).toHaveClass('custom-slot')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('marks the active slot with data-active and ring styles on focus', () => {
|
|
179
|
+
render(<ControlledOTP />)
|
|
180
|
+
|
|
181
|
+
const input = screen.getByRole('textbox')
|
|
182
|
+
fireEvent.focus(input)
|
|
183
|
+
|
|
184
|
+
const slot = screen.getByTestId('otp-slot-0')
|
|
185
|
+
expect(slot).toHaveAttribute('data-active')
|
|
186
|
+
expect(slot).toHaveClass('ring-2', 'ring-ring')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('exposes active ring styling through inputOTPSlotVariants', () => {
|
|
190
|
+
// Aktif slot vurgusu token tabanlı ring sınıflarını içermeli
|
|
191
|
+
expect(inputOTPSlotVariants({ isActive: true })).toContain('ring-2')
|
|
192
|
+
expect(inputOTPSlotVariants({ isActive: true })).toContain('ring-ring')
|
|
193
|
+
expect(inputOTPSlotVariants({ isActive: false })).not.toContain('ring-2')
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('InputOTPSeparator', () => {
|
|
198
|
+
it('renders with separator role', () => {
|
|
199
|
+
render(
|
|
200
|
+
<InputOTP maxLength={4}>
|
|
201
|
+
<InputOTPGroup>
|
|
202
|
+
<InputOTPSlot index={0} />
|
|
203
|
+
</InputOTPGroup>
|
|
204
|
+
<InputOTPSeparator data-testid="separator" />
|
|
205
|
+
<InputOTPGroup>
|
|
206
|
+
<InputOTPSlot index={1} />
|
|
207
|
+
</InputOTPGroup>
|
|
208
|
+
</InputOTP>
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const separator = screen.getByTestId('separator')
|
|
212
|
+
expect(separator).toHaveAttribute('role', 'separator')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('applies custom className', () => {
|
|
216
|
+
render(
|
|
217
|
+
<InputOTP maxLength={2}>
|
|
218
|
+
<InputOTPGroup>
|
|
219
|
+
<InputOTPSlot index={0} />
|
|
220
|
+
</InputOTPGroup>
|
|
221
|
+
<InputOTPSeparator className="custom-separator" data-testid="separator" />
|
|
222
|
+
</InputOTP>
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
expect(screen.getByTestId('separator')).toHaveClass('custom-separator')
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import useEmblaCarousel, {
|
|
5
|
+
type UseEmblaCarouselType,
|
|
6
|
+
} from "embla-carousel-react";
|
|
7
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
8
|
+
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
import { cn } from "../../lib/utils";
|
|
11
|
+
import { Button } from "./button";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Premium Carousel Component
|
|
15
|
+
*
|
|
16
|
+
* Embla Carousel tabanlı, erişilebilir ve esnek carousel bileşeni.
|
|
17
|
+
* Yatay/dikey yön desteği, klavye navigasyonu ve plugin (autoplay vb.) desteği sunar.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type CarouselApi = UseEmblaCarouselType[1];
|
|
21
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
22
|
+
type CarouselOptions = UseCarouselParameters[0];
|
|
23
|
+
type CarouselPlugin = UseCarouselParameters[1];
|
|
24
|
+
|
|
25
|
+
export interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
26
|
+
/** Embla carousel seçenekleri (loop, align, axis vb.) */
|
|
27
|
+
opts?: CarouselOptions;
|
|
28
|
+
/** Embla plugin listesi (ör. autoplay) */
|
|
29
|
+
plugins?: CarouselPlugin;
|
|
30
|
+
/** Kaydırma yönü */
|
|
31
|
+
orientation?: "horizontal" | "vertical";
|
|
32
|
+
/** Embla API'sine dışarıdan erişmek için callback */
|
|
33
|
+
setApi?: (api: CarouselApi) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CarouselContextProps
|
|
37
|
+
extends Pick<CarouselProps, "opts" | "plugins" | "setApi"> {
|
|
38
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
39
|
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
40
|
+
scrollPrev: () => void;
|
|
41
|
+
scrollNext: () => void;
|
|
42
|
+
canScrollPrev: boolean;
|
|
43
|
+
canScrollNext: boolean;
|
|
44
|
+
orientation: "horizontal" | "vertical";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Carousel context hook'u — Carousel alt bileşenlerinin embla API'sine
|
|
51
|
+
* ve yön bilgisine erişmesini sağlar.
|
|
52
|
+
*/
|
|
53
|
+
function useCarousel() {
|
|
54
|
+
const context = React.useContext(CarouselContext);
|
|
55
|
+
|
|
56
|
+
if (!context) {
|
|
57
|
+
throw new Error("useCarousel must be used within a <Carousel />");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* -------------------------------------------------------------------------------------------------
|
|
64
|
+
* Carousel Root
|
|
65
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
66
|
+
const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
|
|
67
|
+
(
|
|
68
|
+
{
|
|
69
|
+
orientation = "horizontal",
|
|
70
|
+
opts,
|
|
71
|
+
setApi,
|
|
72
|
+
plugins,
|
|
73
|
+
className,
|
|
74
|
+
children,
|
|
75
|
+
...props
|
|
76
|
+
},
|
|
77
|
+
ref
|
|
78
|
+
) => {
|
|
79
|
+
// Yön bilgisi opts.axis ile de verilebilir — orientation prop'u öncelikli
|
|
80
|
+
const resolvedOrientation =
|
|
81
|
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal");
|
|
82
|
+
|
|
83
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
84
|
+
{
|
|
85
|
+
...opts,
|
|
86
|
+
axis: resolvedOrientation === "horizontal" ? "x" : "y",
|
|
87
|
+
},
|
|
88
|
+
plugins
|
|
89
|
+
);
|
|
90
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
91
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
92
|
+
|
|
93
|
+
// Embla "select" olayında ileri/geri butonlarının durumunu güncelle
|
|
94
|
+
const onSelect = React.useCallback((emblaApi: CarouselApi) => {
|
|
95
|
+
if (!emblaApi) return;
|
|
96
|
+
setCanScrollPrev(emblaApi.canScrollPrev());
|
|
97
|
+
setCanScrollNext(emblaApi.canScrollNext());
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const scrollPrev = React.useCallback(() => {
|
|
101
|
+
api?.scrollPrev();
|
|
102
|
+
}, [api]);
|
|
103
|
+
|
|
104
|
+
const scrollNext = React.useCallback(() => {
|
|
105
|
+
api?.scrollNext();
|
|
106
|
+
}, [api]);
|
|
107
|
+
|
|
108
|
+
// Klavye navigasyonu: yatayda sol/sağ, dikeyde yukarı/aşağı ok tuşları
|
|
109
|
+
const handleKeyDown = React.useCallback(
|
|
110
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
111
|
+
const prevKey =
|
|
112
|
+
resolvedOrientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
|
|
113
|
+
const nextKey =
|
|
114
|
+
resolvedOrientation === "horizontal" ? "ArrowRight" : "ArrowDown";
|
|
115
|
+
|
|
116
|
+
if (event.key === prevKey) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
scrollPrev();
|
|
119
|
+
} else if (event.key === nextKey) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
scrollNext();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[resolvedOrientation, scrollPrev, scrollNext]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
if (!api || !setApi) return;
|
|
129
|
+
setApi(api);
|
|
130
|
+
}, [api, setApi]);
|
|
131
|
+
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
if (!api) return;
|
|
134
|
+
|
|
135
|
+
onSelect(api);
|
|
136
|
+
api.on("reInit", onSelect);
|
|
137
|
+
api.on("select", onSelect);
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
api.off("reInit", onSelect);
|
|
141
|
+
api.off("select", onSelect);
|
|
142
|
+
};
|
|
143
|
+
}, [api, onSelect]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<CarouselContext.Provider
|
|
147
|
+
value={{
|
|
148
|
+
carouselRef,
|
|
149
|
+
api,
|
|
150
|
+
opts,
|
|
151
|
+
scrollPrev,
|
|
152
|
+
scrollNext,
|
|
153
|
+
canScrollPrev,
|
|
154
|
+
canScrollNext,
|
|
155
|
+
orientation: resolvedOrientation,
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
<div
|
|
159
|
+
ref={ref}
|
|
160
|
+
onKeyDownCapture={handleKeyDown}
|
|
161
|
+
className={cn("moonui-theme", "relative", className)}
|
|
162
|
+
role="region"
|
|
163
|
+
aria-roledescription="carousel"
|
|
164
|
+
{...props}
|
|
165
|
+
>
|
|
166
|
+
{children}
|
|
167
|
+
</div>
|
|
168
|
+
</CarouselContext.Provider>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
Carousel.displayName = "Carousel";
|
|
173
|
+
|
|
174
|
+
/* -------------------------------------------------------------------------------------------------
|
|
175
|
+
* CarouselContent
|
|
176
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
177
|
+
const carouselContentVariants = cva("flex", {
|
|
178
|
+
variants: {
|
|
179
|
+
orientation: {
|
|
180
|
+
horizontal: "-ml-4",
|
|
181
|
+
vertical: "-mt-4 flex-col",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
defaultVariants: {
|
|
185
|
+
orientation: "horizontal",
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export interface CarouselContentProps
|
|
190
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
191
|
+
Omit<VariantProps<typeof carouselContentVariants>, "orientation"> {}
|
|
192
|
+
|
|
193
|
+
const CarouselContent = React.forwardRef<HTMLDivElement, CarouselContentProps>(
|
|
194
|
+
({ className, ...props }, ref) => {
|
|
195
|
+
const { carouselRef, orientation } = useCarousel();
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div ref={carouselRef} className="overflow-hidden">
|
|
199
|
+
<div
|
|
200
|
+
ref={ref}
|
|
201
|
+
className={cn(carouselContentVariants({ orientation }), className)}
|
|
202
|
+
{...props}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
CarouselContent.displayName = "CarouselContent";
|
|
209
|
+
|
|
210
|
+
/* -------------------------------------------------------------------------------------------------
|
|
211
|
+
* CarouselItem
|
|
212
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
213
|
+
const carouselItemVariants = cva("min-w-0 shrink-0 grow-0 basis-full", {
|
|
214
|
+
variants: {
|
|
215
|
+
orientation: {
|
|
216
|
+
horizontal: "pl-4",
|
|
217
|
+
vertical: "pt-4",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
defaultVariants: {
|
|
221
|
+
orientation: "horizontal",
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
export interface CarouselItemProps
|
|
226
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
227
|
+
Omit<VariantProps<typeof carouselItemVariants>, "orientation"> {}
|
|
228
|
+
|
|
229
|
+
const CarouselItem = React.forwardRef<HTMLDivElement, CarouselItemProps>(
|
|
230
|
+
({ className, ...props }, ref) => {
|
|
231
|
+
const { orientation } = useCarousel();
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div
|
|
235
|
+
ref={ref}
|
|
236
|
+
role="group"
|
|
237
|
+
aria-roledescription="slide"
|
|
238
|
+
className={cn(carouselItemVariants({ orientation }), className)}
|
|
239
|
+
{...props}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
CarouselItem.displayName = "CarouselItem";
|
|
245
|
+
|
|
246
|
+
/* -------------------------------------------------------------------------------------------------
|
|
247
|
+
* CarouselPrevious
|
|
248
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
249
|
+
const CarouselPrevious = React.forwardRef<
|
|
250
|
+
HTMLButtonElement,
|
|
251
|
+
React.ComponentProps<typeof Button>
|
|
252
|
+
>(({ className, variant = "outline", size = "icon-sm", ...props }, ref) => {
|
|
253
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<Button
|
|
257
|
+
ref={ref}
|
|
258
|
+
variant={variant}
|
|
259
|
+
size={size}
|
|
260
|
+
rounded="full"
|
|
261
|
+
className={cn(
|
|
262
|
+
"absolute",
|
|
263
|
+
orientation === "horizontal"
|
|
264
|
+
? "-left-12 top-1/2 -translate-y-1/2"
|
|
265
|
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
266
|
+
className
|
|
267
|
+
)}
|
|
268
|
+
disabled={!canScrollPrev}
|
|
269
|
+
onClick={scrollPrev}
|
|
270
|
+
{...props}
|
|
271
|
+
>
|
|
272
|
+
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
|
273
|
+
<span className="sr-only">Previous slide</span>
|
|
274
|
+
</Button>
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
CarouselPrevious.displayName = "CarouselPrevious";
|
|
278
|
+
|
|
279
|
+
/* -------------------------------------------------------------------------------------------------
|
|
280
|
+
* CarouselNext
|
|
281
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
282
|
+
const CarouselNext = React.forwardRef<
|
|
283
|
+
HTMLButtonElement,
|
|
284
|
+
React.ComponentProps<typeof Button>
|
|
285
|
+
>(({ className, variant = "outline", size = "icon-sm", ...props }, ref) => {
|
|
286
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<Button
|
|
290
|
+
ref={ref}
|
|
291
|
+
variant={variant}
|
|
292
|
+
size={size}
|
|
293
|
+
rounded="full"
|
|
294
|
+
className={cn(
|
|
295
|
+
"absolute",
|
|
296
|
+
orientation === "horizontal"
|
|
297
|
+
? "-right-12 top-1/2 -translate-y-1/2"
|
|
298
|
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
299
|
+
className
|
|
300
|
+
)}
|
|
301
|
+
disabled={!canScrollNext}
|
|
302
|
+
onClick={scrollNext}
|
|
303
|
+
{...props}
|
|
304
|
+
>
|
|
305
|
+
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
|
306
|
+
<span className="sr-only">Next slide</span>
|
|
307
|
+
</Button>
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
CarouselNext.displayName = "CarouselNext";
|
|
311
|
+
|
|
312
|
+
export {
|
|
313
|
+
type CarouselApi,
|
|
314
|
+
type CarouselOptions,
|
|
315
|
+
type CarouselPlugin,
|
|
316
|
+
Carousel,
|
|
317
|
+
CarouselContent,
|
|
318
|
+
CarouselItem,
|
|
319
|
+
CarouselPrevious,
|
|
320
|
+
CarouselNext,
|
|
321
|
+
useCarousel,
|
|
322
|
+
carouselContentVariants,
|
|
323
|
+
carouselItemVariants,
|
|
324
|
+
};
|
|
@@ -98,6 +98,23 @@ export {
|
|
|
98
98
|
CardZipInput as MoonUICardZipInput,
|
|
99
99
|
} from "./card-input";
|
|
100
100
|
|
|
101
|
+
// Carousel
|
|
102
|
+
export {
|
|
103
|
+
Carousel as MoonUICarousel,
|
|
104
|
+
CarouselContent as MoonUICarouselContent,
|
|
105
|
+
CarouselItem as MoonUICarouselItem,
|
|
106
|
+
CarouselPrevious as MoonUICarouselPrevious,
|
|
107
|
+
CarouselNext as MoonUICarouselNext,
|
|
108
|
+
carouselContentVariants as moonUICarouselContentVariants,
|
|
109
|
+
carouselItemVariants as moonUICarouselItemVariants,
|
|
110
|
+
useCarousel,
|
|
111
|
+
} from "./carousel";
|
|
112
|
+
|
|
113
|
+
export type {
|
|
114
|
+
CarouselProps as MoonUICarouselProps,
|
|
115
|
+
CarouselApi as MoonUICarouselApi,
|
|
116
|
+
} from "./carousel";
|
|
117
|
+
|
|
101
118
|
// Checkbox
|
|
102
119
|
export {
|
|
103
120
|
Checkbox as MoonUICheckbox,
|
|
@@ -220,6 +237,19 @@ export type {
|
|
|
220
237
|
InputProps as MoonUIInputProps,
|
|
221
238
|
} from "./input";
|
|
222
239
|
|
|
240
|
+
// InputOTP
|
|
241
|
+
export {
|
|
242
|
+
InputOTP as MoonUIInputOTP,
|
|
243
|
+
InputOTPGroup as MoonUIInputOTPGroup,
|
|
244
|
+
InputOTPSlot as MoonUIInputOTPSlot,
|
|
245
|
+
InputOTPSeparator as MoonUIInputOTPSeparator,
|
|
246
|
+
inputOTPSlotVariants as moonUIInputOTPSlotVariants,
|
|
247
|
+
} from "./input-otp";
|
|
248
|
+
|
|
249
|
+
export type {
|
|
250
|
+
InputOTPProps as MoonUIInputOTPProps,
|
|
251
|
+
} from "./input-otp";
|
|
252
|
+
|
|
223
253
|
// Label
|
|
224
254
|
export { Label as MoonUILabel } from "./label";
|
|
225
255
|
|
|
@@ -404,6 +434,7 @@ export * from "./breadcrumb";
|
|
|
404
434
|
export * from "./button";
|
|
405
435
|
export * from "./card";
|
|
406
436
|
export * from "./card-input";
|
|
437
|
+
export * from "./carousel";
|
|
407
438
|
export * from "./checkbox";
|
|
408
439
|
export * from "./collapsible";
|
|
409
440
|
export * from "./color-picker";
|
|
@@ -416,6 +447,7 @@ export * from "./file-upload";
|
|
|
416
447
|
export * from "./gesture-drawer";
|
|
417
448
|
export * from "./github-stars";
|
|
418
449
|
export * from "./input";
|
|
450
|
+
export * from "./input-otp";
|
|
419
451
|
export * from "./label";
|
|
420
452
|
export * from "./locked-component";
|
|
421
453
|
export * from "./moon-logo";
|