@hkdigital/lib-sveltekit 0.0.97 → 0.0.98

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.
@@ -8,9 +8,16 @@ export default class ImageVariantsLoader {
8
8
  /**
9
9
  * Set new optimal image variant or keep current
10
10
  *
11
- * @param {number} containerWidth
11
+ * @param {object} params
12
+ * @param {number} [params.containerWidth] Container width
13
+ * @param {number} [params.containerHeight] Container height
14
+ * @param {'cover'|'contain'|'fill'} [params.fit='contain'] Fit mode
12
15
  */
13
- updateOptimalImageMeta(containerWidth: number): void;
16
+ updateOptimalImageMeta({ containerWidth, containerHeight, fit }: {
17
+ containerWidth?: number;
18
+ containerHeight?: number;
19
+ fit?: "cover" | "contain" | "fill";
20
+ }): void;
14
21
  get loaded(): boolean;
15
22
  get variant(): import("./typedef.js").ImageMeta;
16
23
  /**
@@ -2,6 +2,8 @@
2
2
 
3
3
  // import * as expect from '../../../util/expect/index.js';
4
4
 
5
+ import { calculateEffectiveWidth } from '../../../util/image/index.js';
6
+
5
7
  import { untrack } from 'svelte';
6
8
 
7
9
  import ImageLoader from './ImageLoader.svelte.js';
@@ -21,57 +23,60 @@ export default class ImageVariantsLoader {
21
23
 
22
24
  #progress = $derived.by(() => {
23
25
  if (this.#imageLoader) {
24
- // const progress = this.#imageLoader.progress;
25
-
26
26
  return this.#imageLoader.progress;
27
27
  } else {
28
28
  return { bytesLoaded: 0, size: 0, loaded: false };
29
29
  }
30
30
  });
31
31
 
32
- #loaded = $derived(this.#progress.loaded);
32
+ #loaded = $derived.by(() => this.#progress?.loaded || false);
33
33
 
34
34
  /**
35
35
  * @param {ImageMeta[]} imagesMeta
36
36
  */
37
37
  constructor(imagesMeta, { devicePixelRatio = 1 } = {}) {
38
- // expect.notEmptyArray( imagesMeta );
39
-
40
38
  this.#devicePixelRatio = devicePixelRatio ?? 1;
41
-
42
- // Sort images meta by width ascending
43
39
  this.#imagesMeta = [...imagesMeta].sort((a, b) => a.width - b.width);
44
-
45
- $effect(() => {
46
- const variant = this.#imageVariant;
47
-
48
- if (variant) {
49
- // console.log('Load new variant', $state.snapshot(variant));
50
-
51
- // TODO: abort loading if imageLoader exists
52
-
53
- untrack(() => {
54
- const loader = (this.#imageLoader = new ImageLoader({
55
- url: variant.src
56
- }));
57
-
58
- loader.load();
59
- });
60
- }
61
- });
62
40
  }
63
41
 
64
42
  /**
65
43
  * Set new optimal image variant or keep current
66
44
  *
67
- * @param {number} containerWidth
45
+ * @param {object} params
46
+ * @param {number} [params.containerWidth] Container width
47
+ * @param {number} [params.containerHeight] Container height
48
+ * @param {'cover'|'contain'|'fill'} [params.fit='contain'] Fit mode
68
49
  */
69
- updateOptimalImageMeta(containerWidth) {
70
- const newVariant = this.getOptimalImageMeta(containerWidth);
50
+ updateOptimalImageMeta({ containerWidth, containerHeight, fit = 'contain' }) {
51
+ const baseImage = this.#imagesMeta[0];
52
+ const imageAspectRatio = baseImage.width / baseImage.height;
53
+
54
+ const effectiveWidth = calculateEffectiveWidth({
55
+ containerWidth,
56
+ containerHeight,
57
+ imageAspectRatio,
58
+ fit
59
+ });
60
+
61
+ const newVariant = this.getOptimalImageMeta(effectiveWidth);
71
62
 
72
- if (!newVariant || !this.#imageVariant || newVariant.width > this.#imageVariant.width) {
73
- // Only update imageVariant is width is larger
63
+ if (
64
+ !newVariant ||
65
+ !this.#imageVariant ||
66
+ newVariant.width > this.#imageVariant.width
67
+ ) {
74
68
  this.#imageVariant = newVariant;
69
+
70
+ // Create and start loader here directly when variant changes
71
+ if (this.#imageLoader?.initial) {
72
+ this.#imageLoader.unload();
73
+ }
74
+
75
+ this.#imageLoader = new ImageLoader({
76
+ imageMeta: newVariant
77
+ });
78
+
79
+ this.#imageLoader.load();
75
80
  }
76
81
  }
77
82
 
