@r2digisolutions/components 0.17.6 → 0.17.7

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.
@@ -25,6 +25,8 @@
25
25
  maxSizeMb?: number;
26
26
  /** Show image thumbnails / file cards for selected files. */
27
27
  showPreview?: boolean;
28
+ /** Append “multiple · max N MB” under the hint. Default true. */
29
+ showConstraints?: boolean;
28
30
  class?: string;
29
31
  ondropfiles?: (files: File[]) => void;
30
32
  onchange?: (files: File[]) => void;
@@ -41,6 +43,7 @@
41
43
  disabled = false,
42
44
  maxSizeMb = 25,
43
45
  showPreview = true,
46
+ showConstraints = true,
44
47
  class: className = '',
45
48
  ondropfiles,
46
49
  onchange,
@@ -239,8 +242,8 @@
239
242
  <div>
240
243
  <p class="text-sm font-medium text-primary">{label}</p>
241
244
  <p class="mt-0.5 text-xs text-muted">
242
- {hint}{#if multiple} · multiple{/if}
243
- {#if maxSizeMb} · max {maxSizeMb}MB{/if}
245
+ {hint}{#if showConstraints && multiple} · multiple{/if}
246
+ {#if showConstraints && maxSizeMb} · max {maxSizeMb}MB{/if}
244
247
  </p>
245
248
  </div>
246
249
  <input
@@ -16,6 +16,8 @@ interface DropZoneProps {
16
16
  maxSizeMb?: number;
17
17
  /** Show image thumbnails / file cards for selected files. */
18
18
  showPreview?: boolean;
19
+ /** Append “multiple · max N MB” under the hint. Default true. */
20
+ showConstraints?: boolean;
19
21
  class?: string;
20
22
  ondropfiles?: (files: File[]) => void;
21
23
  onchange?: (files: File[]) => void;
@@ -9,7 +9,7 @@ const meta = {
9
9
  argTypes: {
10
10
  variant: {
11
11
  control: 'select',
12
- options: ['default', 'featured', 'masonry']
12
+ options: ['default', 'featured', 'masonry', 'hero']
13
13
  }
14
14
  }
15
15
  };
@@ -23,3 +23,6 @@ export const Featured = {
23
23
  export const Masonry = {
24
24
  args: { variant: 'masonry' }
25
25
  };
26
+ export const Hero = {
27
+ args: { variant: 'hero' }
28
+ };
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import AspectRatio from '../../atoms/AspectRatio/AspectRatio.svelte';
3
4
  import ImageLightbox from '../ImageLightbox/ImageLightbox.svelte';
4
5
  import type { LightboxImage } from '../ImageLightbox/ImageLightbox.svelte';
@@ -11,14 +12,18 @@
11
12
  caption?: string;
12
13
  }
13
14
 
14
- type GalleryLayout = 'grid' | 'featured' | 'masonry';
15
+ export type GalleryLayout = 'grid' | 'featured' | 'masonry' | 'hero';
15
16
 
16
17
  interface ImageGalleryProps {
17
18
  images?: GalleryImage[];
18
19
  cols?: 2 | 3 | 4;
19
20
  layout?: GalleryLayout;
20
21
  showCaptions?: boolean;
22
+ /** Footer line under the gallery. Default true. */
23
+ showHint?: boolean;
24
+ hint?: string;
21
25
  class?: string;
26
+ empty?: Snippet;
22
27
  }
23
28
 
24
29
  const {
@@ -26,7 +31,10 @@
26
31
  cols = 3,
27
32
  layout = 'grid',
28
33
  showCaptions = false,
29
- class: className = ''
34
+ showHint = true,
35
+ hint,
36
+ class: className = '',
37
+ empty
30
38
  }: ImageGalleryProps = $props();
31
39
 
32
40
  let open = $state(false);
@@ -51,6 +59,12 @@
51
59
  : 'grid-cols-2 lg:grid-cols-3'
52
60
  );
53
61
 
62
+ const heroVisible = $derived(images.slice(0, layout === 'hero' && images.length > 4 ? 4 : images.length));
63
+ const heroExtra = $derived(Math.max(0, images.length - heroVisible.length));
64
+ const hintText = $derived(
65
+ hint ?? `${images.length} photos · click to open lightbox`
66
+ );
67
+
54
68
  const tones = [
55
69
  'from-slate-600 to-slate-800',
56
70
  'from-sky-500 to-indigo-700',
@@ -68,6 +82,19 @@
68
82
  function markFailed(id: string) {
69
83
  failed = { ...failed, [id]: true };
70
84
  }
85
+
86
+ const heroGridClass = $derived.by(() => {
87
+ const n = heroVisible.length;
88
+ if (n <= 1) return 'grid-cols-1';
89
+ if (n === 2) return 'grid-cols-2';
90
+ return 'grid-cols-2 grid-rows-2';
91
+ });
92
+
93
+ const heroMainSpan = $derived.by(() => {
94
+ const n = heroVisible.length;
95
+ if (n === 3) return 'row-span-2';
96
+ return '';
97
+ });
71
98
  </script>
72
99
 
73
100
  {#snippet tile(img: GalleryImage, i: number, ratio = 1, featured = false)}
@@ -82,10 +109,7 @@
82
109
  <AspectRatio {ratio}>
83
110
  {#if failed[img.id]}
84
111
  <div
85
- class={[
86
- 'flex h-full w-full items-end bg-gradient-to-br p-4',
87
- tones[i % tones.length]
88
- ]}
112
+ class={['flex h-full w-full items-end bg-gradient-to-br p-4', tones[i % tones.length]]}
89
113
  >
90
114
  <span class="text-sm font-medium text-white/90">{img.alt}</span>
91
115
  </div>
@@ -119,14 +143,70 @@
119
143
  </button>
120
144
  {/snippet}
121
145
 
122
- <div class={['w-full space-y-3', className]}>
123
- {#if images.length === 0}
146
+ {#snippet fillTile(img: GalleryImage, i: number, spanClass = '', extra = 0)}
147
+ <button
148
+ type="button"
149
+ class={[
150
+ 'group relative min-h-0 overflow-hidden rounded-xl border border-border bg-surface-overlay text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/40',
151
+ spanClass
152
+ ]}
153
+ onclick={() => openAt(i)}
154
+ >
155
+ {#if failed[img.id]}
156
+ <div class={['flex h-full w-full items-end bg-gradient-to-br p-4', tones[i % tones.length]]}>
157
+ <span class="text-sm font-medium text-white/90">{img.alt}</span>
158
+ </div>
159
+ {:else}
160
+ <img
161
+ src={img.src}
162
+ alt={img.alt}
163
+ loading="eager"
164
+ decoding="async"
165
+ class="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]"
166
+ onerror={() => markFailed(img.id)}
167
+ />
168
+ {/if}
124
169
  <div
125
- class="flex min-h-64 w-full items-center justify-center rounded-2xl border border-dashed border-border bg-surface-overlay px-6 text-sm text-muted"
170
+ class="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/45 via-transparent to-transparent opacity-0 transition group-hover:opacity-100"
171
+ ></div>
172
+ <span
173
+ class="absolute top-3 right-3 flex h-8 w-8 items-center justify-center rounded-full bg-black/45 text-white opacity-0 backdrop-blur-sm transition group-hover:opacity-100"
126
174
  >
127
- No images yet
175
+ <Expand class="h-4 w-4" />
176
+ </span>
177
+ {#if extra > 0}
178
+ <span
179
+ class="absolute inset-0 flex items-center justify-center bg-neutral-950/55 text-lg font-semibold text-white"
180
+ >
181
+ +{extra}
182
+ </span>
183
+ {/if}
184
+ </button>
185
+ {/snippet}
186
+
187
+ <div class={['w-full space-y-3', className]}>
188
+ {#if images.length === 0}
189
+ {#if empty}
190
+ {@render empty()}
191
+ {:else}
192
+ <div
193
+ class="flex min-h-64 w-full items-center justify-center rounded-2xl border border-dashed border-border bg-surface-overlay px-6 text-sm text-muted"
194
+ >
195
+ No images yet
196
+ </div>
197
+ {/if}
198
+ {:else if layout === 'hero'}
199
+ <div class={['grid h-72 gap-2 sm:h-80', heroGridClass]}>
200
+ {#each heroVisible as img, i (img.id)}
201
+ {@render fillTile(
202
+ img,
203
+ i,
204
+ i === 0 ? heroMainSpan : '',
205
+ i === heroVisible.length - 1 ? heroExtra : 0
206
+ )}
207
+ {/each}
128
208
  </div>
129
- {:else if layout === 'featured' && images.length > 0}
209
+ {:else if layout === 'featured'}
130
210
  <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
131
211
  {@render tile(images[0], 0, 1, true)}
132
212
  {#each images.slice(1) as img, i (img.id)}
@@ -162,7 +242,7 @@
162
242
  />
163
243
  {/if}
164
244
  <span
165
- class="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full bg-black/45 text-white opacity-0 backdrop-blur-sm transition group-hover:opacity-100"
245
+ class="absolute top-3 right-3 flex h-8 w-8 items-center justify-center rounded-full bg-black/45 text-white opacity-0 backdrop-blur-sm transition group-hover:opacity-100"
166
246
  >
167
247
  <Expand class="h-4 w-4" />
168
248
  </span>
@@ -177,8 +257,8 @@
177
257
  </div>
178
258
  {/if}
179
259
 
180
- {#if images.length}
181
- <p class="text-xs text-muted">{images.length} photos · click to open lightbox</p>
260
+ {#if images.length && showHint}
261
+ <p class="text-xs text-muted">{hintText}</p>
182
262
  {/if}
183
263
 
184
264
  {#if lightboxImages.length}
@@ -1,16 +1,21 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export interface GalleryImage {
2
3
  id: string;
3
4
  src: string;
4
5
  alt: string;
5
6
  caption?: string;
6
7
  }
7
- type GalleryLayout = 'grid' | 'featured' | 'masonry';
8
+ export type GalleryLayout = 'grid' | 'featured' | 'masonry' | 'hero';
8
9
  interface ImageGalleryProps {
9
10
  images?: GalleryImage[];
10
11
  cols?: 2 | 3 | 4;
11
12
  layout?: GalleryLayout;
12
13
  showCaptions?: boolean;
14
+ /** Footer line under the gallery. Default true. */
15
+ showHint?: boolean;
16
+ hint?: string;
13
17
  class?: string;
18
+ empty?: Snippet;
14
19
  }
15
20
  declare const ImageGallery: import("svelte").Component<ImageGalleryProps, {}, "">;
16
21
  type ImageGallery = ReturnType<typeof ImageGallery>;
@@ -3,7 +3,7 @@
3
3
  import type { GalleryImage } from './ImageGallery.svelte';
4
4
 
5
5
  interface Props {
6
- variant?: 'default' | 'featured' | 'masonry';
6
+ variant?: 'default' | 'featured' | 'masonry' | 'hero';
7
7
  }
8
8
 
9
9
  let { variant = 'default' }: Props = $props();
@@ -60,6 +60,8 @@
60
60
  <ImageGallery {images} layout="featured" showCaptions cols={4} class="w-full" />
61
61
  {:else if variant === 'masonry'}
62
62
  <ImageGallery {images} layout="masonry" showCaptions class="w-full" />
63
+ {:else if variant === 'hero'}
64
+ <ImageGallery {images} layout="hero" class="w-full" />
63
65
  {:else}
64
66
  <ImageGallery {images} cols={3} showCaptions class="w-full" />
65
67
  {/if}
@@ -1,5 +1,5 @@
1
1
  interface Props {
2
- variant?: 'default' | 'featured' | 'masonry';
2
+ variant?: 'default' | 'featured' | 'masonry' | 'hero';
3
3
  }
4
4
  declare const ImageGalleryStory: import("svelte").Component<Props, {}, "">;
5
5
  type ImageGalleryStory = ReturnType<typeof ImageGalleryStory>;
@@ -5,6 +5,7 @@
5
5
  import Badge from '../../atoms/Badge/Badge.svelte';
6
6
 
7
7
  export type StatCardTrend = 'up' | 'down' | 'neutral';
8
+ export type StatCardSize = 'sm' | 'md';
8
9
 
9
10
  interface StatCardProps {
10
11
  label: string;
@@ -15,6 +16,7 @@
15
16
  badge?: string;
16
17
  badgeVariant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
17
18
  variant?: CardVariant;
19
+ size?: StatCardSize;
18
20
  class?: string;
19
21
  icon?: Snippet;
20
22
  onclick?: (e: MouseEvent) => void;
@@ -29,39 +31,70 @@
29
31
  badge,
30
32
  badgeVariant = 'primary',
31
33
  variant = 'default',
34
+ size = 'md',
32
35
  class: className = '',
33
36
  icon,
34
37
  onclick
35
38
  }: StatCardProps = $props();
39
+
40
+ const compact = $derived(size === 'sm');
36
41
  </script>
37
42
 
38
- <Card {variant} padding="md" hoverable={!!onclick} {onclick} class={className} chrome={false}>
39
- <div class="flex items-start justify-between gap-3">
43
+ <Card
44
+ {variant}
45
+ padding={compact ? 'sm' : 'md'}
46
+ hoverable={!!onclick}
47
+ {onclick}
48
+ class={className}
49
+ chrome={false}
50
+ >
51
+ <div class="gap-3 flex items-start justify-between">
40
52
  <div class="min-w-0 space-y-1">
41
- <div class="flex flex-wrap items-center gap-2">
42
- <p class="text-sm font-medium text-secondary">{label}</p>
53
+ <div class="gap-2 flex flex-wrap items-center">
54
+ <p class={['font-medium text-secondary', compact ? 'text-xs' : 'text-sm']}>{label}</p>
43
55
  {#if badge}
44
56
  <Badge variant={badgeVariant} size="sm">{badge}</Badge>
45
57
  {/if}
46
58
  </div>
47
- <p class="text-2xl font-semibold tracking-tight text-primary">{value}</p>
59
+ <p
60
+ class={[
61
+ 'min-w-0 font-semibold tracking-tight text-primary',
62
+ compact ? 'text-lg leading-snug' : 'text-2xl'
63
+ ]}
64
+ >
65
+ {value}
66
+ </p>
48
67
  {#if delta || description}
49
- <div class="flex flex-wrap items-center gap-2 text-xs">
68
+ <div class="gap-2 text-xs flex flex-wrap items-center">
50
69
  {#if delta}
51
70
  <span
52
71
  class={[
53
- 'inline-flex items-center gap-0.5 font-medium',
72
+ 'gap-0.5 font-medium inline-flex items-center',
54
73
  trend === 'up' && 'text-green-600 dark:text-green-400',
55
74
  trend === 'down' && 'text-red-600 dark:text-red-400',
56
75
  trend === 'neutral' && 'text-muted'
57
76
  ]}
58
77
  >
59
78
  {#if trend === 'up'}
60
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
79
+ <svg
80
+ class="h-3.5 w-3.5"
81
+ viewBox="0 0 24 24"
82
+ fill="none"
83
+ stroke="currentColor"
84
+ stroke-width="2.5"
85
+ aria-hidden="true"
86
+ >
61
87
  <path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
62
88
  </svg>
63
89
  {:else if trend === 'down'}
64
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
90
+ <svg
91
+ class="h-3.5 w-3.5"
92
+ viewBox="0 0 24 24"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ stroke-width="2.5"
96
+ aria-hidden="true"
97
+ >
65
98
  <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
66
99
  </svg>
67
100
  {/if}
@@ -76,7 +109,10 @@
76
109
  </div>
77
110
  {#if icon}
78
111
  <div
79
- class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50 text-brand-600 dark:bg-brand-950/60 dark:text-brand-400"
112
+ class={[
113
+ 'rounded-xl bg-brand-50 text-brand-600 dark:bg-brand-950/60 dark:text-brand-400 flex shrink-0 items-center justify-center',
114
+ compact ? 'h-8 w-8' : 'h-10 w-10'
115
+ ]}
80
116
  >
81
117
  {@render icon()}
82
118
  </div>
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { CardVariant } from '../Card/Card.svelte';
3
3
  export type StatCardTrend = 'up' | 'down' | 'neutral';
4
+ export type StatCardSize = 'sm' | 'md';
4
5
  interface StatCardProps {
5
6
  label: string;
6
7
  value: string | number;
@@ -10,6 +11,7 @@ interface StatCardProps {
10
11
  badge?: string;
11
12
  badgeVariant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
12
13
  variant?: CardVariant;
14
+ size?: StatCardSize;
13
15
  class?: string;
14
16
  icon?: Snippet;
15
17
  onclick?: (e: MouseEvent) => void;
package/dist/index.d.ts CHANGED
@@ -360,7 +360,7 @@ export { default as OfflineBanner } from './components/molecules/OfflineBanner/O
360
360
  export { default as UnsavedChanges } from './components/molecules/UnsavedChanges/UnsavedChanges.svelte';
361
361
  export { default as ErrorBoundary } from './components/molecules/ErrorBoundary/ErrorBoundary.svelte';
362
362
  export { default as StatCard } from './components/molecules/StatCard/StatCard.svelte';
363
- export type { StatCardTrend } from './components/molecules/StatCard/StatCard.svelte';
363
+ export type { StatCardTrend, StatCardSize } from './components/molecules/StatCard/StatCard.svelte';
364
364
  export { default as MediaCard } from './components/molecules/MediaCard/MediaCard.svelte';
365
365
  export { default as FormActions } from './components/molecules/FormActions/FormActions.svelte';
366
366
  export { default as FormSection } from './components/molecules/FormSection/FormSection.svelte';
@@ -465,7 +465,7 @@ export type { CalendarEvent, CalendarEventColor } from './components/molecules/E
465
465
  export { default as CompareSlider } from './components/molecules/CompareSlider/CompareSlider.svelte';
466
466
  export { default as PdfViewer } from './components/molecules/PdfViewer/PdfViewer.svelte';
467
467
  export { default as ImageGallery } from './components/molecules/ImageGallery/ImageGallery.svelte';
468
- export type { GalleryImage } from './components/molecules/ImageGallery/ImageGallery.svelte';
468
+ export type { GalleryImage, GalleryLayout } from './components/molecules/ImageGallery/ImageGallery.svelte';
469
469
  export { default as LogViewer } from './components/molecules/LogViewer/LogViewer.svelte';
470
470
  export type { LogEntry, LogLevel } from './components/molecules/LogViewer/LogViewer.svelte';
471
471
  export { default as PriceTag } from './components/molecules/PriceTag/PriceTag.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",