@djangocfg/ui-tools 2.1.120 → 2.1.123

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.120",
3
+ "version": "2.1.123",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -62,8 +62,8 @@
62
62
  "check": "tsc --noEmit"
63
63
  },
64
64
  "peerDependencies": {
65
- "@djangocfg/i18n": "^2.1.120",
66
- "@djangocfg/ui-core": "^2.1.120",
65
+ "@djangocfg/i18n": "^2.1.123",
66
+ "@djangocfg/ui-core": "^2.1.123",
67
67
  "lucide-react": "^0.545.0",
68
68
  "react": "^19.0.0",
69
69
  "react-dom": "^19.0.0",
@@ -95,10 +95,10 @@
95
95
  "@maplibre/maplibre-gl-geocoder": "^1.7.0"
96
96
  },
97
97
  "devDependencies": {
98
- "@djangocfg/i18n": "^2.1.120",
98
+ "@djangocfg/i18n": "^2.1.123",
99
99
  "@djangocfg/playground": "workspace:*",
100
- "@djangocfg/typescript-config": "^2.1.120",
101
- "@djangocfg/ui-core": "^2.1.120",
100
+ "@djangocfg/typescript-config": "^2.1.123",
101
+ "@djangocfg/ui-core": "^2.1.123",
102
102
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
103
103
  "@types/node": "^24.7.2",
104
104
  "@types/react": "^19.1.0",
@@ -1,5 +1,5 @@
1
1
  import { defineStory, useBoolean, useSelect, useNumber } from '@djangocfg/playground';
2
- import { Gallery, GalleryCompact, type GalleryMediaItem } from './index';
2
+ import { Gallery, GalleryCompact, type GalleryMediaItem, type GalleryAspectRatio } from './index';
3
3
 
4
4
  // Sample images for testing
5
5
  const SAMPLE_IMAGES: GalleryMediaItem[] = [
@@ -185,23 +185,29 @@ export const Interactive = () => {
185
185
  description: 'Gallery preview display mode',
186
186
  });
187
187
 
188
+ const [aspectRatio] = useSelect('aspectRatio', {
189
+ options: ['auto', '16/9', '4/3', '3/2', '1/1'] as const,
190
+ defaultValue: '4/3' as GalleryAspectRatio,
191
+ label: 'Aspect Ratio',
192
+ description: 'Image container aspect ratio',
193
+ });
194
+
188
195
  return (
189
196
  <div className="space-y-8">
190
197
  {/* GalleryCompact */}
191
198
  <div>
192
199
  <h3 className="text-lg font-semibold mb-4">GalleryCompact</h3>
193
200
  <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
194
- <div className="rounded-xl border border-border overflow-hidden">
195
- <div className="aspect-[4/3]">
196
- <GalleryCompact
197
- images={SAMPLE_IMAGES}
198
- showDots={showDots}
199
- showArrows={showArrows}
200
- enableZoom={enableZoom}
201
- showCounter={showCounter}
202
- maxDots={maxDots}
203
- />
204
- </div>
201
+ <div className="rounded-xl border border-border overflow-hidden bg-black">
202
+ <GalleryCompact
203
+ images={SAMPLE_IMAGES}
204
+ aspectRatio={aspectRatio}
205
+ showDots={showDots}
206
+ showArrows={showArrows}
207
+ enableZoom={enableZoom}
208
+ showCounter={showCounter}
209
+ maxDots={maxDots}
210
+ />
205
211
  <div className="p-4">
206
212
  <h3 className="font-medium">Interactive Card</h3>
207
213
  <p className="text-sm text-muted-foreground">Use controls on the right →</p>
@@ -12,9 +12,14 @@ import { ChevronLeft, ChevronRight, ImageOff } from 'lucide-react'
12
12
  import { GalleryMedia } from '../media'
13
13
  import type { GalleryMediaItem } from '../../types'
14
14
 
15
+ /** Preset aspect ratios for common use cases */
16
+ export type GalleryAspectRatio = '16/9' | '4/3' | '3/2' | '1/1' | 'auto'
17
+
15
18
  export interface GalleryCompactProps {
16
19
  /** Array of images to display */
17
20
  images: GalleryMediaItem[]
21
+ /** Aspect ratio preset or 'auto' to fill parent (default: 'auto') */
22
+ aspectRatio?: GalleryAspectRatio
18
23
  /** Show dots indicator (default: true) */
19
24
  showDots?: boolean
20
25
  /** Max dots to show (default: 5) */
@@ -43,8 +48,18 @@ export interface GalleryCompactProps {
43
48
  * - Fills parent container
44
49
  * - Stops event propagation on navigation
45
50
  */
51
+ /** Map aspect ratio presets to CSS values */
52
+ const aspectRatioMap: Record<GalleryAspectRatio, string | undefined> = {
53
+ '16/9': '16 / 9',
54
+ '4/3': '4 / 3',
55
+ '3/2': '3 / 2',
56
+ '1/1': '1 / 1',
57
+ 'auto': undefined,
58
+ }
59
+
46
60
  export const GalleryCompact = memo(function GalleryCompact({
47
61
  images,
62
+ aspectRatio = 'auto',
48
63
  showDots = true,
49
64
  maxDots = 5,
50
65
  showCounter = false,
@@ -59,12 +74,20 @@ export const GalleryCompact = memo(function GalleryCompact({
59
74
  // Track if component is mounted (client-side) to avoid hydration mismatches
60
75
  const [isMounted, setIsMounted] = useState(false)
61
76
 
77
+ // Compute aspect ratio style - if set, we control height via aspect-ratio, otherwise fill parent
78
+ const aspectRatioStyle = aspectRatioMap[aspectRatio]
79
+ const containerStyle = aspectRatioStyle ? { aspectRatio: aspectRatioStyle } : undefined
80
+ const hasFixedAspect = aspectRatio !== 'auto'
81
+ const containerClass = hasFixedAspect ? 'relative w-full overflow-hidden' : 'relative w-full h-full overflow-hidden'
82
+
62
83
  useEffect(() => {
63
84
  setIsMounted(true)
64
85
  }, [])
65
86
 
66
87
  const total = images.length
67
88
  const hasMultiple = total > 1
89
+ // Stable key for carousel reset on images change
90
+ const carouselKey = images[0]?.id ?? 'empty'
68
91
 
69
92
  // Determine which images should be loaded (current + neighbors for smooth transition)
70
93
  const loadedIndices = useMemo(() => {
@@ -144,7 +167,10 @@ export const GalleryCompact = memo(function GalleryCompact({
144
167
  // Empty state
145
168
  if (total === 0) {
146
169
  return (
147
- <div className={cn('relative w-full h-full bg-muted flex items-center justify-center', className)}>
170
+ <div
171
+ className={cn(containerClass, 'bg-muted flex items-center justify-center', className)}
172
+ style={containerStyle}
173
+ >
148
174
  <ImageOff className="w-8 h-8 text-muted-foreground/50" />
149
175
  </div>
150
176
  )
@@ -154,7 +180,8 @@ export const GalleryCompact = memo(function GalleryCompact({
154
180
  if (!hasMultiple) {
155
181
  return (
156
182
  <div
157
- className={cn('relative w-full h-full overflow-hidden group', className)}
183
+ className={cn(containerClass, 'overflow-hidden group', className)}
184
+ style={containerStyle}
158
185
  onClick={onClick}
159
186
  >
160
187
  <div className={cn(
@@ -180,7 +207,8 @@ export const GalleryCompact = memo(function GalleryCompact({
180
207
  if (!isMounted) {
181
208
  return (
182
209
  <div
183
- className={cn('relative w-full h-full overflow-hidden', className)}
210
+ className={cn(containerClass, 'overflow-hidden', className)}
211
+ style={containerStyle}
184
212
  onClick={onClick}
185
213
  >
186
214
  <GalleryMedia
@@ -228,17 +256,19 @@ export const GalleryCompact = memo(function GalleryCompact({
228
256
 
229
257
  return (
230
258
  <div
231
- className={cn('relative w-full h-full group/gallery', className)}
259
+ className={cn(containerClass, 'group/gallery', className)}
260
+ style={containerStyle}
232
261
  onClick={handleClick}
233
262
  onMouseEnter={() => setIsHovered(true)}
234
263
  onMouseLeave={() => setIsHovered(false)}
235
264
  >
236
265
  <Carousel
266
+ key={carouselKey}
237
267
  setApi={setApi}
238
268
  opts={{
239
269
  loop: true,
240
270
  }}
241
- className="w-full h-full"
271
+ className="absolute inset-0"
242
272
  >
243
273
  <CarouselContent className="-ml-0 h-full">
244
274
  {images.map((image, index) => {
@@ -1 +1 @@
1
- export { GalleryCompact, type GalleryCompactProps } from './GalleryCompact'
1
+ export { GalleryCompact, type GalleryCompactProps, type GalleryAspectRatio } from './GalleryCompact'
@@ -2,7 +2,7 @@
2
2
  export { Gallery } from './Gallery'
3
3
 
4
4
  // Compact mode (for cards)
5
- export { GalleryCompact, type GalleryCompactProps } from './compact'
5
+ export { GalleryCompact, type GalleryCompactProps, type GalleryAspectRatio } from './compact'
6
6
 
7
7
  // Preview modes
8
8
  export { GalleryCarousel } from './preview'
@@ -9,7 +9,7 @@ import type { GalleryImageProps } from '../../types'
9
9
 
10
10
  /**
11
11
  * GalleryImage - Single image with loading state and error handling
12
- * Uses useImageLoader for preloading images before displaying
12
+ * Uses CSS background-image for reliable cover behavior
13
13
  */
14
14
  export const GalleryImage = memo(function GalleryImage({
15
15
  image,
@@ -25,6 +25,10 @@ export const GalleryImage = memo(function GalleryImage({
25
25
  const callbacks = useMemo(() => ({ onLoad, onError }), [onLoad, onError])
26
26
  const { isLoading, isLoaded, hasError } = useImageLoader(image.src, callbacks)
27
27
 
28
+ const backgroundStyle = useMemo(() =>
29
+ isLoaded ? { backgroundImage: `url(${image.src})` } : undefined
30
+ , [isLoaded, image.src])
31
+
28
32
  if (hasError) {
29
33
  return (
30
34
  <div
@@ -43,24 +47,16 @@ export const GalleryImage = memo(function GalleryImage({
43
47
 
44
48
  return (
45
49
  <div
46
- className={cn('relative overflow-hidden', className)}
47
- onClick={onClick}
48
- >
49
- {/* Loading skeleton */}
50
- {showLoading && isLoading && (
51
- <div className="absolute inset-0 bg-muted animate-pulse" />
50
+ className={cn(
51
+ 'w-full h-full bg-cover bg-center bg-no-repeat',
52
+ isLoading && 'bg-muted animate-pulse',
53
+ isLoaded && 'animate-in fade-in-0 duration-300',
54
+ className
52
55
  )}
53
-
54
- {/* Image - only render when preloaded */}
55
- {isLoaded && (
56
- <img
57
- src={image.src}
58
- alt={image.alt || 'Gallery image'}
59
- className="w-full h-full object-cover animate-in fade-in-0 duration-300"
60
- loading={priority ? 'eager' : 'lazy'}
61
- decoding="async"
62
- />
63
- )}
64
- </div>
56
+ style={backgroundStyle}
57
+ onClick={onClick}
58
+ role="img"
59
+ aria-label={image.alt || 'Gallery image'}
60
+ />
65
61
  )
66
62
  })
@@ -67,6 +67,8 @@ export const GalleryCarousel = memo(function GalleryCarousel({
67
67
  const [api, setApi] = React.useState<CarouselApi>()
68
68
  // Track if component is mounted (client-side) to avoid hydration mismatches
69
69
  const [isMounted, setIsMounted] = useState(false)
70
+ // Stable key for carousel reset on images change
71
+ const carouselKey = images[0]?.id ?? 'empty'
70
72
 
71
73
  useEffect(() => {
72
74
  setIsMounted(true)
@@ -246,6 +248,7 @@ export const GalleryCarousel = memo(function GalleryCarousel({
246
248
  return (
247
249
  <div className={cn('space-y-3', className)}>
248
250
  <Carousel
251
+ key={carouselKey}
249
252
  setApi={setApi}
250
253
  opts={{
251
254
  loop: true,
@@ -10,7 +10,7 @@ export {
10
10
  GalleryThumbnailsVirtual,
11
11
  GalleryLightbox,
12
12
  } from './components'
13
- export type { GalleryCompactProps, GalleryGridProps, GalleryGridLayout } from './components'
13
+ export type { GalleryCompactProps, GalleryAspectRatio, GalleryGridProps, GalleryGridLayout } from './components'
14
14
 
15
15
  // Hooks
16
16
  export {