@@ -137,7 +142,9 @@ export default class ImageVariantsLoader {
137
142
 
138
143
  // Find the smallest image that's larger than our required width
139
144
 
140
- const optimal = imagesMeta.find((current) => current.width >= requiredWidth);
145
+ const optimal = imagesMeta.find(
146
+ (current) => current.width >= requiredWidth
147
+ );
141
148
 
142
149
  // Fall back to the largest image if nothing is big enough
143
150
  return optimal || imagesMeta[imagesMeta.length - 1];
@@ -1,93 +1,26 @@
1
1
  <script>
2
- import { onMount } from 'svelte';
3
-
4
2
  import { ImageLoader } from '../../classes/svelte/image/index.js';
5
-
3
+ import { ImageVariantsLoader } from '../../classes/svelte/image/index.js';
6
4
  import { toSingleImageMeta } from '../../util/image/index.js';
7
5
 
8
6
  /**
9
- * @example
10
- * import { ImageBox } from '/path/to/ImageBox/index.js';
11
- *
12
- * // @note 'as=metadata' is set by the preset
13
- * import NeonLightsOff from '../../img/NeonLightsOff.jpg?preset=gradient';
14
- *
15
- * <!-- Example that fits in an outer-box -->
16
- *
17
- * <div class="outer-box">
18
- * <ImageBox image={ArmyGreen} fit="contain" position="center center" />
19
- * </div>
20
- *
21
- * <!-- Examples that has have width, height or aspect set -->
22
- *
23
- * <ImageBox
24
- * image={ArmyGreen}
25
- * fit="contain"
26
- * position="center center"
27
- * width="w-[200px]"
28
- * height="h-[200px]"
29
- * classes="border-8 border-green-500"
30
- * />
31
- *
32
- * <ImageBox
33
- * image={ArmyGreen}
34
- * fit="contain"
35
- * position="center center"
36
- * width="w-[200px]"
37
- * aspect="aspect-square"
38
- * classes="border-8 border-green-500"
39
- * />
40
- *
41
- * <ImageBox
42
- * image={ArmyGreen}
43
- * fit="contain"
44
- * position="center center"
45
- * height="h-[200px]"
46
- * aspect="aspect-square"
47
- * classes="border-8 border-green-500"
48
- * />
49
- *
50
- * <!-- Or hack it using !important -->
51
- *
52
- * <ImageBox
53
- * image={ArmyGreen}
54
- * fit="contain"
55
- * position="center center"
56
- * classes="!w-[200px] !h-[200px] border-8 border-green-500"
57
- * />
7
+ * @type {{
8
+ * base?: string,
9
+ * bg?: string,
10
+ * classes?: string,
11
+ * width?: string,
12
+ * height?: string,
13
+ * aspect?: string,
14
+ * overflow?: string,
15
+ * fit?: 'contain' | 'cover' | 'fill',
16
+ * position?: string,
17
+ * imageMeta: import('../../config/typedef.js').ImageMeta | import('../../config/typedef.js').ImageMeta[],
18
+ * imageLoader?: import('../../classes/svelte/image/index.js').ImageLoader,
19
+ * alt?: string,
20
+ * onProgress?: (progress: import('../../classes/svelte/network-loader/typedef.js').LoadingProgress) => void,
21
+ * [attr: string]: any
22
+ * }}
58
23
  */
59
-
60
- /**
61
- * @typedef {import('./typedef.js').ObjectFit} ObjectFit
62
- * @typedef {import('./typedef.js').ObjectPosition} ObjectPosition
63
- *
64
- * @typedef {import('../../classes/svelte/network-loader/typedef.js').LoadingProgress} LoadingProgress
65
- *
66
- * @typedef {import('../../config/typedef.js').ImageMeta} ImageMeta
67
- *
68
- * @typedef {Object} Props
69
- * @property {string} [base] - Base styling class
70
- * @property {string} [bg] - Background styling class
71
- * @property {string} [classes] - Additional CSS classes
72
- * @property {string} [width] - Width of the image container
73
- * @property {string} [height] - Height of the image container
74
- * @property {string} [aspect] - Aspect ratio of the image container
75
- * @property {string} [overflow] - Overflow behavior
76
- * @property {ObjectFit} [fit] - Object-fit property
77
- * @property {ObjectPosition} [position] - Object-position property
78
- *
79
- * @property {ImageMeta|ImageMeta[]} [imageMeta]
80
- * Image metadata, TODO: array of image metadata for responsive image
81
- *
82
- * @property {ImageLoader} [imageLoader]
83
- * Image loader
84
- *
85
- * @property {string} [alt] - Alternative text for the image
86
- * @property {() => LoadingProgress} [onProgress] - Progress callback function
87
- * @property {*} [attr] - Additional arbitrary attributes
88
- */
89
-
90
- /** @type {Props} */
91
24
  let {
92
25
  // Style
93
26
  base,
@@ -95,111 +28,101 @@
95
28
  classes,
96
29
  width,
97
30
  height,
98
- aspect,
31
+ aspect,
99
32
  overflow = 'overflow-clip',
100
33
 
101
- // Fitting and positioning of image in its container
34
+ // Fitting and positioning of image in its container
102
35
  fit = 'contain',
103
36
  position = 'left top',
104
37
 
105
- // Image meta data
38
+ // Image data
106
39
  imageMeta,
107
-
108
40
  imageLoader,
109
41
 
42
+ // Accessibility
110
43
  alt = '',
111
44
 
112
- // Attributes
45
+ // Events
46
+ onProgress,
47
+
48
+ // Additional attributes
113
49
  ...attrs
114
50
  } = $props();
115
51
 
116
- if( !imageMeta )
117
- {
118
- throw new Error('Missing [imageMeta]');
119
- }
120
-
121
- // let show = $state(false);
52
+ if (!imageMeta) {
53
+ throw new Error('Missing [imageMeta]');
54
+ }
122
55
 
123
56
  /** @type {HTMLDivElement|undefined} */
124
- let imgBoxElem = $state();
125
-
126
- /** @type {HTMLImageElement|undefined} */
127
- let imgElem = $state();
57
+ let containerElem = $state();
128
58
 
129
- let aspectStyle = $state('');
130
-
131
- // > Loading
59
+ let imageMeta_ = $state();
60
+ let variantsLoader = $state();
61
+ let variantObjectUrl = $state(null);
62
+ let objectUrl = $state(null);
132
63
 
64
+ // For single image meta
133
65
  let metaWidth = $state(0);
134
66
  let metaHeight = $state(0);
135
67
 
136
- let imageMeta_ = $state();
68
+ /** @type {ImageLoader|undefined} */
69
+ let imageLoader_ = $state();
137
70
 
138
71
  $effect(() => {
139
- if( imageMeta )
140
- {
141
- imageMeta_ = toSingleImageMeta( imageMeta );
72
+ // Setup variants loader for responsive images
73
+ if (Array.isArray(imageMeta) && !imageLoader && !variantsLoader) {
74
+ variantsLoader = new ImageVariantsLoader(imageMeta, {
75
+ devicePixelRatio: window.devicePixelRatio
76
+ });
77
+ }
78
+ // Handle single image meta
79
+ else if (imageMeta && !variantsLoader) {
80
+ imageMeta_ = toSingleImageMeta(imageMeta);
142
81
  }
143
82
  });
144
83
 
84
+ // Handle progress reporting
145
85
  $effect(() => {
146
- //
147
- // Set meta width and height
148
- //
149
- if (imageMeta_) {
150
- if (imageMeta_.width) {
151
- metaWidth = imageMeta_.width;
152
- }
86
+ if (!onProgress) return;
153
87
 
154
- if (imageMeta_.height) {
155
- metaHeight = imageMeta_.height;
156
- }
88
+ // Report progress from variants loader
89
+ if (variantsLoader) {
90
+ onProgress(variantsLoader.progress);
91
+ }
92
+ // Report progress from single image loader
93
+ else if (imageLoader_) {
94
+ onProgress(imageLoader_.progress);
157
95
  }
158
96
  });
159
97
 
160
- /** @type {ImageLoader|undefined} */
161
- let imageLoader_ = $state();
98
+ $effect(() => {
99
+ if (imageMeta_) {
100
+ metaWidth = imageMeta_.width ?? 0;
101
+ metaHeight = imageMeta_.height ?? 0;
102
+ }
103
+ });
162
104
 
163
- $effect( () => {
164
- //
165
- // User supplied imageLoader instead of imageMeta
166
- //
167
- if( !imageMeta && imageLoader && !imageLoader_ )
168
- {
105
+ $effect(() => {
106
+ if (!imageMeta && imageLoader && !imageLoader_) {
169
107
  imageLoader_ = imageLoader;
170
108
  imageMeta_ = imageLoader.imageMeta;
171
109
  }
172
- } );
173
-
174
- /** @type {string|null} */
175
- let objectUrl = $state(null);
110
+ });
176
111
 
177
112
  $effect(() => {
178
- //
179
- // Create image loader
180
- //
181
113
  if (imageMeta_ && !imageLoader_) {
182
114
  imageLoader_ = new ImageLoader({ imageMeta: imageMeta_ });
183
115
  }
184
116
  });
185
117
 
186
118
  $effect(() => {
187
- //
188
- // Start loading if imageLoader_ is in state 'initial'
189
- //
190
- // TODO: implement lazy flag
191
- //
192
119
  if (imageLoader_?.initial) {
193
120
  imageLoader_.load();
194
121
  }
195
122
  });
196
123
 
197
124
  $effect(() => {
198
- //
199
- // Get objectUrl when the image has finished loading
200
- //
201
- if (imageLoader_.loaded) {
202
- // @ts-ignore
125
+ if (imageLoader_?.loaded) {
203
126
  objectUrl = imageLoader_.getObjectURL();
204
127
  }
205
128
 
@@ -211,26 +134,62 @@
211
134
  };
212
135
  });
213
136
 
137
+ $effect(() => {
138
+ if (!containerElem || !variantsLoader) return;
139
+
140
+ const resizeObserver = new ResizeObserver((entries) => {
141
+ for (const entry of entries) {
142
+ const { width, height } = entry.contentRect;
143
+ variantsLoader.updateOptimalImageMeta({
144
+ containerWidth: width,
145
+ containerHeight: height,
146
+ fit
147
+ });
148
+ }
149
+ });
150
+
151
+ resizeObserver.observe(containerElem);
152
+ return () => resizeObserver.disconnect();
153
+ });
154
+
155
+ $effect(() => {
156
+ if (variantsLoader?.loaded) {
157
+ variantObjectUrl = variantsLoader.getObjectURL();
158
+ }
159
+
160
+ return () => {
161
+ if (variantObjectUrl) {
162
+ URL.revokeObjectURL(variantObjectUrl);
163
+ variantObjectUrl = null;
164
+ }
165
+ };
166
+ });
214
167
  </script>
215
168
 
216
169
  <div
217
170
  data-image="box"
218
- bind:this={imgBoxElem}
171
+ bind:this={containerElem}
219
172
  class="{base} {bg} {aspect} {overflow} {width} {height} {classes}"
220
173
  style:--fit={fit}
221
174
  style:--pos={position}
222
- style:aspect-ratio={aspectStyle}
223
175
  style:width={width || (height && aspect) ? undefined : '100%'}
224
176
  style:height={height || (width && aspect) ? undefined : '100%'}
225
177
  {...attrs}
226
178
  >
227
- {#if metaWidth && metaHeight}
179
+ {#if variantsLoader?.loaded && variantObjectUrl}
180
+ <img
181
+ src={variantObjectUrl}
182
+ {alt}
183
+ width={variantsLoader.variant.width}
184
+ height={variantsLoader.variant.height}
185
+ />
186
+ {:else if objectUrl && metaWidth && metaHeight}
228
187
  <img src={objectUrl} {alt} width={metaWidth} height={metaHeight} />
229
188
  {/if}
230
189
  </div>
231
190
 
232
191
  <style>
233
- [data-image="box"] {
192
+ [data-image='box'] {
234
193
  max-width: 100%;
235
194
  max-height: 100%;
236
195
  }
@@ -1,60 +1,17 @@
1
1
  export default ImageBox;
2
2
  declare const ImageBox: import("svelte").Component<{
3
- /**
4
- * - Base styling class
5
- */
3
+ [attr: string]: any;
6
4
  base?: string;
7
- /**
8
- * - Background styling class
9
- */
10
5
  bg?: string;
11
- /**
12
- * - Additional CSS classes
13
- */
14
6
  classes?: string;
15
- /**
16
- * - Width of the image container
17
- */
18
7
  width?: string;
19
- /**
20
- * - Height of the image container
21
- */
22
8
  height?: string;
23
- /**
24
- * - Aspect ratio of the image container
25
- */
26
9
  aspect?: string;
27
- /**
28
- * - Overflow behavior
29
- */
30
10
  overflow?: string;
31
- /**
32
- * - Object-fit property
33
- */
34
- fit?: import("./typedef.js").ObjectFit;
35
- /**
36
- * - Object-position property
37
- */
38
- position?: import("./typedef.js").ObjectPosition;
39
- /**
40
- * Image metadata, TODO: array of image metadata for responsive image
41
- */
42
- imageMeta?: import("../../config/typedef.js").ImageMeta | import("../../config/typedef.js").ImageMeta[];
43
- /**
44
- * Image loader
45
- */
46
- imageLoader?: ImageLoader;
47
- /**
48
- * - Alternative text for the image
49
- */
11
+ fit?: "contain" | "cover" | "fill";
12
+ position?: string;
13
+ imageMeta: import("../../config/typedef.js").ImageMeta | import("../../config/typedef.js").ImageMeta[];
14
+ imageLoader?: import("../../classes/svelte/image/index.js").ImageLoader;
50
15
  alt?: string;
51
- /**
52
- * - Progress callback function
53
- */
54
- onProgress?: () => import("../../classes/svelte/network-loader/typedef.js").LoadingProgress;
55
- /**
56
- * - Additional arbitrary attributes
57
- */
58
- attr?: any;
16
+ onProgress?: (progress: import("../../classes/svelte/network-loader/typedef.js").LoadingProgress) => void;
59
17
  }, {}, "">;
60
- import { ImageLoader } from '../../classes/svelte/image/index.js';
@@ -37,7 +37,7 @@
37
37
  let imageVariant = $state(null);
38
38
 
39
39
  $effect(() => {
40
- variantsLoader.updateOptimalImageMeta(containerWidth);
40
+ variantsLoader.updateOptimalImageMeta({ containerWidth });
41
41
  });
42
42
 
43
43
  // $effect(() => {
@@ -53,22 +53,9 @@
53
53
  if (variantsLoader.loaded) {
54
54
  // @ts-ignore
55
55
  imageUrl = variantsLoader.getObjectURL();
56
-
57
- // image = new Image();
58
- // image.src = url;
59
-
60
- // image.onload = () => {
61
- // console.log('loaded');
62
- // imageUrl = url;
63
- // };
64
56
  }
65
57
 
66
58
  return () => {
67
- // if (image) {
68
- // image.onload = null;
69
- // image = undefined;
70
- // }
71
-
72
59
  if (imageUrl) {
73
60
  URL.revokeObjectURL(imageUrl);
74
61
  imageUrl = null;
@@ -81,6 +68,7 @@
81
68
  // let image = $derived(variantsLoader.image);
82
69
  </script>
83
70
 
71
+
84
72
  <div
85
73
  bind:clientWidth={containerWidth}
86
74
  class="{boxBase} {boxClasses}"
@@ -0,0 +1,90 @@
1
+ <script>
2
+ /** @typedef {import('$lib/config/typedef.js').ImageMeta} ImageMeta */
3
+
4
+ import { ImageVariantsLoader } from '$lib/classes/svelte/image/index.js';
5
+
6
+ /**
7
+ * @type {{
8
+ * base?: string,
9
+ * classes?: string
10
+ * boxBase?: string,
11
+ * boxClasses?: string
12
+ * boxAttrs?: { [attr: string]: * },
13
+ * images: ImageMeta[],
14
+ * alt?: string
15
+ * } & { [attr: string]: * }}
16
+ */
17
+ const {
18
+ base,
19
+ classes,
20
+ boxBase,
21
+ boxClasses,
22
+ boxAttrs,
23
+
24
+ // Functional
25
+ images,
26
+ alt = '',
27
+
28
+ // Attributes
29
+ ...attrs
30
+ } = $props();
31
+
32
+ let variantsLoader = new ImageVariantsLoader(images);
33
+
34
+ let containerWidth = $state(0);
35
+
36
+ /** @type {ImageMeta|null} */
37
+ let imageVariant = $state(null);
38
+
39
+ $effect(() => {
40
+ variantsLoader.updateOptimalImageMeta({ containerWidth });
41
+ });
42
+
43
+ // $effect(() => {
44
+ // console.log('imageVariant', $state.snapshot(imageVariant));
45
+ // });
46
+
47
+ /** @type {string|null} */
48
+ let imageUrl = $state(null);
49
+
50
+ $effect(() => {
51
+ let image;
52
+
53
+ if (variantsLoader.loaded) {
54
+ // @ts-ignore
55
+ imageUrl = variantsLoader.getObjectURL();
56
+ }
57
+
58
+ return () => {
59
+ if (imageUrl) {
60
+ URL.revokeObjectURL(imageUrl);
61
+ imageUrl = null;
62
+ }
63
+ };
64
+ });
65
+
66
+ let variant = $derived(variantsLoader.variant);
67
+
68
+ // let image = $derived(variantsLoader.image);
69
+ </script>
70
+
71
+
72
+ <div
73
+ bind:clientWidth={containerWidth}
74
+ class="{boxBase} {boxClasses}"
75
+ {...boxAttrs}
76
+ >
77
+ <!-- <p class="p text-white">variant: {JSON.stringify(variant)}</p> -->
78
+
79
+ {#if variant}
80
+ <img
81
+ data-image="responsive"
82
+ src={imageUrl ? imageUrl : ''}
83
+ width={variant.width}
84
+ height={variant.height}
85
+ {alt}
86
+ class="{boxBase} {boxClasses}"
87
+ {...attrs}
88
+ />
89
+ {/if}
90
+ </div>
@@ -7,3 +7,19 @@
7
7
  * @param {ImageMeta|ImageMeta[]} imageMeta
8
8
  */
9
9
  export function toSingleImageMeta(imageMeta: ImageMeta | ImageMeta[]): import("../../config/typedef").ImageMeta;
10
+ /**
11
+ * Calculate effective width based on container dimensions and fit mode
12
+ *
13
+ * @param {object} params
14
+ * @param {number} [params.containerWidth] Container width in pixels
15
+ * @param {number} [params.containerHeight] Container height in pixels
16
+ * @param {number} params.imageAspectRatio Original image aspect ratio (width/height)
17
+ * @param {'cover'|'contain'|'fill'} [params.fit='contain'] Fit mode
18
+ * @returns {number} Effective width needed
19
+ */
20
+ export function calculateEffectiveWidth({ containerWidth, containerHeight, imageAspectRatio, fit }: {
21
+ containerWidth?: number;
22
+ containerHeight?: number;
23
+ imageAspectRatio: number;
24
+ fit?: "cover" | "contain" | "fill";
25
+ }): number;
@@ -22,3 +22,65 @@ export function toSingleImageMeta(imageMeta) {
22
22
 
23
23
  throw new Error('Invalid value for parameter [imageMeta]');
24
24
  }
25
+
26
+ /**
27
+ * Calculate effective width based on container dimensions and fit mode
28
+ *
29
+ * @param {object} params
30
+ * @param {number} [params.containerWidth] Container width in pixels
31
+ * @param {number} [params.containerHeight] Container height in pixels
32
+ * @param {number} params.imageAspectRatio Original image aspect ratio (width/height)
33
+ * @param {'cover'|'contain'|'fill'} [params.fit='contain'] Fit mode
34
+ * @returns {number} Effective width needed
35
+ */
36
+ export function calculateEffectiveWidth({
37
+ containerWidth,
38
+ containerHeight,
39
+ imageAspectRatio,
40
+ fit = 'contain'
41
+ }) {
42
+ if (containerWidth && !containerHeight) {
43
+ // If only width is provided, use it
44
+
45
+ return containerWidth;
46
+ }
47
+
48
+ if (!containerWidth && containerHeight) {
49
+ // If only height is provided, calculate width based on aspect ratio
50
+
51
+ return containerHeight * imageAspectRatio;
52
+ }
53
+
54
+ if (containerWidth && containerHeight) {
55
+ // If both dimensions are provided, calculate based on fit mode
56
+
57
+ const containerAspectRatio = containerWidth / containerHeight;
58
+
59
+ switch (fit) {
60
+ case 'fill':
61
+ return containerWidth;
62
+
63
+ case 'contain':
64
+ if (containerAspectRatio > imageAspectRatio) {
65
+ // Height constrained, scale width accordingly
66
+
67
+ return containerHeight * imageAspectRatio;
68
+ }
69
+ return containerWidth;
70
+
71
+ case 'cover':
72
+ if (containerAspectRatio < imageAspectRatio) {
73
+ // Height constrained, scale width accordingly
74
+
75
+ return containerHeight * imageAspectRatio;
76
+ }
77
+ return containerWidth;
78
+
79
+ default:
80
+ return containerWidth;
81
+ }
82
+ }
83
+
84
+ // Fallback if neither dimension is provided
85
+ throw new Error('Either containerWidth or containerHeight must be provided');
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-sveltekit",
3
- "version": "0.0.97",
3
+ "version": "0.0.98",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"
@@ -70,6 +70,7 @@
70
70
  "@sveltejs/adapter-auto": "^3.3.1",
71
71
  "@sveltejs/package": "^2.3.7",
72
72
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
73
+ "@tailwindcss/typography": "^0.5.16",
73
74
  "@types/eslint": "^9.6.1",
74
75
  "autoprefixer": "^10.4.20",
75
76
  "cross-env": "^7.0.3",
@@ -94,7 +95,6 @@
94
95
  "vitest": "^2.1.8"
95
96
  },
96
97
  "dependencies": {
97
- "@tailwindcss/typography": "^0.5.16",
98
98
  "zod": "^3.24.1"
99
99
  }
100
100
  }
@@ -1,102 +0,0 @@
1
- <script>
2
- /**
3
- * @type {{
4
- * base?: string,
5
- * error?: string,
6
- * classes?: string,
7
- * fieldClasses?: string,
8
- * fieldError?: string,
9
- * legendBase?: string,
10
- * legendClasses?: string,
11
- * legendError?: string,
12
- * value?: string,
13
- * type?: 'text' | 'url' | 'email' | 'number',
14
- * pattern?: string,
15
- * required?: boolean,
16
- * title?: string,
17
- * valid?: boolean,
18
- * pristine?: boolean,
19
- * validate?: (value: string) => string | undefined,
20
- * } & { [attr: string]: * }}
21
- */
22
- let {
23
- base = '',
24
- error = '',
25
- classes = '',
26
-
27
- fieldClasses,
28
- fieldError,
29
-
30
- legendBase = 'ml-16p px-8p',
31
- legendClasses,
32
- legendError,
33
-
34
- value = $bindable(''),
35
- type = 'text',
36
- pattern,
37
- required = false,
38
-
39
- title = '',
40
-
41
- valid = $bindable(true),
42
- pristine = $bindable(true),
43
-
44
- validate,
45
-
46
- ...attrs
47
- } = $props();
48
-
49
- let inputRef = $state();
50
- let validationMessage = $state('');
51
- let initialValue = $state('');
52
-
53
- $effect(() => {
54
- if (!inputRef) return;
55
- initialValue = value;
56
- validateInput(inputRef, value);
57
- });
58
-
59
- function validateInput(input, currentValue) {
60
- input.setCustomValidity('');
61
- const isBuiltInValid = input.checkValidity();
62
-
63
- if (isBuiltInValid && validate) {
64
- const customError = validate(currentValue);
65
- input.setCustomValidity(customError || '');
66
- }
67
-
68
- pristine = currentValue === initialValue;
69
- valid = input.validity.valid;
70
- validationMessage = input.validationMessage;
71
- }
72
-
73
- function handleInput(event) {
74
- validateInput(event.target, event.target.value);
75
- }
76
- </script>
77
-
78
- <fieldset
79
- data-inputs="text-input"
80
- class="flex w-full items-center rounded {fieldClasses}"
81
- >
82
- <legend class="{legendBase} {legendClasses}">{title}</legend>
83
-
84
- <input
85
- bind:this={inputRef}
86
- {type}
87
- {pattern}
88
- {required}
89
- {value}
90
- class="w-full border-none bg-transparent {base} {classes}"
91
- aria-invalid={!valid}
92
- aria-errormessage={!valid ? 'validation-message' : undefined}
93
- oninput={handleInput}
94
- {...attrs}
95
- />
96
-
97
- {#if !valid}
98
- <small id="validation-message" class="text-error" role="alert">
99
- {validationMessage}
100
- </small>
101
- {/if}
102
- </fieldset>
@@ -1,83 +0,0 @@
1
- <script>
2
- /**
3
- * @type {{
4
- * classes?: string,
5
- * fieldClasses?: string,
6
- * legendClasses?: string,
7
- * legendTitle?: string,
8
- * error?: boolean,
9
- * type?: string,
10
- * placeholder: string,
11
- * required: boolean,
12
- * snippetWarning?: import('svelte').Snippet,
13
- * } & { [attr: string]: * }}
14
- */
15
- let {
16
- // Style
17
- classes,
18
- fieldClasses,
19
- legendClasses,
20
-
21
- // Functionality
22
- name,
23
- disabled,
24
- required,
25
-
26
- // initialValue
27
- // value
28
- // readonly
29
- // pattern
30
- // minlength
31
- // maxlength
32
-
33
- // Text placeholders
34
- legendTitle,
35
- placeholder,
36
-
37
- type,
38
- snippetWarning,
39
-
40
- // Attributes
41
- ...attrs
42
- } = $props();
43
- </script>
44
-
45
- {#snippet defaultWarning()}
46
- <svg
47
- width="17"
48
- height="16"
49
- viewBox="0 0 17 16"
50
- fill="none"
51
- xmlns="http://www.w3.org/2000/svg"
52
- >
53
- <path
54
- fill-rule="evenodd"
55
- clip-rule="evenodd"
56
- d="M6.36747 1.28014C7.3152 -0.426712 9.68492 -0.426712 10.6318 1.28014L16.6669 12.1596C17.6138 13.8664 16.429 16 14.5343 16H2.46497C0.570331 16 -0.613713 13.8664 0.333194 12.1596L6.36665 1.28014H6.36747ZM8.50006 5.75805C8.66328 5.75805 8.81981 5.82549 8.93522 5.94553C9.05063 6.06556 9.11547 6.22837 9.11547 6.39812V9.59846C9.11547 9.76822 9.05063 9.93102 8.93522 10.0511C8.81981 10.1711 8.66328 10.2385 8.50006 10.2385C8.33684 10.2385 8.18031 10.1711 8.0649 10.0511C7.94949 9.93102 7.88465 9.76822 7.88465 9.59846V6.39812C7.88465 6.22837 7.94949 6.06556 8.0649 5.94553C8.18031 5.82549 8.33684 5.75805 8.50006 5.75805ZM8.50006 12.7988C8.66328 12.7988 8.81981 12.7314 8.93522 12.6113C9.05063 12.4913 9.11547 12.3285 9.11547 12.1587C9.11547 11.989 9.05063 11.8262 8.93522 11.7061C8.81981 11.5861 8.66328 11.5187 8.50006 11.5187C8.33684 11.5187 8.18031 11.5861 8.0649 11.7061C7.94949 11.8262 7.88465 11.989 7.88465 12.1587C7.88465 12.3285 7.94949 12.4913 8.0649 12.6113C8.18031 12.7314 8.33684 12.7988 8.50006 12.7988Z"
57
- fill="#F8705E"
58
- />
59
- </svg>
60
- {/snippet}
61
-
62
- <fieldset
63
- data-input="text-input"
64
- class="flex w-full items-center rounded {fieldClasses}"
65
- >
66
- <legend class="px-2 {legendClasses}" class:error>{legendTitle}</legend>
67
- <input
68
- class="w-full border-none bg-transparent {classes}"
69
- {type}
70
- {placeholder}
71
- {name}
72
- {required}
73
- {...attrs}
74
- />
75
- {#if error}
76
- {#if snippetWarning}
77
- {@render snippetWarning()}
78
- {:else}
79
- {@render defaultWarning()}
80
- {/if}
81
- <!-- <img src={warningSymbol} class="mb-2 mr-8" alt="Warning" /> -->
82
- {/if}
83
- </fieldset>
@@ -1,259 +0,0 @@
1
- /* Base prose styles */
2
- .prose {
3
- font-size: 1rem;
4
- line-height: 1.75;
5
- max-width: 65ch;
6
- }
7
-
8
- .prose > * + * {
9
- margin-top: 1.25em;
10
- }
11
-
12
- /* Headings */
13
- .prose h1 {
14
- font-size: 2.25em;
15
- line-height: 1.1111111;
16
- margin-top: 0;
17
- margin-bottom: 0.8888889em;
18
- font-weight: 800;
19
- }
20
-
21
- .prose h2 {
22
- font-size: 1.5em;
23
- line-height: 1.3333333;
24
- margin-top: 2em;
25
- margin-bottom: 1em;
26
- font-weight: 700;
27
- }
28
-
29
- .prose h3 {
30
- font-size: 1.25em;
31
- line-height: 1.6;
32
- margin-top: 1.6em;
33
- margin-bottom: 0.6em;
34
- font-weight: 600;
35
- }
36
-
37
- .prose h4 {
38
- font-size: 1.125em;
39
- line-height: 1.5;
40
- margin-top: 1.5em;
41
- margin-bottom: 0.5em;
42
- font-weight: 600;
43
- }
44
-
45
- /* Paragraphs */
46
- .prose p {
47
- margin-top: 1.25em;
48
- margin-bottom: 1.25em;
49
- }
50
-
51
- /* Lists */
52
- .prose ul,
53
- .prose ol {
54
- padding-left: 1.625em;
55
- margin-top: 1.25em;
56
- margin-bottom: 1.25em;
57
- }
58
-
59
- .prose li {
60
- margin-top: 0.5em;
61
- margin-bottom: 0.5em;
62
- }
63
-
64
- .prose > ul > li p {
65
- margin-top: 0.75em;
66
- margin-bottom: 0.75em;
67
- }
68
-
69
- .prose > ul > li > *:first-child {
70
- margin-top: 1.25em;
71
- }
72
-
73
- .prose > ul > li > *:last-child {
74
- margin-bottom: 1.25em;
75
- }
76
-
77
- /* Nested lists */
78
- .prose ul ul,
79
- .prose ul ol,
80
- .prose ol ul,
81
- .prose ol ol {
82
- margin-top: 0.75em;
83
- margin-bottom: 0.75em;
84
- }
85
-
86
- /* Links */
87
- .prose a {
88
- color: #111827;
89
- text-decoration: underline;
90
- font-weight: 500;
91
- }
92
-
93
- .prose a:hover {
94
- text-decoration-thickness: 2px;
95
- }
96
-
97
- /* Code blocks */
98
- .prose code {
99
- color: #111827;
100
- font-weight: 600;
101
- font-size: 0.875em;
102
- }
103
-
104
- .prose pre {
105
- color: #e5e7eb;
106
- background-color: #1f2937;
107
- overflow-x: auto;
108
- font-size: 0.875em;
109
- line-height: 1.7142857;
110
- margin-top: 1.7142857em;
111
- margin-bottom: 1.7142857em;
112
- border-radius: 0.375rem;
113
- padding: 0.8571429em 1.1428571em;
114
- }
115
-
116
- .prose pre code {
117
- background-color: transparent;
118
- border-radius: 0;
119
- padding: 0;
120
- font-weight: 400;
121
- color: inherit;
122
- font-size: inherit;
123
- font-family: inherit;
124
- line-height: inherit;
125
- }
126
-
127
- /* Blockquotes */
128
- .prose blockquote {
129
- font-weight: 500;
130
- font-style: italic;
131
- color: #111827;
132
- border-left-width: 0.25rem;
133
- border-left-color: #e5e7eb;
134
- margin-top: 1.6em;
135
- margin-bottom: 1.6em;
136
- padding-left: 1em;
137
- }
138
-
139
- /* Tables */
140
- .prose table {
141
- width: 100%;
142
- table-layout: auto;
143
- text-align: left;
144
- margin-top: 2em;
145
- margin-bottom: 2em;
146
- font-size: 0.875em;
147
- line-height: 1.7142857;
148
- }
149
-
150
- .prose thead {
151
- font-weight: 600;
152
- border-bottom-width: 1px;
153
- border-bottom-color: #d1d5db;
154
- }
155
-
156
- .prose thead th {
157
- vertical-align: bottom;
158
- padding-right: 0.5714286em;
159
- padding-bottom: 0.5714286em;
160
- padding-left: 0.5714286em;
161
- }
162
-
163
- .prose tbody tr {
164
- border-bottom-width: 1px;
165
- border-bottom-color: #e5e7eb;
166
- }
167
-
168
- .prose tbody td {
169
- vertical-align: top;
170
- padding: 0.5714286em;
171
- }
172
-
173
- /* Size variations */
174
- .prose-sm {
175
- font-size: 0.875rem;
176
- line-height: 1.7142857;
177
- }
178
-
179
- .prose-lg {
180
- font-size: 1.125rem;
181
- line-height: 1.7777778;
182
- }
183
-
184
- .prose-xl {
185
- font-size: 1.25rem;
186
- line-height: 1.8;
187
- }
188
-
189
- /* Dark mode */
190
- .prose-invert {
191
- color: #d1d5db;
192
- }
193
-
194
- .prose-invert a {
195
- color: #fff;
196
- }
197
-
198
- .prose-invert strong {
199
- color: #fff;
200
- }
201
-
202
- .prose-invert code {
203
- color: #fff;
204
- }
205
-
206
- .prose-invert thead {
207
- border-bottom-color: #4b5563;
208
- }
209
-
210
- .prose-invert tbody tr {
211
- border-bottom-color: #374151;
212
- }
213
-
214
- .prose-invert blockquote {
215
- color: #9ca3af;
216
- border-left-color: #4b5563;
217
- }
218
-
219
- /* Images */
220
- .prose img {
221
- margin-top: 2em;
222
- margin-bottom: 2em;
223
- }
224
-
225
- .prose figure > * {
226
- margin-top: 0;
227
- margin-bottom: 0;
228
- }
229
-
230
- .prose figure figcaption {
231
- color: #6b7280;
232
- font-size: 0.875em;
233
- line-height: 1.4285714;
234
- margin-top: 0.8571429em;
235
- }
236
-
237
- /* Custom elements */
238
- .prose hr {
239
- border-color: #e5e7eb;
240
- border-top-width: 1px;
241
- margin-top: 3em;
242
- margin-bottom: 3em;
243
- }
244
-
245
- .prose strong {
246
- font-weight: 600;
247
- color: #111827;
248
- }
249
-
250
- .prose em {
251
- font-style: italic;
252
- }
253
-
254
- /* Focus styles */
255
- .prose a:focus {
256
- outline: 2px solid transparent;
257
- outline-offset: 2px;
258
- text-decoration-thickness: 2px;
259
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * Tailwind defaults
3
- *
4
- * text-sm 14px;
5
- * text-base 16px;
6
- * text-lg 18px;
7
- * text-xl 20px;
8
- * text-2xl 24px;
9
- * text-3xl 30px;
10
- * text-4xl 36px;
11
- * text-5xl 48px;
12
- * text-6xl 60px;
13
- * text-7xl 72px;
14
- * text-8xl 96px;
15
- * text-9xl 128px;
16
- *
17
- * @see https://tailwindcss.com/docs/font-size
18
- */
19
-
20
- @define-mixin all_text {
21
-
22
- a {
23
- @apply text-sm font-bold underline;
24
- }
25
-
26
- p {
27
- @apply text-xl;
28
- }
29
-
30
- h1 {
31
- &.text-presenter-title {
32
- @apply text-5xl;
33
- }
34
- }
35
- }
@@ -1,7 +0,0 @@
1
-
2
- @define-mixin all_vars {
3
-
4
- /* --tab-bar-height: 72px;
5
- --tab-bar-selector-width: 132px;
6
- --tab-bar-selector-height: var(--tab-bar-height, 72px); */
7
- }