@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.
@@ -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 = 'text-gray-600 dark:text-gray-400', ariaLabel = 'Breadcrumbs', items = [] } = Astro.props;
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 { Card0Props as Props } from './types';
3
- import { twMerge } from 'tailwind-merge';
2
+ import type {Card0Props as Props} from './types';
3
+ import {twMerge} from 'tailwind-merge';
4
4
 
5
- const { badge, as = 'article', classes = {} } = Astro.props;
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 { container: containerClass = '', badge: badgeClass = 'top-2 left-2' } = classes;
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
- <div class={twMerge('absolute z-10', badgeClass)}>
19
- <slot name="badge">
20
- {badge && <span set:html={badge} />}
21
- </slot>
22
- </div>
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>
@@ -9,5 +9,8 @@ export type Card0Props = {
9
9
  content?: string;
10
10
  ['as']?: HTMLTag;
11
11
  badge?: string;
12
+ badgeTopRight?: string;
13
+ badgeBottomRight?: string;
14
+ badgeBottomLeft?: string;
12
15
  classes?: Record<string, string>;
13
16
  };
@@ -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="cover"
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="cover"
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="cover"
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="cover"
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="cover"
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="cover"
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
- {callToAction ? (
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
  )}
@@ -91,7 +91,6 @@ const {
91
91
  height={500}
92
92
  widths={[400, 768]}
93
93
  sizes="(max-width: 768px) 100vw, 432px"
94
- layout="responsive"
95
94
  {...image}
96
95
  />
97
96
  )}
@@ -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="aspect-square w-full md:h-full group-hover:scale-105 transition duration-300"
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yatoday/astro-ui",
3
3
  "type": "module",
4
- "version": "0.10.2",
4
+ "version": "0.11.0",
5
5
  "scripts": {
6
6
  "prepare": "husky",
7
7
  "pre-commit": "lint-staged",
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
- const content = fs.readFileSync(configPathOrData, 'utf8');
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