@srcroot/ui 0.0.27 → 0.0.30
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/README.md +1 -1
- package/package.json +2 -2
- package/registry/carousel.tsx +86 -41
- package/registry/sidebar.tsx +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @srcroot/ui
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A UI library with polymorphic, accessible React components.
|
|
4
4
|
This library provides a collection of re-usable components that you can copy and paste into your apps.
|
|
5
5
|
|
|
6
6
|
## Features
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srcroot/ui",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.0.30",
|
|
4
|
+
"description": "A UI library with polymorphic, accessible React components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"srcroot-ui": "./dist/index.js"
|
package/registry/carousel.tsx
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
'use client'
|
|
1
2
|
import * as React from "react"
|
|
2
3
|
import { cn } from "@/lib/utils"
|
|
3
4
|
|
|
4
5
|
interface CarouselContextValue {
|
|
5
6
|
currentIndex: number
|
|
6
|
-
setCurrentIndex:
|
|
7
|
+
setCurrentIndex: React.Dispatch<React.SetStateAction<number>>
|
|
7
8
|
itemsCount: number
|
|
8
|
-
setItemsCount:
|
|
9
|
+
setItemsCount: React.Dispatch<React.SetStateAction<number>>
|
|
10
|
+
isTransitioning: boolean
|
|
11
|
+
setIsTransitioning: React.Dispatch<React.SetStateAction<boolean>>
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
const CarouselContext = React.createContext<CarouselContextValue | null>(null)
|
|
@@ -13,44 +16,42 @@ const CarouselContext = React.createContext<CarouselContextValue | null>(null)
|
|
|
13
16
|
interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
14
17
|
/** Auto-play interval in ms (0 to disable) */
|
|
15
18
|
autoPlay?: number
|
|
16
|
-
/** Loop back to start */
|
|
19
|
+
/** Loop back to start (ignored in this infinite implementation as it's always true-ish, but kept for API) */
|
|
17
20
|
loop?: boolean
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
|
-
* Carousel/Slider component
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* <Carousel>
|
|
25
|
-
* <CarouselContent>
|
|
26
|
-
* <CarouselItem>Slide 1</CarouselItem>
|
|
27
|
-
* <CarouselItem>Slide 2</CarouselItem>
|
|
28
|
-
* </CarouselContent>
|
|
29
|
-
* <CarouselPrevious />
|
|
30
|
-
* <CarouselNext />
|
|
31
|
-
* </Carousel>
|
|
24
|
+
* Carousel/Slider component with Infinite Looping
|
|
32
25
|
*/
|
|
33
26
|
const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
|
|
34
27
|
({ className, children, autoPlay = 0, loop = true, ...props }, ref) => {
|
|
35
|
-
|
|
28
|
+
// Start at 1 because 0 is the clone of the last item
|
|
29
|
+
const [currentIndex, setCurrentIndex] = React.useState(1)
|
|
36
30
|
const [itemsCount, setItemsCount] = React.useState(0)
|
|
31
|
+
const [isTransitioning, setIsTransitioning] = React.useState(true)
|
|
32
|
+
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
37
33
|
|
|
38
34
|
React.useEffect(() => {
|
|
39
|
-
if (autoPlay > 0 && itemsCount >
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return loop ? 0 : prev
|
|
44
|
-
}
|
|
45
|
-
return prev + 1
|
|
46
|
-
})
|
|
35
|
+
if (autoPlay > 0 && itemsCount > 1) {
|
|
36
|
+
intervalRef.current = setInterval(() => {
|
|
37
|
+
setIsTransitioning(true)
|
|
38
|
+
setCurrentIndex((prev) => prev + 1)
|
|
47
39
|
}, autoPlay)
|
|
48
|
-
return () =>
|
|
40
|
+
return () => {
|
|
41
|
+
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
42
|
+
}
|
|
49
43
|
}
|
|
50
|
-
}, [autoPlay, itemsCount
|
|
44
|
+
}, [autoPlay, itemsCount])
|
|
51
45
|
|
|
52
46
|
return (
|
|
53
|
-
<CarouselContext.Provider value={{
|
|
47
|
+
<CarouselContext.Provider value={{
|
|
48
|
+
currentIndex,
|
|
49
|
+
setCurrentIndex,
|
|
50
|
+
itemsCount,
|
|
51
|
+
setItemsCount,
|
|
52
|
+
isTransitioning,
|
|
53
|
+
setIsTransitioning
|
|
54
|
+
}}>
|
|
54
55
|
<div
|
|
55
56
|
ref={ref}
|
|
56
57
|
className={cn("relative", className)}
|
|
@@ -71,19 +72,59 @@ const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
|
|
|
71
72
|
const context = React.useContext(CarouselContext)
|
|
72
73
|
if (!context) throw new Error("CarouselContent must be used within Carousel")
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
+
const items = React.Children.toArray(children)
|
|
75
76
|
|
|
76
77
|
React.useEffect(() => {
|
|
77
|
-
context.setItemsCount(
|
|
78
|
-
}, [
|
|
78
|
+
context.setItemsCount(items.length)
|
|
79
|
+
}, [items.length, context])
|
|
80
|
+
|
|
81
|
+
// Clone first and last items for infinite loop illusion
|
|
82
|
+
const firstClone = items.length > 0 && React.isValidElement(items[0])
|
|
83
|
+
? React.cloneElement(items[0] as React.ReactElement, { key: "clone-first" })
|
|
84
|
+
: null
|
|
85
|
+
const lastClone = items.length > 0 && React.isValidElement(items[items.length - 1])
|
|
86
|
+
? React.cloneElement(items[items.length - 1] as React.ReactElement, { key: "clone-last" })
|
|
87
|
+
: null
|
|
88
|
+
|
|
89
|
+
// If we have items, prepend last-clone and append first-clone
|
|
90
|
+
const displayItems = items.length > 1 ? [lastClone, ...items, firstClone] : items
|
|
91
|
+
|
|
92
|
+
const handleTransitionEnd = () => {
|
|
93
|
+
if (items.length <= 1) return
|
|
94
|
+
|
|
95
|
+
// If reached the end clone (index = N + 1), snap back to first real item (index = 1)
|
|
96
|
+
if (context.currentIndex >= items.length + 1) {
|
|
97
|
+
context.setIsTransitioning(false)
|
|
98
|
+
context.setCurrentIndex(1)
|
|
99
|
+
}
|
|
100
|
+
// If reached the start clone (index = 0), snap forward to last real item (index = N)
|
|
101
|
+
else if (context.currentIndex <= 0) {
|
|
102
|
+
context.setIsTransitioning(false)
|
|
103
|
+
context.setCurrentIndex(items.length)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Re-enable transition after a snap (on next frame)
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
if (!context.isTransitioning) {
|
|
110
|
+
const timer = setTimeout(() => {
|
|
111
|
+
context.setIsTransitioning(true)
|
|
112
|
+
}, 50)
|
|
113
|
+
return () => clearTimeout(timer)
|
|
114
|
+
}
|
|
115
|
+
}, [context.isTransitioning, context])
|
|
79
116
|
|
|
80
117
|
return (
|
|
81
118
|
<div ref={ref} className={cn("overflow-hidden", className)} {...props}>
|
|
82
119
|
<div
|
|
83
|
-
className="flex
|
|
84
|
-
style={{
|
|
120
|
+
className="flex h-full w-full"
|
|
121
|
+
style={{
|
|
122
|
+
transform: `translateX(-${context.currentIndex * 100}%)`,
|
|
123
|
+
transition: context.isTransitioning ? 'transform 300ms ease-in-out' : 'none'
|
|
124
|
+
}}
|
|
125
|
+
onTransitionEnd={handleTransitionEnd}
|
|
85
126
|
>
|
|
86
|
-
{
|
|
127
|
+
{displayItems}
|
|
87
128
|
</div>
|
|
88
129
|
</div>
|
|
89
130
|
)
|
|
@@ -97,7 +138,7 @@ const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLD
|
|
|
97
138
|
ref={ref}
|
|
98
139
|
role="group"
|
|
99
140
|
aria-roledescription="slide"
|
|
100
|
-
className={cn("min-w-0 shrink-0 grow-0 basis-full", className)}
|
|
141
|
+
className={cn("min-w-0 h-full shrink-0 grow-0 basis-full", className)}
|
|
101
142
|
{...props}
|
|
102
143
|
/>
|
|
103
144
|
)
|
|
@@ -111,19 +152,21 @@ const CarouselPrevious = React.forwardRef<
|
|
|
111
152
|
const context = React.useContext(CarouselContext)
|
|
112
153
|
if (!context) throw new Error("CarouselPrevious must be used within Carousel")
|
|
113
154
|
|
|
114
|
-
const
|
|
155
|
+
const handlePrev = () => {
|
|
156
|
+
context.setIsTransitioning(true)
|
|
157
|
+
context.setCurrentIndex((prev) => prev - 1)
|
|
158
|
+
}
|
|
115
159
|
|
|
116
160
|
return (
|
|
117
161
|
<button
|
|
118
162
|
ref={ref}
|
|
119
163
|
type="button"
|
|
120
164
|
className={cn(
|
|
121
|
-
"absolute left-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center",
|
|
165
|
+
"absolute left-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
|
|
122
166
|
"hover:bg-accent disabled:opacity-50",
|
|
123
167
|
className
|
|
124
168
|
)}
|
|
125
|
-
|
|
126
|
-
onClick={() => context.setCurrentIndex(context.currentIndex - 1)}
|
|
169
|
+
onClick={handlePrev}
|
|
127
170
|
aria-label="Previous slide"
|
|
128
171
|
{...props}
|
|
129
172
|
>
|
|
@@ -142,19 +185,21 @@ const CarouselNext = React.forwardRef<
|
|
|
142
185
|
const context = React.useContext(CarouselContext)
|
|
143
186
|
if (!context) throw new Error("CarouselNext must be used within Carousel")
|
|
144
187
|
|
|
145
|
-
const
|
|
188
|
+
const handleNext = () => {
|
|
189
|
+
context.setIsTransitioning(true)
|
|
190
|
+
context.setCurrentIndex((prev) => prev + 1)
|
|
191
|
+
}
|
|
146
192
|
|
|
147
193
|
return (
|
|
148
194
|
<button
|
|
149
195
|
ref={ref}
|
|
150
196
|
type="button"
|
|
151
197
|
className={cn(
|
|
152
|
-
"absolute right-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center",
|
|
198
|
+
"absolute right-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
|
|
153
199
|
"hover:bg-accent disabled:opacity-50",
|
|
154
200
|
className
|
|
155
201
|
)}
|
|
156
|
-
|
|
157
|
-
onClick={() => context.setCurrentIndex(context.currentIndex + 1)}
|
|
202
|
+
onClick={handleNext}
|
|
158
203
|
aria-label="Next slide"
|
|
159
204
|
{...props}
|
|
160
205
|
>
|
package/registry/sidebar.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import {
|
|
4
|
+
import { PanelLeft } from "lucide-react"
|
|
5
5
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
6
6
|
|
|
7
7
|
import { cn } from "@/lib/utils"
|
|
@@ -168,7 +168,10 @@ const Sidebar = React.forwardRef<
|
|
|
168
168
|
return (
|
|
169
169
|
<div
|
|
170
170
|
ref={ref}
|
|
171
|
-
className=
|
|
171
|
+
className={cn(
|
|
172
|
+
"group peer hidden md:block text-sidebar-foreground",
|
|
173
|
+
className
|
|
174
|
+
)}
|
|
172
175
|
data-state={state}
|
|
173
176
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
|
174
177
|
data-variant={variant}
|
|
@@ -233,7 +236,7 @@ const SidebarTrigger = React.forwardRef<
|
|
|
233
236
|
}}
|
|
234
237
|
{...props}
|
|
235
238
|
>
|
|
236
|
-
<
|
|
239
|
+
<PanelLeft />
|
|
237
240
|
<span className="sr-only">Toggle Sidebar</span>
|
|
238
241
|
</Button>
|
|
239
242
|
)
|