@yatoday/astro-ui 0.10.2 → 0.11.0
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/components/Breadcrumbs/Breadcrumbs.astro +1 -1
- package/components/Card0/Card0.astro +34 -11
- package/components/Card0/types.ts +3 -0
- package/components/Card1/Card1.astro +33 -4
- package/components/Card2/Card2.astro +31 -3
- package/components/Card3/Card3.astro +33 -4
- package/components/Card6/Card6.astro +29 -2
- package/components/Card7/Card7.astro +4 -2
- package/components/WidgetContent/WidgetContent.astro +0 -1
- package/components/WidgetSwiperPhotoSlider/WidgetSwiperPhotoSlider.astro +3 -4
- package/index.d.ts +20 -0
- package/package.json +1 -1
- package/styles/styles.css +24 -3
- package/vendor-config/.env.example +53 -0
- package/vendor-config/README.md +301 -0
- package/vendor-config/config.example.yaml +105 -0
- package/vendor-config/utils/__tests__/loadConfig.test.ts +209 -0
- package/vendor-config/utils/loadConfig.ts +43 -1
|
@@ -3,7 +3,7 @@ import type { BreadcrumbsProps as Props } from './types';
|
|
|
3
3
|
|
|
4
4
|
import { Icon } from 'astro-icon/components';
|
|
5
5
|
|
|
6
|
-
const { class: classNames = '
|
|
6
|
+
const { class: classNames = '', ariaLabel = 'Breadcrumbs', items = [] } = Astro.props;
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
<nav class:list={['breadcrumbs', classNames]} aria-label={ariaLabel}>
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type {Card0Props as Props} from './types';
|
|
3
|
+
import {twMerge} from 'tailwind-merge';
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
badge,
|
|
7
|
+
badgeTopRight,
|
|
8
|
+
badgeBottomRight,
|
|
9
|
+
badgeBottomLeft,
|
|
10
|
+
as = 'article',
|
|
11
|
+
classes = {}
|
|
12
|
+
} = Astro.props;
|
|
6
13
|
|
|
7
14
|
const WrapperTag = as;
|
|
8
15
|
|
|
9
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
container: containerClass = '',
|
|
18
|
+
badge: badgeClass = '',
|
|
19
|
+
badgeTopRight: badgeTopRightClass = '',
|
|
20
|
+
} = classes;
|
|
10
21
|
---
|
|
11
22
|
|
|
12
23
|
<WrapperTag
|
|
@@ -15,13 +26,25 @@ const { container: containerClass = '', badge: badgeClass = 'top-2 left-2' } = c
|
|
|
15
26
|
containerClass
|
|
16
27
|
)}
|
|
17
28
|
>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
<!-- Default badge -->
|
|
30
|
+
{(Astro.slots.has('badge') || badge) && (
|
|
31
|
+
<div class={twMerge('absolute z-10 top-2 left-2', badgeClass)}>
|
|
32
|
+
<slot name="badge">
|
|
33
|
+
{badge && <span set:html={badge}/>}
|
|
34
|
+
</slot>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
{(Astro.slots.has('badgeTopRight') || badgeTopRight) && (
|
|
39
|
+
<div class={twMerge('absolute z-10 top-2 right-2', badgeTopRightClass)}>
|
|
40
|
+
<slot name="badgeTopRight">
|
|
41
|
+
{badgeTopRight && <span set:html={badgeTopRight}/>}
|
|
42
|
+
</slot>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
|
|
23
46
|
|
|
24
|
-
<slot name="image"
|
|
47
|
+
<slot name="image"/>
|
|
25
48
|
|
|
26
|
-
<slot
|
|
49
|
+
<slot/>
|
|
27
50
|
</WrapperTag>
|
|
@@ -19,6 +19,9 @@ const {
|
|
|
19
19
|
as = 'article',
|
|
20
20
|
asHeader = 'h3',
|
|
21
21
|
badge = await Astro.slots.render('badge'),
|
|
22
|
+
badgeTopRight = await Astro.slots.render('badgeTopRight'),
|
|
23
|
+
badgeBottomRight = await Astro.slots.render('badgeBottomRight'),
|
|
24
|
+
badgeBottomLeft = await Astro.slots.render('badgeBottomLeft'),
|
|
22
25
|
} = Astro.props;
|
|
23
26
|
|
|
24
27
|
const WrapperHeaderTag = asHeader;
|
|
@@ -27,10 +30,14 @@ const {
|
|
|
27
30
|
title: titleClass = '',
|
|
28
31
|
description: descriptionClass = 'text-muted-foreground',
|
|
29
32
|
image: imageClass = '',
|
|
33
|
+
imageLayout: imageLayout = 'cover',
|
|
30
34
|
icon: iconClass = '',
|
|
31
35
|
quickLink: quickLinkClass = 'font-semibold underline text-primary hover:text-primary/80',
|
|
32
36
|
action: actionClass = '',
|
|
33
|
-
} = classes
|
|
37
|
+
} = classes as {
|
|
38
|
+
// Ensure imageLayout is typed as the Layout union, not just string
|
|
39
|
+
imageLayout?: 'fixed' | 'constrained' | 'fullWidth' | 'cover' | 'responsive' | 'contained';
|
|
40
|
+
} & Record<string, string>;
|
|
34
41
|
|
|
35
42
|
const urlForImage = Array.isArray(callToAction)
|
|
36
43
|
? typeof callToAction[0] === 'string'
|
|
@@ -44,14 +51,36 @@ const urlForImage = Array.isArray(callToAction)
|
|
|
44
51
|
<Card0
|
|
45
52
|
as={as}
|
|
46
53
|
badge={badge}
|
|
54
|
+
badgeTopRight={badgeTopRight}
|
|
55
|
+
badgeBottomRight={badgeBottomRight}
|
|
56
|
+
badgeBottomLeft={badgeBottomLeft}
|
|
47
57
|
classes={{
|
|
48
58
|
content: 'space-y-6',
|
|
59
|
+
badge: classes?.badge,
|
|
60
|
+
badgeTopRight: classes?.badgeTopRight,
|
|
49
61
|
}}
|
|
50
62
|
>
|
|
51
63
|
<Fragment slot="image">
|
|
52
64
|
{
|
|
53
65
|
image && (
|
|
54
|
-
<div class={twMerge('w-full overflow-hidden -mt-6 h-60 bg-gray-400 dark:bg-zinc-700', imageClass)}>
|
|
66
|
+
<div class={twMerge('relative w-full overflow-hidden -mt-6 h-60 bg-gray-400 dark:bg-zinc-700', imageClass)}>
|
|
67
|
+
|
|
68
|
+
{(Astro.slots.has('badgeBottomRight') || badgeBottomRight) && (
|
|
69
|
+
<div class={twMerge('absolute z-10 bottom-2 right-2', classes?.badgeBottomRight)}>
|
|
70
|
+
<slot name="badgeBottomRight">
|
|
71
|
+
{badgeBottomRight && <span set:html={badgeBottomRight}/>}
|
|
72
|
+
</slot>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{(Astro.slots.has('badgeBottomLeft') || badgeBottomLeft) && (
|
|
77
|
+
<div class={twMerge('absolute z-10 bottom-2 left-2', classes?.badgeBottomLeft)}>
|
|
78
|
+
<slot name="badgeBottomLeft">
|
|
79
|
+
{badgeBottomLeft && <span set:html={badgeBottomLeft}/>}
|
|
80
|
+
</slot>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
|
|
55
84
|
{urlForImage ? (
|
|
56
85
|
<a href={urlForImage} class="group">
|
|
57
86
|
{typeof image === 'string' ? (
|
|
@@ -63,7 +92,7 @@ const urlForImage = Array.isArray(callToAction)
|
|
|
63
92
|
width={400}
|
|
64
93
|
height={400}
|
|
65
94
|
sizes="(max-width: 900px) 400px, 900px"
|
|
66
|
-
layout=
|
|
95
|
+
layout={imageLayout}
|
|
67
96
|
loading="lazy"
|
|
68
97
|
decoding="async"
|
|
69
98
|
{...image}
|
|
@@ -81,7 +110,7 @@ const urlForImage = Array.isArray(callToAction)
|
|
|
81
110
|
width={400}
|
|
82
111
|
height={400}
|
|
83
112
|
sizes="(max-width: 900px) 400px, 900px"
|
|
84
|
-
layout=
|
|
113
|
+
layout={imageLayout}
|
|
85
114
|
loading="lazy"
|
|
86
115
|
decoding="async"
|
|
87
116
|
{...image}
|
|
@@ -17,6 +17,9 @@ const {
|
|
|
17
17
|
as = 'article',
|
|
18
18
|
asHeader = 'h3',
|
|
19
19
|
badge = await Astro.slots.render('badge'),
|
|
20
|
+
badgeTopRight = await Astro.slots.render('badgeTopRight'),
|
|
21
|
+
badgeBottomRight = await Astro.slots.render('badgeBottomRight'),
|
|
22
|
+
badgeBottomLeft = await Astro.slots.render('badgeBottomLeft'),
|
|
20
23
|
} = Astro.props;
|
|
21
24
|
|
|
22
25
|
const WrapperHeaderTag = asHeader;
|
|
@@ -27,23 +30,48 @@ const {
|
|
|
27
30
|
content: contentClass = 'text-muted-foreground',
|
|
28
31
|
icon: iconClass = '',
|
|
29
32
|
image: imageClass = '',
|
|
33
|
+
imageLayout: imageLayout = 'cover',
|
|
30
34
|
badge: badgeClass = 'top-2 left-2',
|
|
31
|
-
} = classes
|
|
35
|
+
} = classes as {
|
|
36
|
+
// Ensure imageLayout is typed as the Layout union, not just string
|
|
37
|
+
imageLayout?: 'fixed' | 'constrained' | 'fullWidth' | 'cover' | 'responsive' | 'contained';
|
|
38
|
+
} & Record<string, string>;
|
|
32
39
|
---
|
|
33
40
|
|
|
34
41
|
<Card0
|
|
35
42
|
as={as}
|
|
36
43
|
badge={badge}
|
|
44
|
+
badgeTopRight={badgeTopRight}
|
|
45
|
+
badgeBottomRight={badgeBottomRight}
|
|
46
|
+
badgeBottomLeft={badgeBottomLeft}
|
|
37
47
|
classes={{
|
|
38
48
|
container: cn('group', containerClass),
|
|
39
49
|
badge: badgeClass,
|
|
50
|
+
badgeTopRight: classes?.badgeTopRight,
|
|
40
51
|
}}
|
|
41
52
|
>
|
|
42
53
|
<!-- Image -->
|
|
43
54
|
<Fragment slot="image">
|
|
44
55
|
{
|
|
45
56
|
image && (
|
|
46
|
-
<div class={twMerge('w-full overflow-hidden -mt-6 h-40 bg-gray-400 dark:bg-zinc-700', imageClass)}>
|
|
57
|
+
<div class={twMerge('relative w-full overflow-hidden -mt-6 h-40 bg-gray-400 dark:bg-zinc-700', imageClass)}>
|
|
58
|
+
|
|
59
|
+
{(Astro.slots.has('badgeBottomRight') || badgeBottomRight) && (
|
|
60
|
+
<div class={twMerge('absolute z-10 bottom-2 right-2', classes?.badgeBottomRight)}>
|
|
61
|
+
<slot name="badgeBottomRight">
|
|
62
|
+
{badgeBottomRight && <span set:html={badgeBottomRight}/>}
|
|
63
|
+
</slot>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{(Astro.slots.has('badgeBottomLeft') || badgeBottomLeft) && (
|
|
68
|
+
<div class={twMerge('absolute z-10 bottom-2 left-2', classes?.badgeBottomLeft)}>
|
|
69
|
+
<slot name="badgeBottomLeft">
|
|
70
|
+
{badgeBottomLeft && <span set:html={badgeBottomLeft}/>}
|
|
71
|
+
</slot>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
47
75
|
{typeof image === 'string' ? (
|
|
48
76
|
<Fragment set:html={image} />
|
|
49
77
|
) : (
|
|
@@ -57,7 +85,7 @@ const {
|
|
|
57
85
|
width={400}
|
|
58
86
|
height={400}
|
|
59
87
|
sizes="(max-width: 900px) 400px, 900px"
|
|
60
|
-
layout=
|
|
88
|
+
layout={imageLayout}
|
|
61
89
|
loading="lazy"
|
|
62
90
|
decoding="async"
|
|
63
91
|
{...image}
|
|
@@ -15,6 +15,9 @@ const {
|
|
|
15
15
|
as = 'article',
|
|
16
16
|
asHeader = 'h3',
|
|
17
17
|
badge = await Astro.slots.render('badge'),
|
|
18
|
+
badgeTopRight = await Astro.slots.render('badgeTopRight'),
|
|
19
|
+
badgeBottomRight = await Astro.slots.render('badgeBottomRight'),
|
|
20
|
+
badgeBottomLeft = await Astro.slots.render('badgeBottomLeft'),
|
|
18
21
|
} = Astro.props;
|
|
19
22
|
|
|
20
23
|
const WrapperHeaderTag = asHeader;
|
|
@@ -24,9 +27,13 @@ const {
|
|
|
24
27
|
title: titleClass = '',
|
|
25
28
|
description: descriptionClass = 'text-muted-foreground',
|
|
26
29
|
image: imageClass = '',
|
|
30
|
+
imageLayout: imageLayout = 'cover',
|
|
27
31
|
action: actionClass = '',
|
|
28
32
|
badge: badgeClass = 'top-2 left-2',
|
|
29
|
-
} = classes
|
|
33
|
+
} = classes as {
|
|
34
|
+
// Ensure imageLayout is typed as the Layout union, not just string
|
|
35
|
+
imageLayout?: 'fixed' | 'constrained' | 'fullWidth' | 'cover' | 'responsive' | 'contained';
|
|
36
|
+
} & Record<string, string>;
|
|
30
37
|
|
|
31
38
|
const urlForImage = Array.isArray(callToAction)
|
|
32
39
|
? typeof callToAction[0] === 'string'
|
|
@@ -40,17 +47,39 @@ const urlForImage = Array.isArray(callToAction)
|
|
|
40
47
|
<Card0
|
|
41
48
|
as={as}
|
|
42
49
|
badge={badge}
|
|
50
|
+
badgeTopRight={badgeTopRight}
|
|
51
|
+
badgeBottomRight={badgeBottomRight}
|
|
52
|
+
badgeBottomLeft={badgeBottomLeft}
|
|
43
53
|
classes={{
|
|
44
54
|
container: twMerge(containerClass, 'justify-start py-0'),
|
|
45
55
|
badge: badgeClass,
|
|
56
|
+
badgeTopRight: classes?.badgeTopRight,
|
|
46
57
|
}}
|
|
47
58
|
>
|
|
48
59
|
<Fragment slot="image">
|
|
49
60
|
{
|
|
50
61
|
image && (
|
|
51
62
|
<div
|
|
52
|
-
class={twMerge('w-full aspect-square overflow-hidden rounded-lg bg-gray-400 dark:bg-zinc-700', imageClass)}
|
|
63
|
+
class={twMerge('relative w-full aspect-square overflow-hidden rounded-lg bg-gray-400 dark:bg-zinc-700', imageClass)}
|
|
53
64
|
>
|
|
65
|
+
|
|
66
|
+
{(Astro.slots.has('badgeBottomRight') || badgeBottomRight) && (
|
|
67
|
+
<div class={twMerge('absolute z-10 bottom-2 right-2', classes?.badgeBottomRight)}>
|
|
68
|
+
<slot name="badgeBottomRight">
|
|
69
|
+
{badgeBottomRight && <span set:html={badgeBottomRight}/>}
|
|
70
|
+
</slot>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{(Astro.slots.has('badgeBottomLeft') || badgeBottomLeft) && (
|
|
75
|
+
<div class={twMerge('absolute z-10 bottom-2 left-2', classes?.badgeBottomLeft)}>
|
|
76
|
+
<slot name="badgeBottomLeft">
|
|
77
|
+
{badgeBottomLeft && <span set:html={badgeBottomLeft}/>}
|
|
78
|
+
</slot>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
|
|
54
83
|
{urlForImage ? (
|
|
55
84
|
<a href={urlForImage} class="group">
|
|
56
85
|
{typeof image === 'string' ? (
|
|
@@ -62,7 +91,7 @@ const urlForImage = Array.isArray(callToAction)
|
|
|
62
91
|
width={400}
|
|
63
92
|
height={400}
|
|
64
93
|
sizes="(max-width: 900px) 400px, 900px"
|
|
65
|
-
layout=
|
|
94
|
+
layout={imageLayout}
|
|
66
95
|
loading="lazy"
|
|
67
96
|
decoding="async"
|
|
68
97
|
{...image}
|
|
@@ -80,7 +109,7 @@ const urlForImage = Array.isArray(callToAction)
|
|
|
80
109
|
width={400}
|
|
81
110
|
height={400}
|
|
82
111
|
sizes="(max-width: 900px) 400px, 900px"
|
|
83
|
-
layout=
|
|
112
|
+
layout={imageLayout}
|
|
84
113
|
loading="lazy"
|
|
85
114
|
decoding="async"
|
|
86
115
|
{...image}
|
|
@@ -15,6 +15,9 @@ const {
|
|
|
15
15
|
as = 'article',
|
|
16
16
|
asHeader = 'h3',
|
|
17
17
|
badge = await Astro.slots.render('badge'),
|
|
18
|
+
badgeTopRight = await Astro.slots.render('badgeTopRight'),
|
|
19
|
+
badgeBottomRight = await Astro.slots.render('badgeBottomRight'),
|
|
20
|
+
badgeBottomLeft = await Astro.slots.render('badgeBottomLeft'),
|
|
18
21
|
widths = [800, 1600],
|
|
19
22
|
size = 800,
|
|
20
23
|
sizes = '(max-width: 1600px) 800px, 1600px'
|
|
@@ -28,9 +31,13 @@ const {
|
|
|
28
31
|
title: titleClass = '',
|
|
29
32
|
description: descriptionClass = '',
|
|
30
33
|
image: imageClass = '',
|
|
34
|
+
imageLayout: imageLayout = 'cover',
|
|
31
35
|
badge: badgeClass = 'top-2 left-2',
|
|
32
36
|
aspect: aspectClass = 'pb-[100%]',
|
|
33
|
-
} = classes
|
|
37
|
+
} = classes as {
|
|
38
|
+
// Ensure imageLayout is typed as the Layout union, not just string
|
|
39
|
+
imageLayout?: 'fixed' | 'constrained' | 'fullWidth' | 'cover' | 'responsive' | 'contained';
|
|
40
|
+
} & Record<string, string>;
|
|
34
41
|
|
|
35
42
|
const urlForImage = Array.isArray(callToAction)
|
|
36
43
|
? typeof callToAction[0] === 'string'
|
|
@@ -45,9 +52,13 @@ const LinkWrapperTag = urlForImage ? 'a' : 'div';
|
|
|
45
52
|
|
|
46
53
|
<Card0
|
|
47
54
|
as={as}
|
|
55
|
+
badgeTopRight={badgeTopRight}
|
|
56
|
+
badgeBottomRight={badgeBottomRight}
|
|
57
|
+
badgeBottomLeft={badgeBottomLeft}
|
|
48
58
|
classes={{
|
|
49
59
|
container: cn('relative h-full bg-gray-200 dark:bg-zinc-700 border-transparent text-inherit justify-start py-0 @container', containerClass),
|
|
50
60
|
badge: badgeClass,
|
|
61
|
+
badgeTopRight: classes?.badgeTopRight,
|
|
51
62
|
}}
|
|
52
63
|
>
|
|
53
64
|
<!-- Content & link -->
|
|
@@ -55,6 +66,22 @@ const LinkWrapperTag = urlForImage ? 'a' : 'div';
|
|
|
55
66
|
<div class={cn("p-6 @2xl:p-10 pt-8 @2xl:pt-12", contentClass, !image && 'bg-transparent')}>
|
|
56
67
|
{badge && (
|
|
57
68
|
<div class="" set:html={badge}></div>)}
|
|
69
|
+
|
|
70
|
+
{(Astro.slots.has('badgeBottomRight') || badgeBottomRight) && (
|
|
71
|
+
<div class={twMerge('absolute z-10 bottom-2 right-2', classes?.badgeBottomRight)}>
|
|
72
|
+
<slot name="badgeBottomRight">
|
|
73
|
+
{badgeBottomRight && <span set:html={badgeBottomRight}/>}
|
|
74
|
+
</slot>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{(Astro.slots.has('badgeBottomLeft') || badgeBottomLeft) && (
|
|
79
|
+
<div class={twMerge('absolute z-10 bottom-2 left-2', classes?.badgeBottomLeft)}>
|
|
80
|
+
<slot name="badgeBottomLeft">
|
|
81
|
+
{badgeBottomLeft && <span set:html={badgeBottomLeft}/>}
|
|
82
|
+
</slot>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
58
85
|
|
|
59
86
|
<WrapperHeaderTag
|
|
60
87
|
class={cn('font-bold text-lg @lg:text-xl @2xl:text-2xl ', urlForImage && 'group-hover:underline', titleClass)}>
|
|
@@ -102,7 +129,7 @@ const LinkWrapperTag = urlForImage ? 'a' : 'div';
|
|
|
102
129
|
width={size}
|
|
103
130
|
height={size}
|
|
104
131
|
sizes={sizes}
|
|
105
|
-
layout=
|
|
132
|
+
layout={imageLayout}
|
|
106
133
|
loading="lazy"
|
|
107
134
|
decoding="async"
|
|
108
135
|
{...image}
|
|
@@ -33,14 +33,16 @@ const {
|
|
|
33
33
|
>
|
|
34
34
|
<div class="flex items-center relative">
|
|
35
35
|
<div class={cn("flex flex-col gap-3 justify-between h-full p-4 ", callToAction && 'border-r dark:border-white/10 border-black/10')}>
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
{title && (callToAction ? (
|
|
37
38
|
<a href={callToAction.href} class="flex items-center">
|
|
38
39
|
<WrapperHeaderTag class={cn('text-lg md:text-xl font-bold', titleClass)}>{title}</WrapperHeaderTag>
|
|
39
40
|
<span class="absolute inset-0" aria-hidden="true"></span>
|
|
40
41
|
</a>
|
|
41
42
|
) : (
|
|
42
43
|
<WrapperHeaderTag class={cn('text-lg md:text-xl font-bold', titleClass)}>{title}</WrapperHeaderTag>
|
|
43
|
-
)}
|
|
44
|
+
))}
|
|
45
|
+
|
|
44
46
|
{description && (
|
|
45
47
|
<p class={cn('text-muted-foreground text-sm/5 md:text-base', descriptionClass)} set:html={description}/>
|
|
46
48
|
)}
|
|
@@ -24,7 +24,7 @@ const {
|
|
|
24
24
|
...rest
|
|
25
25
|
} = Astro.props;
|
|
26
26
|
|
|
27
|
-
const images = await fetchLocalImages()
|
|
27
|
+
const images = await fetchLocalImages() as Record<string, () => any>;
|
|
28
28
|
const imagePaths = Object.keys(images).filter((imagePath) => {
|
|
29
29
|
return imagePath.startsWith(`/src/assets/images/${imagesFolder}/`);
|
|
30
30
|
});
|
|
@@ -61,16 +61,15 @@ const imagePaths = Object.keys(images).filter((imagePath) => {
|
|
|
61
61
|
data-pswp-width={optimizedImage.attributes.width}
|
|
62
62
|
data-pswp-height={optimizedImage.attributes.height}
|
|
63
63
|
target="_blank"
|
|
64
|
-
class="group overflow-hidden rounded-md border-primary cursor-zoom-in block"
|
|
64
|
+
class="group overflow-hidden rounded-md border-primary cursor-zoom-in block aspect-square"
|
|
65
65
|
>
|
|
66
66
|
<AstroImage
|
|
67
67
|
src={image}
|
|
68
68
|
alt={'altText'}
|
|
69
69
|
widths={[400, 900]}
|
|
70
70
|
width={400}
|
|
71
|
-
height={400}
|
|
72
71
|
sizes="(max-width: 900px) 400px, 900px"
|
|
73
|
-
class="
|
|
72
|
+
class="object-cover object-top w-full md:h-full group-hover:scale-105 transition duration-300"
|
|
74
73
|
/>
|
|
75
74
|
</a>
|
|
76
75
|
</swiper-slide>
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// Utility Types
|
|
2
|
+
import type {HTMLAttributes} from "astro/types";
|
|
3
|
+
import type {ImageMetadata} from "astro";
|
|
4
|
+
|
|
2
5
|
export type ClassValue = ClassArray | ClassDictionary | string | number | bigint | null | boolean | undefined;
|
|
3
6
|
export type ClassDictionary = Record<string, any>;
|
|
4
7
|
export type ClassArray = ClassValue[];
|
|
@@ -47,6 +50,23 @@ export type Image = {
|
|
|
47
50
|
alt?: string;
|
|
48
51
|
aspectRatio?: string;
|
|
49
52
|
class?: string;
|
|
53
|
+
} & Omit<HTMLAttributes<'img'>, 'src'> & {
|
|
54
|
+
src?: string | ImageMetadata | null;
|
|
55
|
+
width?: string | number | null;
|
|
56
|
+
height?: string | number | null;
|
|
57
|
+
alt?: string | null;
|
|
58
|
+
loading?: 'eager' | 'lazy' | null;
|
|
59
|
+
decoding?: 'sync' | 'async' | 'auto' | null;
|
|
60
|
+
class?: string;
|
|
61
|
+
style?: string;
|
|
62
|
+
srcset?: string | null;
|
|
63
|
+
sizes?: string | null;
|
|
64
|
+
fetchpriority?: 'high' | 'low' | 'auto' | null;
|
|
65
|
+
layout?: Layout;
|
|
66
|
+
widths?: number[] | null;
|
|
67
|
+
aspectRatio?: string | number | null;
|
|
68
|
+
objectPosition?: string;
|
|
69
|
+
format?: string;
|
|
50
70
|
};
|
|
51
71
|
|
|
52
72
|
export type Testimonial = {
|
package/package.json
CHANGED
package/styles/styles.css
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
|
8
8
|
"Courier New", monospace;
|
|
9
9
|
--color-red-500: oklch(0.637 0.237 25.331);
|
|
10
|
+
--color-red-700: oklch(0.505 0.213 27.518);
|
|
10
11
|
--color-orange-300: oklch(0.837 0.128 66.29);
|
|
12
|
+
--color-amber-500: oklch(0.769 0.188 70.08);
|
|
11
13
|
--color-green-600: oklch(0.627 0.194 149.214);
|
|
12
14
|
--color-green-700: oklch(0.527 0.154 150.069);
|
|
13
15
|
--color-emerald-200: oklch(0.905 0.093 164.15);
|
|
16
|
+
--color-blue-700: oklch(0.488 0.243 264.376);
|
|
14
17
|
--color-violet-500: oklch(0.606 0.25 292.717);
|
|
15
18
|
--color-purple-500: oklch(0.627 0.265 303.9);
|
|
16
19
|
--color-purple-800: oklch(0.438 0.218 303.724);
|
|
@@ -312,12 +315,18 @@
|
|
|
312
315
|
.right-0 {
|
|
313
316
|
right: calc(var(--spacing) * 0);
|
|
314
317
|
}
|
|
318
|
+
.right-2 {
|
|
319
|
+
right: calc(var(--spacing) * 2);
|
|
320
|
+
}
|
|
315
321
|
.right-5 {
|
|
316
322
|
right: calc(var(--spacing) * 5);
|
|
317
323
|
}
|
|
318
324
|
.bottom-0 {
|
|
319
325
|
bottom: calc(var(--spacing) * 0);
|
|
320
326
|
}
|
|
327
|
+
.bottom-2 {
|
|
328
|
+
bottom: calc(var(--spacing) * 2);
|
|
329
|
+
}
|
|
321
330
|
.-left-5 {
|
|
322
331
|
left: calc(var(--spacing) * -5);
|
|
323
332
|
}
|
|
@@ -580,9 +589,15 @@
|
|
|
580
589
|
.h-8 {
|
|
581
590
|
height: calc(var(--spacing) * 8);
|
|
582
591
|
}
|
|
592
|
+
.h-9 {
|
|
593
|
+
height: calc(var(--spacing) * 9);
|
|
594
|
+
}
|
|
583
595
|
.h-10 {
|
|
584
596
|
height: calc(var(--spacing) * 10);
|
|
585
597
|
}
|
|
598
|
+
.h-11 {
|
|
599
|
+
height: calc(var(--spacing) * 11);
|
|
600
|
+
}
|
|
586
601
|
.h-12 {
|
|
587
602
|
height: calc(var(--spacing) * 12);
|
|
588
603
|
}
|
|
@@ -1386,6 +1401,9 @@
|
|
|
1386
1401
|
.whitespace-nowrap {
|
|
1387
1402
|
white-space: nowrap;
|
|
1388
1403
|
}
|
|
1404
|
+
.text-amber-500 {
|
|
1405
|
+
color: var(--color-amber-500);
|
|
1406
|
+
}
|
|
1389
1407
|
.text-black {
|
|
1390
1408
|
color: var(--color-black);
|
|
1391
1409
|
}
|
|
@@ -1395,6 +1413,9 @@
|
|
|
1395
1413
|
.text-black\/70 {
|
|
1396
1414
|
color: color-mix(in oklab, var(--color-black) 70%, transparent);
|
|
1397
1415
|
}
|
|
1416
|
+
.text-blue-700 {
|
|
1417
|
+
color: var(--color-blue-700);
|
|
1418
|
+
}
|
|
1398
1419
|
.text-gray-50 {
|
|
1399
1420
|
color: var(--color-gray-50);
|
|
1400
1421
|
}
|
|
@@ -1404,9 +1425,6 @@
|
|
|
1404
1425
|
.text-gray-500 {
|
|
1405
1426
|
color: var(--color-gray-500);
|
|
1406
1427
|
}
|
|
1407
|
-
.text-gray-600 {
|
|
1408
|
-
color: var(--color-gray-600);
|
|
1409
|
-
}
|
|
1410
1428
|
.text-gray-700 {
|
|
1411
1429
|
color: var(--color-gray-700);
|
|
1412
1430
|
}
|
|
@@ -1419,6 +1437,9 @@
|
|
|
1419
1437
|
.text-red-500 {
|
|
1420
1438
|
color: var(--color-red-500);
|
|
1421
1439
|
}
|
|
1440
|
+
.text-red-700 {
|
|
1441
|
+
color: var(--color-red-700);
|
|
1442
|
+
}
|
|
1422
1443
|
.text-slate-800 {
|
|
1423
1444
|
color: var(--color-slate-800);
|
|
1424
1445
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Example Environment Variables for vendor-config Integration
|
|
2
|
+
# Copy this to your project root as .env.development, .env.production, etc.
|
|
3
|
+
|
|
4
|
+
# Site Configuration
|
|
5
|
+
SITE_NAME="My Website"
|
|
6
|
+
SITE_URL="https://example.com"
|
|
7
|
+
SITE_TITLE="My Awesome Site"
|
|
8
|
+
SITE_DESCRIPTION="A comprehensive website built with Astro"
|
|
9
|
+
BASE_PATH="/"
|
|
10
|
+
|
|
11
|
+
# SEO & Verification
|
|
12
|
+
GOOGLE_SITE_VERIFICATION=""
|
|
13
|
+
ROBOTS_INDEX=true
|
|
14
|
+
ROBOTS_FOLLOW=true
|
|
15
|
+
|
|
16
|
+
# Social Media
|
|
17
|
+
TWITTER_HANDLE="@myhandle"
|
|
18
|
+
TWITTER_SITE="@myhandle"
|
|
19
|
+
OG_SITE_NAME="My Website"
|
|
20
|
+
|
|
21
|
+
# Internationalization
|
|
22
|
+
LANG="en"
|
|
23
|
+
TEXT_DIRECTION="ltr"
|
|
24
|
+
|
|
25
|
+
# Blog Configuration
|
|
26
|
+
BLOG_ENABLED=true
|
|
27
|
+
POSTS_PER_PAGE=6
|
|
28
|
+
BLOG_POST_INDEX=true
|
|
29
|
+
RELATED_POSTS_ENABLED=true
|
|
30
|
+
RELATED_POSTS_COUNT=4
|
|
31
|
+
|
|
32
|
+
# Analytics
|
|
33
|
+
GA_ID="" # e.g., "G-XXXXXXXXXX"
|
|
34
|
+
|
|
35
|
+
# UI Settings
|
|
36
|
+
UI_THEME="system" # Options: system | light | dark | light:only | dark:only
|
|
37
|
+
|
|
38
|
+
# Example: Development Environment
|
|
39
|
+
# SITE_URL="http://localhost:4321"
|
|
40
|
+
# SITE_NAME="My Website (Dev)"
|
|
41
|
+
# GA_ID="" # Disable analytics in development
|
|
42
|
+
# ROBOTS_INDEX=false # Don't index development site
|
|
43
|
+
|
|
44
|
+
# Example: Staging Environment
|
|
45
|
+
# SITE_URL="https://staging.example.com"
|
|
46
|
+
# SITE_NAME="My Website (Staging)"
|
|
47
|
+
# ROBOTS_INDEX=false # Don't index staging site
|
|
48
|
+
|
|
49
|
+
# Example: Production Environment
|
|
50
|
+
# SITE_URL="https://example.com"
|
|
51
|
+
# SITE_NAME="My Website"
|
|
52
|
+
# GA_ID="G-XXXXXXXXXX"
|
|
53
|
+
# ROBOTS_INDEX=true
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Vendor Config Integration
|
|
2
|
+
|
|
3
|
+
A custom Astro integration that loads YAML configuration files and provides them as a virtual module with native environment variable support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **YAML Configuration Loading**: Load configuration from `.yaml` or `.yml` files
|
|
8
|
+
- **Environment Variable Interpolation**: Native support for environment variables with default values
|
|
9
|
+
- **Virtual Module**: Access configuration via `vendor:config` import
|
|
10
|
+
- **Auto-generated TypeScript Definitions**: Type-safe configuration access
|
|
11
|
+
- **Hot Reload**: Automatic reload on configuration file changes during development
|
|
12
|
+
- **robots.txt Integration**: Automatically updates robots.txt with sitemap reference
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
This integration is built-in to `@yatoday/astro-ui`. To use it in your Astro project:
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
// astro.config.mjs
|
|
20
|
+
import vendorConfig from '@yatoday/astro-ui/vendor-config';
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
integrations: [
|
|
24
|
+
vendorConfig({
|
|
25
|
+
config: 'src/config.yaml' // Path to your config file
|
|
26
|
+
})
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Environment Variable Support
|
|
32
|
+
|
|
33
|
+
The integration natively supports environment variable interpolation in YAML configuration files using the `${VAR:default}` syntax.
|
|
34
|
+
|
|
35
|
+
### Syntax
|
|
36
|
+
|
|
37
|
+
- **With default value**: `${VAR_NAME:default_value}`
|
|
38
|
+
- **Without default**: `${VAR_NAME}`
|
|
39
|
+
|
|
40
|
+
### Behavior
|
|
41
|
+
|
|
42
|
+
1. If the environment variable is set, its value is used
|
|
43
|
+
2. If the environment variable is not set and a default is provided, the default is used
|
|
44
|
+
3. If the environment variable is not set and no default is provided, the placeholder remains unchanged
|
|
45
|
+
|
|
46
|
+
### Examples
|
|
47
|
+
|
|
48
|
+
#### Example 1: Basic Usage
|
|
49
|
+
|
|
50
|
+
**config.yaml**:
|
|
51
|
+
```yaml
|
|
52
|
+
site:
|
|
53
|
+
name: 'My Site'
|
|
54
|
+
url: '${SITE_URL:https://example.com}'
|
|
55
|
+
api_key: '${API_KEY}'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**With environment variables set**:
|
|
59
|
+
```bash
|
|
60
|
+
SITE_URL=https://prod.example.com
|
|
61
|
+
API_KEY=secret123
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Result**:
|
|
65
|
+
```javascript
|
|
66
|
+
{
|
|
67
|
+
site: {
|
|
68
|
+
name: 'My Site',
|
|
69
|
+
url: 'https://prod.example.com',
|
|
70
|
+
api_key: 'secret123'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Without environment variables**:
|
|
76
|
+
```javascript
|
|
77
|
+
{
|
|
78
|
+
site: {
|
|
79
|
+
name: 'My Site',
|
|
80
|
+
url: 'https://example.com', // Uses default
|
|
81
|
+
api_key: '${API_KEY}' // Placeholder preserved
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Example 2: Multi-Environment Setup
|
|
87
|
+
|
|
88
|
+
**config.yaml**:
|
|
89
|
+
```yaml
|
|
90
|
+
site:
|
|
91
|
+
site: '${SITE_URL:https://localhost:3000}'
|
|
92
|
+
base: '${BASE_PATH:/}'
|
|
93
|
+
|
|
94
|
+
metadata:
|
|
95
|
+
title:
|
|
96
|
+
default: '${SITE_NAME:My App}'
|
|
97
|
+
|
|
98
|
+
analytics:
|
|
99
|
+
vendors:
|
|
100
|
+
googleAnalytics:
|
|
101
|
+
id: '${GA_ID:}'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Development (.env.development)**:
|
|
105
|
+
```bash
|
|
106
|
+
SITE_URL=http://localhost:4321
|
|
107
|
+
BASE_PATH=/
|
|
108
|
+
SITE_NAME=My App (Dev)
|
|
109
|
+
# GA_ID not set - uses empty default
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Production (environment variables)**:
|
|
113
|
+
```bash
|
|
114
|
+
SITE_URL=https://production.example.com
|
|
115
|
+
BASE_PATH=/app
|
|
116
|
+
SITE_NAME=My Production App
|
|
117
|
+
GA_ID=G-XXXXXXXXXX
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Example 3: Numeric and Boolean Values
|
|
121
|
+
|
|
122
|
+
**config.yaml**:
|
|
123
|
+
```yaml
|
|
124
|
+
config:
|
|
125
|
+
# Unquoted values will be parsed as their YAML type
|
|
126
|
+
timeout: ${TIMEOUT:5000} # Number
|
|
127
|
+
max_retries: ${MAX_RETRIES:3} # Number
|
|
128
|
+
debug: ${DEBUG:false} # Boolean
|
|
129
|
+
enabled: ${ENABLED:true} # Boolean
|
|
130
|
+
|
|
131
|
+
# Quoted values remain as strings
|
|
132
|
+
port: '${PORT:3000}' # String
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Example 4: Complex URLs with Colons
|
|
136
|
+
|
|
137
|
+
**config.yaml**:
|
|
138
|
+
```yaml
|
|
139
|
+
api:
|
|
140
|
+
# Colons in default values are handled correctly
|
|
141
|
+
endpoint: '${API_ENDPOINT:https://api.example.com:8080/v1}'
|
|
142
|
+
websocket: '${WS_URL:wss://ws.example.com:9000/socket}'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Example 5: Composed Values
|
|
146
|
+
|
|
147
|
+
**config.yaml**:
|
|
148
|
+
```yaml
|
|
149
|
+
site:
|
|
150
|
+
protocol: '${PROTOCOL:https}'
|
|
151
|
+
host: '${HOST:example.com}'
|
|
152
|
+
port: '${PORT:443}'
|
|
153
|
+
|
|
154
|
+
# Combine multiple environment variables
|
|
155
|
+
full_url: '${PROTOCOL}://${HOST}:${PORT}'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Variable Naming Rules
|
|
159
|
+
|
|
160
|
+
Environment variable names must:
|
|
161
|
+
- Start with an uppercase letter or underscore (`A-Z`, `_`)
|
|
162
|
+
- Contain only uppercase letters, numbers, or underscores (`A-Z`, `0-9`, `_`)
|
|
163
|
+
|
|
164
|
+
**Valid**: `SITE_URL`, `API_KEY`, `_INTERNAL_VAR`, `CONFIG_V2`
|
|
165
|
+
|
|
166
|
+
**Invalid**: `siteUrl`, `api-key`, `123VAR`
|
|
167
|
+
|
|
168
|
+
## Usage in Your Code
|
|
169
|
+
|
|
170
|
+
Once the integration is configured, import the configuration in your Astro components or pages:
|
|
171
|
+
|
|
172
|
+
```astro
|
|
173
|
+
---
|
|
174
|
+
import { SITE, METADATA, ANALYTICS } from 'vendor:config';
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
<head>
|
|
178
|
+
<title>{METADATA.title.default}</title>
|
|
179
|
+
<meta name="description" content={METADATA.description} />
|
|
180
|
+
<link rel="canonical" href={SITE.site} />
|
|
181
|
+
</head>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Configuration Structure
|
|
185
|
+
|
|
186
|
+
The integration expects a YAML file with the following optional sections:
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
site:
|
|
190
|
+
site: string # Main site URL
|
|
191
|
+
base: string # Base path for deployment
|
|
192
|
+
trailingSlash: boolean
|
|
193
|
+
|
|
194
|
+
metadata:
|
|
195
|
+
title:
|
|
196
|
+
default: string
|
|
197
|
+
template: string
|
|
198
|
+
description: string
|
|
199
|
+
robots:
|
|
200
|
+
index: boolean
|
|
201
|
+
follow: boolean
|
|
202
|
+
openGraph: { ... }
|
|
203
|
+
twitter: { ... }
|
|
204
|
+
|
|
205
|
+
i18n:
|
|
206
|
+
language: string
|
|
207
|
+
textDirection: string
|
|
208
|
+
|
|
209
|
+
apps:
|
|
210
|
+
blog:
|
|
211
|
+
isEnabled: boolean
|
|
212
|
+
postsPerPage: number
|
|
213
|
+
# ... blog configuration
|
|
214
|
+
|
|
215
|
+
analytics:
|
|
216
|
+
vendors:
|
|
217
|
+
googleAnalytics:
|
|
218
|
+
id: string | null
|
|
219
|
+
|
|
220
|
+
ui:
|
|
221
|
+
theme: string # 'system' | 'light' | 'dark' | 'light:only' | 'dark:only'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Advanced: Passing Configuration Directly
|
|
225
|
+
|
|
226
|
+
You can also pass configuration as a JavaScript object instead of a file path:
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
// astro.config.mjs
|
|
230
|
+
export default defineConfig({
|
|
231
|
+
integrations: [
|
|
232
|
+
vendorConfig({
|
|
233
|
+
config: {
|
|
234
|
+
site: {
|
|
235
|
+
name: 'My Site',
|
|
236
|
+
site: 'https://example.com'
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Note: Environment variable interpolation only works with YAML files, not with direct object configuration.
|
|
245
|
+
|
|
246
|
+
## Benefits Over Manual Preprocessing
|
|
247
|
+
|
|
248
|
+
This native implementation offers several advantages:
|
|
249
|
+
|
|
250
|
+
1. **No Build Scripts Required**: Environment variables are processed automatically during the Astro build
|
|
251
|
+
2. **Hot Reload Support**: Configuration changes (including env var updates) are reflected immediately during development
|
|
252
|
+
3. **Type Safety**: Auto-generated TypeScript definitions provide intellisense and type checking
|
|
253
|
+
4. **Standard Syntax**: Uses familiar `${VAR:default}` syntax common in many configuration systems
|
|
254
|
+
5. **Clean Integration**: Works seamlessly with Astro's build pipeline
|
|
255
|
+
|
|
256
|
+
## Migration from Prebuild Scripts
|
|
257
|
+
|
|
258
|
+
If you're currently using a prebuild script for environment variable substitution:
|
|
259
|
+
|
|
260
|
+
1. **Remove** your prebuild script from `package.json`
|
|
261
|
+
2. **Update** your `config.yaml` to use the `${VAR:default}` syntax
|
|
262
|
+
3. **Rebuild** your project - environment variables will be processed automatically
|
|
263
|
+
|
|
264
|
+
**Before** (with prebuild script):
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"scripts": {
|
|
268
|
+
"prebuild": "node scripts/process-env-vars.js",
|
|
269
|
+
"build": "astro build"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**After** (native support):
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"scripts": {
|
|
278
|
+
"build": "astro build"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Troubleshooting
|
|
284
|
+
|
|
285
|
+
### Environment variable not being replaced
|
|
286
|
+
|
|
287
|
+
1. **Check variable name format**: Must be uppercase with underscores (e.g., `SITE_URL`, not `siteUrl`)
|
|
288
|
+
2. **Verify quotes**: YAML strings should be quoted: `'${VAR:default}'`
|
|
289
|
+
3. **Check environment**: Ensure the variable is set in your environment or deployment platform
|
|
290
|
+
|
|
291
|
+
### Type mismatch errors
|
|
292
|
+
|
|
293
|
+
- YAML parses unquoted values according to type
|
|
294
|
+
- Use quotes for string values: `'${PORT:3000}'` → string `'3000'`
|
|
295
|
+
- Omit quotes for numbers/booleans: `${PORT:3000}` → number `3000`
|
|
296
|
+
|
|
297
|
+
### Default value with colons not working
|
|
298
|
+
|
|
299
|
+
- The integration correctly handles colons in default values
|
|
300
|
+
- Example: `${URL:https://example.com:8080}` works correctly
|
|
301
|
+
- Always quote URLs to prevent YAML parsing issues: `'${URL:https://example.com:8080}'`
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Example configuration file demonstrating environment variable support
|
|
2
|
+
# Copy this to your project and rename to config.yaml
|
|
3
|
+
|
|
4
|
+
# Site Configuration
|
|
5
|
+
# Use environment variables for deployment-specific values
|
|
6
|
+
site:
|
|
7
|
+
name: '${SITE_NAME:My Website}'
|
|
8
|
+
site: '${SITE_URL:https://example.com}'
|
|
9
|
+
base: '${BASE_PATH:/}'
|
|
10
|
+
trailingSlash: false
|
|
11
|
+
|
|
12
|
+
# Google Site Verification (optional)
|
|
13
|
+
googleSiteVerificationId: '${GOOGLE_SITE_VERIFICATION:}'
|
|
14
|
+
|
|
15
|
+
# SEO Metadata
|
|
16
|
+
# Environment variables useful for different environments (dev/staging/prod)
|
|
17
|
+
metadata:
|
|
18
|
+
title:
|
|
19
|
+
default: '${SITE_TITLE:Example Website}'
|
|
20
|
+
template: '%s — ${SITE_TITLE:Example Website}'
|
|
21
|
+
description: '${SITE_DESCRIPTION:This is the default meta description}'
|
|
22
|
+
|
|
23
|
+
robots:
|
|
24
|
+
# Disable indexing in non-production environments
|
|
25
|
+
index: ${ROBOTS_INDEX:true}
|
|
26
|
+
follow: ${ROBOTS_FOLLOW:true}
|
|
27
|
+
|
|
28
|
+
openGraph:
|
|
29
|
+
site_name: '${OG_SITE_NAME:Example}'
|
|
30
|
+
images:
|
|
31
|
+
- url: '~/assets/images/default.png'
|
|
32
|
+
width: 1200
|
|
33
|
+
height: 628
|
|
34
|
+
type: website
|
|
35
|
+
|
|
36
|
+
twitter:
|
|
37
|
+
handle: '${TWITTER_HANDLE:@twitter_user}'
|
|
38
|
+
site: '${TWITTER_SITE:@twitter_user}'
|
|
39
|
+
cardType: summary_large_image
|
|
40
|
+
|
|
41
|
+
# Internationalization
|
|
42
|
+
i18n:
|
|
43
|
+
language: '${LANG:en}'
|
|
44
|
+
textDirection: '${TEXT_DIRECTION:ltr}'
|
|
45
|
+
|
|
46
|
+
# Application Features
|
|
47
|
+
apps:
|
|
48
|
+
blog:
|
|
49
|
+
isEnabled: ${BLOG_ENABLED:true}
|
|
50
|
+
postsPerPage: ${POSTS_PER_PAGE:6}
|
|
51
|
+
|
|
52
|
+
post:
|
|
53
|
+
isEnabled: true
|
|
54
|
+
permalink: '/blog/%slug%'
|
|
55
|
+
robots:
|
|
56
|
+
index: ${BLOG_POST_INDEX:true}
|
|
57
|
+
|
|
58
|
+
list:
|
|
59
|
+
isEnabled: true
|
|
60
|
+
pathname: 'blog'
|
|
61
|
+
robots:
|
|
62
|
+
index: true
|
|
63
|
+
|
|
64
|
+
category:
|
|
65
|
+
isEnabled: true
|
|
66
|
+
pathname: 'category'
|
|
67
|
+
robots:
|
|
68
|
+
index: true
|
|
69
|
+
|
|
70
|
+
tag:
|
|
71
|
+
isEnabled: true
|
|
72
|
+
pathname: 'tag'
|
|
73
|
+
robots:
|
|
74
|
+
index: false
|
|
75
|
+
|
|
76
|
+
isRelatedPostsEnabled: ${RELATED_POSTS_ENABLED:true}
|
|
77
|
+
relatedPostsCount: ${RELATED_POSTS_COUNT:4}
|
|
78
|
+
|
|
79
|
+
# Analytics Configuration
|
|
80
|
+
# Use environment variables for API keys and tracking IDs
|
|
81
|
+
analytics:
|
|
82
|
+
vendors:
|
|
83
|
+
googleAnalytics:
|
|
84
|
+
# Use empty string as default to disable when not set
|
|
85
|
+
id: '${GA_ID:}'
|
|
86
|
+
|
|
87
|
+
# UI Configuration
|
|
88
|
+
ui:
|
|
89
|
+
# Options: 'system' | 'light' | 'dark' | 'light:only' | 'dark:only'
|
|
90
|
+
theme: '${UI_THEME:system}'
|
|
91
|
+
|
|
92
|
+
# Example: Custom API Configuration
|
|
93
|
+
# (Add this section if your application needs external APIs)
|
|
94
|
+
# api:
|
|
95
|
+
# baseUrl: '${API_BASE_URL:https://api.example.com}'
|
|
96
|
+
# timeout: ${API_TIMEOUT:5000}
|
|
97
|
+
# retries: ${API_RETRIES:3}
|
|
98
|
+
# apiKey: '${API_KEY:}'
|
|
99
|
+
|
|
100
|
+
# Example: Feature Flags
|
|
101
|
+
# (Add this section for feature toggles)
|
|
102
|
+
# features:
|
|
103
|
+
# beta: ${FEATURE_BETA:false}
|
|
104
|
+
# experimental: ${FEATURE_EXPERIMENTAL:false}
|
|
105
|
+
# maintenance: ${MAINTENANCE_MODE:false}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import loadConfig from '../loadConfig';
|
|
6
|
+
|
|
7
|
+
describe('loadConfig - Environment Variable Interpolation', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let configPath: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Create a temporary directory for test files
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-config-test-'));
|
|
14
|
+
configPath = path.join(tmpDir, 'test-config.yaml');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// Clean up temporary files
|
|
19
|
+
if (fs.existsSync(tmpDir)) {
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should replace environment variables with their values', async () => {
|
|
25
|
+
process.env.TEST_SITE_URL = 'https://prod.example.com';
|
|
26
|
+
process.env.TEST_API_KEY = 'secret123';
|
|
27
|
+
|
|
28
|
+
const yamlContent = `
|
|
29
|
+
site:
|
|
30
|
+
url: '\${TEST_SITE_URL}'
|
|
31
|
+
api_key: '\${TEST_API_KEY}'
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
35
|
+
const config = await loadConfig(configPath);
|
|
36
|
+
|
|
37
|
+
expect(config).toEqual({
|
|
38
|
+
site: {
|
|
39
|
+
url: 'https://prod.example.com',
|
|
40
|
+
api_key: 'secret123',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
delete process.env.TEST_SITE_URL;
|
|
45
|
+
delete process.env.TEST_API_KEY;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should use default values when environment variables are not set', async () => {
|
|
49
|
+
const yamlContent = `
|
|
50
|
+
site:
|
|
51
|
+
url: '\${UNDEFINED_VAR:https://default.example.com}'
|
|
52
|
+
port: '\${UNDEFINED_PORT:3000}'
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
56
|
+
const config = await loadConfig(configPath);
|
|
57
|
+
|
|
58
|
+
expect(config).toEqual({
|
|
59
|
+
site: {
|
|
60
|
+
url: 'https://default.example.com',
|
|
61
|
+
port: '3000',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should prefer environment variable over default value', async () => {
|
|
67
|
+
process.env.TEST_WITH_DEFAULT = 'from-env';
|
|
68
|
+
|
|
69
|
+
const yamlContent = `
|
|
70
|
+
site:
|
|
71
|
+
value: '\${TEST_WITH_DEFAULT:default-value}'
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
75
|
+
const config = await loadConfig(configPath);
|
|
76
|
+
|
|
77
|
+
expect(config).toEqual({
|
|
78
|
+
site: {
|
|
79
|
+
value: 'from-env',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
delete process.env.TEST_WITH_DEFAULT;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should keep placeholder when env var is not set and no default provided', async () => {
|
|
87
|
+
const yamlContent = `
|
|
88
|
+
site:
|
|
89
|
+
value: '\${UNDEFINED_NO_DEFAULT}'
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
93
|
+
const config = await loadConfig(configPath);
|
|
94
|
+
|
|
95
|
+
expect(config).toEqual({
|
|
96
|
+
site: {
|
|
97
|
+
value: '${UNDEFINED_NO_DEFAULT}',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle multiple environment variables in the same file', async () => {
|
|
103
|
+
process.env.TEST_HOST = 'example.com';
|
|
104
|
+
process.env.TEST_PROTOCOL = 'https';
|
|
105
|
+
process.env.TEST_PORT = '8080';
|
|
106
|
+
|
|
107
|
+
const yamlContent = `
|
|
108
|
+
site:
|
|
109
|
+
protocol: '\${TEST_PROTOCOL:http}'
|
|
110
|
+
host: '\${TEST_HOST:localhost}'
|
|
111
|
+
port: '\${TEST_PORT:3000}'
|
|
112
|
+
base_url: '\${TEST_PROTOCOL}://\${TEST_HOST}:\${TEST_PORT}'
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
116
|
+
const config = await loadConfig(configPath);
|
|
117
|
+
|
|
118
|
+
expect(config).toEqual({
|
|
119
|
+
site: {
|
|
120
|
+
protocol: 'https',
|
|
121
|
+
host: 'example.com',
|
|
122
|
+
port: '8080',
|
|
123
|
+
base_url: 'https://example.com:8080',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
delete process.env.TEST_HOST;
|
|
128
|
+
delete process.env.TEST_PROTOCOL;
|
|
129
|
+
delete process.env.TEST_PORT;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should handle numeric values from environment variables', async () => {
|
|
133
|
+
process.env.TEST_TIMEOUT = '5000';
|
|
134
|
+
process.env.TEST_MAX_RETRIES = '3';
|
|
135
|
+
|
|
136
|
+
const yamlContent = `
|
|
137
|
+
config:
|
|
138
|
+
timeout: \${TEST_TIMEOUT:3000}
|
|
139
|
+
max_retries: \${TEST_MAX_RETRIES:5}
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
143
|
+
const config = await loadConfig(configPath);
|
|
144
|
+
|
|
145
|
+
// YAML will parse unquoted numbers as numbers
|
|
146
|
+
expect(config).toEqual({
|
|
147
|
+
config: {
|
|
148
|
+
timeout: 5000,
|
|
149
|
+
max_retries: 3,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
delete process.env.TEST_TIMEOUT;
|
|
154
|
+
delete process.env.TEST_MAX_RETRIES;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle boolean-like values from environment variables', async () => {
|
|
158
|
+
process.env.TEST_ENABLED = 'true';
|
|
159
|
+
process.env.TEST_DEBUG = 'false';
|
|
160
|
+
|
|
161
|
+
const yamlContent = `
|
|
162
|
+
config:
|
|
163
|
+
enabled: \${TEST_ENABLED:false}
|
|
164
|
+
debug: \${TEST_DEBUG:true}
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
168
|
+
const config = await loadConfig(configPath);
|
|
169
|
+
|
|
170
|
+
// YAML will parse unquoted booleans as booleans
|
|
171
|
+
expect(config).toEqual({
|
|
172
|
+
config: {
|
|
173
|
+
enabled: true,
|
|
174
|
+
debug: false,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
delete process.env.TEST_ENABLED;
|
|
179
|
+
delete process.env.TEST_DEBUG;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should work with object config passed directly', async () => {
|
|
183
|
+
const configObj = {
|
|
184
|
+
site: {
|
|
185
|
+
url: 'https://example.com',
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const config = await loadConfig(configObj);
|
|
190
|
+
|
|
191
|
+
expect(config).toEqual(configObj);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle colons in default values correctly', async () => {
|
|
195
|
+
const yamlContent = `
|
|
196
|
+
site:
|
|
197
|
+
url: '\${UNDEFINED_URL:https://example.com:8080/path}'
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
201
|
+
const config = await loadConfig(configPath);
|
|
202
|
+
|
|
203
|
+
expect(config).toEqual({
|
|
204
|
+
site: {
|
|
205
|
+
url: 'https://example.com:8080/path',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
import yaml from 'js-yaml';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Interpolates environment variables in a string.
|
|
6
|
+
* Supports patterns: ${VAR} and ${VAR:default}
|
|
7
|
+
*
|
|
8
|
+
* @param content - String content to process
|
|
9
|
+
* @returns String with environment variables replaced
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // With env var set: SITE_URL=https://prod.example.com
|
|
13
|
+
* interpolateEnvVars('${SITE_URL:https://example.com}')
|
|
14
|
+
* // Returns: 'https://prod.example.com'
|
|
15
|
+
*
|
|
16
|
+
* // Without env var set:
|
|
17
|
+
* interpolateEnvVars('${SITE_URL:https://example.com}')
|
|
18
|
+
* // Returns: 'https://example.com'
|
|
19
|
+
*/
|
|
20
|
+
const interpolateEnvVars = (content: string): string => {
|
|
21
|
+
return content.replace(
|
|
22
|
+
/\$\{([A-Z_][A-Z0-9_]*):?([^}]*)\}/g,
|
|
23
|
+
(match, varName, defaultValue) => {
|
|
24
|
+
const envValue = process.env[varName];
|
|
25
|
+
|
|
26
|
+
// If env var exists, use it
|
|
27
|
+
if (envValue !== undefined) {
|
|
28
|
+
return envValue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If default value provided, use it
|
|
32
|
+
if (defaultValue !== '') {
|
|
33
|
+
return defaultValue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Otherwise, keep the original placeholder
|
|
37
|
+
return match;
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
4
42
|
const loadConfig = async (configPathOrData: string | object) => {
|
|
5
43
|
if (typeof configPathOrData === 'string') {
|
|
6
|
-
|
|
44
|
+
let content = fs.readFileSync(configPathOrData, 'utf8');
|
|
45
|
+
|
|
7
46
|
if (configPathOrData.endsWith('.yaml') || configPathOrData.endsWith('.yml')) {
|
|
47
|
+
// Interpolate environment variables before parsing YAML
|
|
48
|
+
content = interpolateEnvVars(content);
|
|
8
49
|
return yaml.load(content);
|
|
9
50
|
}
|
|
51
|
+
|
|
10
52
|
return content;
|
|
11
53
|
}
|
|
12
54
|
|