@hkdigital/lib-sveltekit 0.0.96 → 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.
- package/dist/classes/svelte/image/ImageVariantsLoader.svelte.d.ts +9 -2
- package/dist/classes/svelte/image/ImageVariantsLoader.svelte.js +38 -31
- package/dist/components/image/ImageBox.svelte +104 -145
- package/dist/components/image/ImageBox.svelte.d.ts +6 -49
- package/dist/components/image/ResponsiveImage.svelte +2 -14
- package/dist/components/image/ResponsiveImage.svelte__ +90 -0
- package/dist/util/image/index.d.ts +16 -0
- package/dist/util/image/index.js +62 -0
- package/dist/util/sveltekit/index.d.ts +1 -0
- package/dist/util/sveltekit/index.js +1 -0
- package/dist/util/sveltekit/route-folders/index.d.ts +23 -0
- package/dist/util/sveltekit/route-folders/index.js +82 -0
- package/package.json +2 -2
- package/dist/components/inputs/text-input/TestTextInput.svelte__ +0 -102
- package/dist/components/inputs/text-input/TextInput.svelte___ +0 -83
- package/dist/css/tw-prose.postcss__ +0 -259
- package/dist/themes/hkdev/global/text.postcss__ +0 -35
- package/dist/themes/hkdev/global/vars.postcss__ +0 -7
@@ -8,9 +8,16 @@ export default class ImageVariantsLoader {
|
|
8
8
|
/**
|
9
9
|
* Set new optimal image variant or keep current
|
10
10
|
*
|
11
|
-
* @param {
|
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
|
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
|
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 {
|
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
|
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 (
|
73
|
-
|
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(
|
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
|
-
* @
|
10
|
-
*
|
11
|
-
*
|
12
|
-
*
|
13
|
-
*
|
14
|
-
*
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
31
|
+
aspect,
|
99
32
|
overflow = 'overflow-clip',
|
100
33
|
|
101
|
-
|
34
|
+
// Fitting and positioning of image in its container
|
102
35
|
fit = 'contain',
|
103
36
|
position = 'left top',
|
104
37
|
|
105
|
-
|
38
|
+
// Image data
|
106
39
|
imageMeta,
|
107
|
-
|
108
40
|
imageLoader,
|
109
41
|
|
42
|
+
// Accessibility
|
110
43
|
alt = '',
|
111
44
|
|
112
|
-
//
|
45
|
+
// Events
|
46
|
+
onProgress,
|
47
|
+
|
48
|
+
// Additional attributes
|
113
49
|
...attrs
|
114
50
|
} = $props();
|
115
51
|
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
125
|
-
|
126
|
-
/** @type {HTMLImageElement|undefined} */
|
127
|
-
let imgElem = $state();
|
57
|
+
let containerElem = $state();
|
128
58
|
|
129
|
-
let
|
130
|
-
|
131
|
-
|
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
|
-
|
68
|
+
/** @type {ImageLoader|undefined} */
|
69
|
+
let imageLoader_ = $state();
|
137
70
|
|
138
71
|
$effect(() => {
|
139
|
-
|
140
|
-
{
|
141
|
-
|
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
|
-
|
155
|
-
|
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
|
-
|
161
|
-
|
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={
|
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
|
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=
|
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
|
-
|
33
|
-
|
34
|
-
|
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;
|
package/dist/util/image/index.js
CHANGED
@@ -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
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./route-folders/index.js";
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './route-folders/index.js';
|
@@ -0,0 +1,23 @@
|
|
1
|
+
/**
|
2
|
+
* Validates if a path is within the project's src/routes directory
|
3
|
+
* @param {string} path - Path to validate
|
4
|
+
* @returns {boolean}
|
5
|
+
*/
|
6
|
+
export function isValidRoutePath(path: string): boolean;
|
7
|
+
/**
|
8
|
+
* Scans route folders recursively, only including folders with +page.svelte
|
9
|
+
* @param {Object} options - Scan options
|
10
|
+
* @param {string} options.dirPath - Directory path to scan
|
11
|
+
* @param {number} [options.maxDepth=1] - Maximum depth to scan
|
12
|
+
* @param {Set<string>} [options.skipFolders=new Set(['assets'])] - Folders to skip
|
13
|
+
* @returns {Promise<Array<{displayName: string, path: string}>>}
|
14
|
+
* @throws {Error} If path is outside project routes directory
|
15
|
+
*/
|
16
|
+
export function scanRouteFolders({ dirPath, maxDepth, skipFolders }: {
|
17
|
+
dirPath: string;
|
18
|
+
maxDepth?: number;
|
19
|
+
skipFolders?: Set<string>;
|
20
|
+
}): Promise<Array<{
|
21
|
+
displayName: string;
|
22
|
+
path: string;
|
23
|
+
}>>;
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
2
|
+
import { join, resolve } from 'node:path';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Validates if a path is within the project's src/routes directory
|
6
|
+
* @param {string} path - Path to validate
|
7
|
+
* @returns {boolean}
|
8
|
+
*/
|
9
|
+
export function isValidRoutePath(path) {
|
10
|
+
const normalizedPath = resolve(path);
|
11
|
+
const routesPath = resolve(process.cwd(), 'src/routes');
|
12
|
+
return normalizedPath.startsWith(routesPath);
|
13
|
+
}
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Scans route folders recursively, only including folders with +page.svelte
|
17
|
+
* @param {Object} options - Scan options
|
18
|
+
* @param {string} options.dirPath - Directory path to scan
|
19
|
+
* @param {number} [options.maxDepth=1] - Maximum depth to scan
|
20
|
+
* @param {Set<string>} [options.skipFolders=new Set(['assets'])] - Folders to skip
|
21
|
+
* @returns {Promise<Array<{displayName: string, path: string}>>}
|
22
|
+
* @throws {Error} If path is outside project routes directory
|
23
|
+
*/
|
24
|
+
export async function scanRouteFolders({
|
25
|
+
dirPath,
|
26
|
+
maxDepth = 1,
|
27
|
+
skipFolders = new Set(['assets'])
|
28
|
+
}) {
|
29
|
+
if (!isValidRoutePath(dirPath)) {
|
30
|
+
throw new Error('Invalid path: Must be within src/routes directory');
|
31
|
+
}
|
32
|
+
|
33
|
+
if (maxDepth < 1) return [];
|
34
|
+
|
35
|
+
try {
|
36
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
37
|
+
const results = [];
|
38
|
+
|
39
|
+
for (const entry of entries) {
|
40
|
+
if (
|
41
|
+
!entry.isDirectory() ||
|
42
|
+
skipFolders.has(entry.name) ||
|
43
|
+
entry.name.startsWith('.')
|
44
|
+
) {
|
45
|
+
continue;
|
46
|
+
}
|
47
|
+
|
48
|
+
const fullPath = join(dirPath, entry.name);
|
49
|
+
const currentPath = entry.name;
|
50
|
+
|
51
|
+
const dirContents = await readdir(fullPath);
|
52
|
+
const hasPageFile = dirContents.includes('+page.svelte');
|
53
|
+
|
54
|
+
if (hasPageFile) {
|
55
|
+
results.push({
|
56
|
+
displayName: entry.name,
|
57
|
+
path: currentPath
|
58
|
+
});
|
59
|
+
}
|
60
|
+
|
61
|
+
if (maxDepth > 1) {
|
62
|
+
const subFolders = await scanRouteFolders({
|
63
|
+
dirPath: fullPath,
|
64
|
+
maxDepth: maxDepth - 1,
|
65
|
+
skipFolders
|
66
|
+
});
|
67
|
+
|
68
|
+
for (const subFolder of subFolders) {
|
69
|
+
results.push({
|
70
|
+
displayName: `${entry.name}/${subFolder.displayName}`,
|
71
|
+
path: `${currentPath}/${subFolder.path}`
|
72
|
+
});
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
return results;
|
78
|
+
} catch (err) {
|
79
|
+
console.error(err);
|
80
|
+
throw new Error('Failed to scan directory');
|
81
|
+
}
|
82
|
+
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@hkdigital/lib-sveltekit",
|
3
|
-
"version": "0.0.
|
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
|
-
}
|