@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @srcroot/ui
2
2
 
3
- A **shadcn-like** React UI library for building accessible, polymorphic web applications.
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.27",
4
- "description": "A shadcn-style CLI UI library with polymorphic, accessible React components",
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"
@@ -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: (index: number) => void
7
+ setCurrentIndex: React.Dispatch<React.SetStateAction<number>>
7
8
  itemsCount: number
8
- setItemsCount: (count: number) => void
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
- const [currentIndex, setCurrentIndex] = React.useState(0)
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 > 0) {
40
- const interval = setInterval(() => {
41
- setCurrentIndex((prev) => {
42
- if (prev >= itemsCount - 1) {
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 () => clearInterval(interval)
40
+ return () => {
41
+ if (intervalRef.current) clearInterval(intervalRef.current)
42
+ }
49
43
  }
50
- }, [autoPlay, itemsCount, loop])
44
+ }, [autoPlay, itemsCount])
51
45
 
52
46
  return (
53
- <CarouselContext.Provider value={{ currentIndex, setCurrentIndex, itemsCount, setItemsCount }}>
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 childrenArray = React.Children.toArray(children)
75
+ const items = React.Children.toArray(children)
75
76
 
76
77
  React.useEffect(() => {
77
- context.setItemsCount(childrenArray.length)
78
- }, [childrenArray.length, context])
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 transition-transform duration-300 ease-in-out"
84
- style={{ transform: `translateX(-${context.currentIndex * 100}%)` }}
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
- {children}
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 canGoPrev = context.currentIndex > 0
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
- disabled={!canGoPrev}
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 canGoNext = context.currentIndex < context.itemsCount - 1
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
- disabled={!canGoNext}
157
- onClick={() => context.setCurrentIndex(context.currentIndex + 1)}
202
+ onClick={handleNext}
158
203
  aria-label="Next slide"
159
204
  {...props}
160
205
  >
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import { LuPanelLeft } from "react-icons/lu"
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="group peer hidden md:block text-sidebar-foreground"
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
- <LuPanelLeft />
239
+ <PanelLeft />
237
240
  <span className="sr-only">Toggle Sidebar</span>
238
241
  </Button>
239
242
  )