create-velocity-astro 1.4.2 → 1.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-velocity-astro",
3
- "version": "1.4.2",
3
+ "version": "1.5.1",
4
4
  "description": "Create Velocity - A CLI to scaffold production-ready Astro 6 + Tailwind v4 projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,248 +1,154 @@
1
- ---
2
- import type { HTMLAttributes } from 'astro/types';
3
- import { cn } from '@/lib/cn';
4
-
5
- interface Props extends HTMLAttributes<'section'> {
6
- /** Text alignment */
7
- align?: 'center' | 'left' | 'right';
8
- /** Layout mode */
9
- layout?: 'single' | 'split';
10
- /** Split column ratio (only applies when layout="split") */
11
- splitRatio?: '1:1' | '1:2' | '2:1';
12
- /** Background style */
13
- background?: 'default' | 'secondary' | 'invert' | 'gradient' | 'image';
14
- /** Background image URL (only applies when background="image") */
15
- backgroundImage?: string;
16
- /** Vertical padding size */
17
- size?: 'sm' | 'md' | 'lg' | 'xl';
18
- /** Show background grid pattern */
19
- showGrid?: boolean;
20
- /** Show decorative gradient blob */
21
- showBlob?: boolean;
22
- /** Blob position (only applies when showBlob is true) */
23
- blobPosition?: 'left' | 'right' | 'center';
24
- /** Convenience prop for badge text */
25
- badge?: string;
26
- /** Show pulsing dot on badge */
27
- badgeIcon?: boolean;
28
- /** Convenience prop for title text */
29
- title?: string;
30
- /** Text within title to highlight with brand color */
31
- titleHighlight?: string;
32
- /** Convenience prop for description text */
33
- description?: string;
34
- /** Heading element for title */
35
- titleElement?: 'h1' | 'h2';
36
- }
37
-
38
- const {
39
- align = 'left',
40
- layout = 'single',
41
- splitRatio = '1:1',
42
- background = 'default',
43
- backgroundImage,
44
- size = 'lg',
45
- showGrid = false,
46
- showBlob = false,
47
- blobPosition = 'right',
48
- badge,
49
- badgeIcon = false,
50
- title,
51
- titleHighlight,
52
- description,
53
- titleElement = 'h1',
54
- class: className,
55
- ...attrs
56
- } = Astro.props;
57
-
58
- // Check which slots are provided
59
- const hasBadgeSlot = Astro.slots.has('badge');
60
- const hasTitleSlot = Astro.slots.has('title');
61
- const hasDescriptionSlot = Astro.slots.has('description');
62
- const hasActionsSlot = Astro.slots.has('actions');
63
- const hasAsideSlot = Astro.slots.has('aside');
64
- const hasSocialProofSlot = Astro.slots.has('social-proof');
65
-
66
- // Background variants
67
- const backgrounds: Record<NonNullable<Props['background']>, string> = {
68
- default: 'bg-background',
69
- secondary: 'bg-background-secondary',
70
- invert: 'bg-surface-invert text-on-invert',
71
- gradient: 'bg-gradient-to-br from-background via-background to-background-secondary',
72
- image: 'bg-cover bg-center bg-no-repeat',
73
- };
74
-
75
- // Size (padding) variants
76
- const sizes: Record<NonNullable<Props['size']>, string> = {
77
- sm: 'pt-20 pb-12 md:pt-24 md:pb-16',
78
- md: 'pt-24 pb-16 md:pt-32 md:pb-24',
79
- lg: 'pt-32 pb-20 md:pt-40 md:pb-32',
80
- xl: 'pt-40 pb-28 md:pt-48 md:pb-40',
81
- };
82
-
83
- // Alignment variants
84
- const alignments: Record<NonNullable<Props['align']>, string> = {
85
- left: 'text-left items-start',
86
- center: 'text-center items-center',
87
- right: 'text-right items-end',
88
- };
89
-
90
- // Split ratio grid classes
91
- const splitRatios: Record<NonNullable<Props['splitRatio']>, string> = {
92
- '1:1': 'lg:grid-cols-2',
93
- '1:2': 'lg:grid-cols-[1fr_2fr]',
94
- '2:1': 'lg:grid-cols-[2fr_1fr]',
95
- };
96
-
97
- // Blob position variants
98
- const blobPositions: Record<NonNullable<Props['blobPosition']>, string> = {
99
- left: '-left-20 -top-20',
100
- right: '-right-20 -top-20',
101
- center: 'left-1/2 -translate-x-1/2 -top-20',
102
- };
103
-
104
- // Title element
105
- const TitleElement = titleElement;
106
-
107
- // Compute classes
108
- const sectionClasses = cn(
109
- 'relative overflow-hidden',
110
- sizes[size],
111
- backgrounds[background],
112
- className
113
- );
114
-
115
- const contentClasses = cn(
116
- 'z-10 flex flex-col',
117
- alignments[align]
118
- );
119
-
120
- const gridClasses = cn(
121
- 'mx-auto grid max-w-6xl grid-cols-1 items-center gap-12 px-6',
122
- layout === 'split' && splitRatios[splitRatio],
123
- layout === 'split' && 'lg:gap-20'
124
- );
125
-
126
- // Title text processing
127
- function processTitle(text: string, highlight?: string): string {
128
- if (!highlight) return text;
129
- return text.replace(highlight, `<span class="text-brand-500">${highlight}</span>`);
130
- }
131
- ---
132
-
133
- <section class={sectionClasses} style={background === 'image' && backgroundImage ? `background-image: url('${backgroundImage}')` : undefined} {...attrs}>
134
- {/* Background grid pattern */}
135
- {showGrid && (
136
- <div
137
- class="bg-grid-pattern pointer-events-none absolute inset-0 [mask-image:linear-gradient(to_bottom,white,transparent)] opacity-40"
138
- aria-hidden="true"
139
- />
140
- )}
141
-
142
- {/* Background overlay for image background */}
143
- {background === 'image' && (
144
- <div class="absolute inset-0 bg-background/80" aria-hidden="true" />
145
- )}
146
-
147
- <div class={gridClasses}>
148
- {/* Content Column */}
149
- <div class={contentClasses}>
150
- {/* Badge */}
151
- {(hasBadgeSlot || badge) && (
152
- <div class="border-border bg-background mb-6 inline-flex items-center rounded-full border px-3 py-1 shadow-sm">
153
- {badgeIcon && (
154
- <span class="bg-brand-500 mr-2 flex h-2 w-2 animate-pulse rounded-full" aria-hidden="true" />
155
- )}
156
- {hasBadgeSlot ? (
157
- <slot name="badge" />
158
- ) : (
159
- <span class={cn(
160
- 'text-xs font-medium',
161
- background === 'invert' ? 'text-foreground' : 'text-foreground-secondary'
162
- )}>{badge}</span>
163
- )}
164
- </div>
165
- )}
166
-
167
- {/* Title */}
168
- {(hasTitleSlot || title) && (
169
- <TitleElement class={cn(
170
- 'font-display mb-6 text-5xl leading-[1.1] font-bold tracking-tight text-balance md:text-6xl lg:text-7xl',
171
- background === 'invert' ? 'text-on-invert' : 'text-foreground'
172
- )}>
173
- {hasTitleSlot ? (
174
- <slot name="title" />
175
- ) : (
176
- <Fragment set:html={processTitle(title!, titleHighlight)} />
177
- )}
178
- </TitleElement>
179
- )}
180
-
181
- {/* Description */}
182
- {(hasDescriptionSlot || description) && (
183
- <p class={cn(
184
- 'mb-8 max-w-xl text-lg leading-relaxed',
185
- background === 'invert' ? 'text-on-invert/70' : 'text-foreground-muted',
186
- align === 'center' && 'mx-auto'
187
- )}>
188
- {hasDescriptionSlot ? (
189
- <slot name="description" />
190
- ) : (
191
- description
192
- )}
193
- </p>
194
- )}
195
-
196
- {/* Actions */}
197
- {hasActionsSlot && (
198
- <div class={cn(
199
- 'flex w-full flex-col gap-4 sm:w-auto sm:flex-row',
200
- align === 'center' && 'justify-center'
201
- )}>
202
- <slot name="actions" />
203
- </div>
204
- )}
205
-
206
- {/* Social Proof */}
207
- {hasSocialProofSlot && (
208
- <div class={cn(
209
- 'mt-8 flex items-center gap-4 text-sm',
210
- background === 'invert' ? 'text-on-invert/60' : 'text-foreground-subtle'
211
- )}>
212
- <slot name="social-proof" />
213
- </div>
214
- )}
215
- </div>
216
-
217
- {/* Aside Column (for split layout) */}
218
- {layout === 'split' && hasAsideSlot && (
219
- <div class="relative z-10 w-full">
220
- <slot name="aside" />
221
- {/* Decorative blob */}
222
- {showBlob && (
223
- <div
224
- class={cn(
225
- 'bg-brand-200/20 pointer-events-none absolute h-64 w-64 rounded-full blur-3xl',
226
- blobPositions[blobPosition]
227
- )}
228
- aria-hidden="true"
229
- />
230
- )}
231
- </div>
232
- )}
233
-
234
- {/* Decorative blob for single layout */}
235
- {layout === 'single' && showBlob && (
236
- <div
237
- class={cn(
238
- 'bg-brand-200/20 pointer-events-none absolute h-64 w-64 rounded-full blur-3xl',
239
- blobPositions[blobPosition]
240
- )}
241
- aria-hidden="true"
242
- />
243
- )}
244
- </div>
245
-
246
- {/* Default slot for additional content */}
247
- <slot />
248
- </section>
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import { cn } from '@/lib/cn';
4
+
5
+ interface Props extends HTMLAttributes<'section'> {
6
+ /** Layout mode: centered single column or split two-column */
7
+ layout?: 'centered' | 'split';
8
+ /** Vertical padding size */
9
+ size?: 'sm' | 'md' | 'lg' | 'xl';
10
+ /** Show background grid pattern */
11
+ showGrid?: boolean;
12
+ /** Show decorative gradient blob */
13
+ showBlob?: boolean;
14
+ /** Blob position (only applies when showBlob is true) */
15
+ blobPosition?: 'left' | 'right' | 'center';
16
+ }
17
+
18
+ const {
19
+ layout = 'centered',
20
+ size = 'lg',
21
+ showGrid = false,
22
+ showBlob = false,
23
+ blobPosition = 'right',
24
+ class: className,
25
+ ...attrs
26
+ } = Astro.props;
27
+
28
+ // Check which slots are provided
29
+ const hasBadgeSlot = Astro.slots.has('badge');
30
+ const hasTitleSlot = Astro.slots.has('title');
31
+ const hasDescriptionSlot = Astro.slots.has('description');
32
+ const hasActionsSlot = Astro.slots.has('actions');
33
+ const hasAsideSlot = Astro.slots.has('aside');
34
+
35
+ // Size (padding) variants - asymmetric: more top padding for header clearance
36
+ const sizes: Record<NonNullable<Props['size']>, string> = {
37
+ sm: 'pt-[var(--space-page-top-sm)] pb-[var(--space-section-sm)]',
38
+ md: 'pt-[var(--space-page-top)] pb-[var(--space-section-md)]',
39
+ lg: 'pt-[calc(var(--space-page-top)+var(--space-8))] pb-[var(--space-section-lg)]',
40
+ xl: 'pt-[calc(var(--space-page-top)+var(--space-16))] pb-[var(--space-section-xl)]',
41
+ };
42
+
43
+ // Blob position variants
44
+ const blobPositions: Record<NonNullable<Props['blobPosition']>, string> = {
45
+ left: '-left-20 -top-20',
46
+ right: '-right-20 -top-20',
47
+ center: 'left-1/2 -translate-x-1/2 -top-20',
48
+ };
49
+
50
+ // Compute alignment based on layout
51
+ const alignment = layout === 'centered' ? 'text-center items-center' : 'text-left items-start';
52
+
53
+ // Compute classes
54
+ const sectionClasses = cn(
55
+ 'relative overflow-hidden bg-background',
56
+ sizes[size],
57
+ className
58
+ );
59
+
60
+ const contentClasses = cn(
61
+ 'z-10 flex flex-col',
62
+ alignment
63
+ );
64
+
65
+ const gridClasses = cn(
66
+ 'mx-auto grid max-w-6xl grid-cols-1 items-center gap-[var(--space-section-gap)] px-6',
67
+ layout === 'split' && 'lg:grid-cols-2 lg:gap-[var(--space-section-gap)]'
68
+ );
69
+ ---
70
+
71
+ <section class={sectionClasses} {...attrs}>
72
+ {/* Background grid pattern */}
73
+ {showGrid && (
74
+ <div
75
+ class="bg-grid-pattern pointer-events-none absolute inset-0 [mask-image:linear-gradient(to_bottom,white,transparent)] opacity-40"
76
+ aria-hidden="true"
77
+ />
78
+ )}
79
+
80
+ <div class={gridClasses}>
81
+ {/* Content Column */}
82
+ <div class={cn(contentClasses, 'order-1 lg:order-none')}>
83
+ {/* Badge Slot */}
84
+ {hasBadgeSlot && (
85
+ <div class="mb-[var(--space-heading-gap)]">
86
+ <slot name="badge" />
87
+ </div>
88
+ )}
89
+
90
+ {/* Title Slot */}
91
+ {hasTitleSlot && (
92
+ <div class={cn(
93
+ 'mb-[var(--space-heading-gap)]',
94
+ '[&>h1]:font-display [&>h1]:text-5xl [&>h1]:leading-[1.1] [&>h1]:font-bold [&>h1]:tracking-tight [&>h1]:text-balance [&>h1]:text-foreground md:[&>h1]:text-6xl lg:[&>h1]:text-7xl',
95
+ '[&>h2]:font-display [&>h2]:text-4xl [&>h2]:leading-[1.1] [&>h2]:font-bold [&>h2]:tracking-tight [&>h2]:text-balance [&>h2]:text-foreground md:[&>h2]:text-5xl lg:[&>h2]:text-6xl'
96
+ )}>
97
+ <slot name="title" />
98
+ </div>
99
+ )}
100
+
101
+ {/* Description Slot */}
102
+ {hasDescriptionSlot && (
103
+ <div class={cn(
104
+ 'mb-[var(--space-stack-lg)] max-w-xl text-lg leading-relaxed text-foreground-muted',
105
+ '[&>p]:text-lg [&>p]:leading-relaxed [&>p]:text-foreground-muted',
106
+ layout === 'centered' && 'mx-auto'
107
+ )}>
108
+ <slot name="description" />
109
+ </div>
110
+ )}
111
+
112
+ {/* Actions Slot */}
113
+ {hasActionsSlot && (
114
+ <div class={cn(
115
+ 'flex w-full flex-col gap-4 sm:w-auto sm:flex-row',
116
+ layout === 'centered' && 'justify-center'
117
+ )}>
118
+ <slot name="actions" />
119
+ </div>
120
+ )}
121
+
122
+ {/* Default slot for additional content (social proof, etc.) */}
123
+ <slot />
124
+ </div>
125
+
126
+ {/* Aside Column (for split layout) */}
127
+ {layout === 'split' && hasAsideSlot && (
128
+ <div class="relative z-10 w-full order-2 lg:order-none">
129
+ <slot name="aside" />
130
+ {/* Decorative blob for split layout */}
131
+ {showBlob && (
132
+ <div
133
+ class={cn(
134
+ 'bg-brand-200/20 pointer-events-none absolute h-64 w-64 rounded-full blur-3xl',
135
+ blobPositions[blobPosition]
136
+ )}
137
+ aria-hidden="true"
138
+ />
139
+ )}
140
+ </div>
141
+ )}
142
+
143
+ {/* Decorative blob for centered layout */}
144
+ {layout === 'centered' && showBlob && (
145
+ <div
146
+ class={cn(
147
+ 'bg-brand-200/20 pointer-events-none absolute h-64 w-64 rounded-full blur-3xl',
148
+ blobPositions[blobPosition]
149
+ )}
150
+ aria-hidden="true"
151
+ />
152
+ )}
153
+ </div>
154
+ </section>
@@ -32,18 +32,6 @@ interface Props extends HTMLAttributes<'section'> {
32
32
  size?: 'sm' | 'md' | 'lg' | 'xl';
33
33
  /** Max width of content */
34
34
  maxWidth?: 'sm' | 'md' | 'lg' | 'xl';
35
- /** Show logo */
36
- showLogo?: boolean;
37
- /** Logo size */
38
- logoSize?: 'md' | 'lg' | 'xl' | '2xl';
39
- /** Heading text (alternative to slot) */
40
- heading?: string;
41
- /** Text to highlight in brand color (within heading) */
42
- headingHighlight?: string;
43
- /** Description text (alternative to slot) */
44
- description?: string;
45
- /** Content alignment */
46
- align?: 'center' | 'left';
47
35
  /** Show copy command button */
48
36
  showCopyCommand?: boolean;
49
37
  /** Command to copy (e.g., npm create velocity@latest) */
@@ -55,12 +43,6 @@ const {
55
43
  variant = 'default',
56
44
  size = 'lg',
57
45
  maxWidth = 'lg',
58
- showLogo = true,
59
- logoSize = '2xl',
60
- heading,
61
- headingHighlight,
62
- description,
63
- align = 'center',
64
46
  showCopyCommand = true,
65
47
  command,
66
48
  id = 'cta',
@@ -95,10 +77,10 @@ const highlightStyles = {
95
77
  };
96
78
 
97
79
  const sizes = {
98
- sm: 'py-16 md:py-20',
99
- md: 'py-20 md:py-24',
100
- lg: 'py-24 md:py-32',
101
- xl: 'py-32 md:py-40',
80
+ sm: 'py-[var(--space-section-sm)]',
81
+ md: 'py-[var(--space-section-md)]',
82
+ lg: 'py-[var(--space-section-lg)]',
83
+ xl: 'py-[var(--space-section-xl)]',
102
84
  };
103
85
 
104
86
  const maxWidths = {
@@ -108,15 +90,10 @@ const maxWidths = {
108
90
  xl: 'max-w-4xl',
109
91
  };
110
92
 
111
- const alignStyles = {
112
- center: 'text-center mx-auto',
113
- left: 'text-left',
114
- };
115
-
116
93
  // Use translations for default content
117
- const displayHeading = heading || t('cta.title');
118
- const displayHighlight = headingHighlight || t('cta.titleHighlight');
119
- const displayDescription = description || t('cta.description');
94
+ const displayHeading = t('cta.title');
95
+ const displayHighlight = t('cta.titleHighlight');
96
+ const displayDescription = t('cta.description');
120
97
  const displayCommand = command || t('cta.command');
121
98
  const copiedText = t('common.copied');
122
99
  const docsLabel = t('cta.docs');
@@ -127,6 +104,7 @@ function processHeading(text: string, highlight?: string): string {
127
104
  return text.replace(highlight, `<span class="${highlightStyles[variant]}">${highlight}</span>`);
128
105
  }
129
106
 
107
+ const hasLogoSlot = Astro.slots.has('logo');
130
108
  const hasHeadingSlot = Astro.slots.has('heading');
131
109
  const hasDescriptionSlot = Astro.slots.has('description');
132
110
  const hasActionsSlot = Astro.slots.has('actions');
@@ -141,54 +119,61 @@ const buttonId = `copy-command-${Math.random().toString(36).slice(2, 9)}`;
141
119
  {...attrs}
142
120
  >
143
121
  <div class="mx-auto max-w-6xl px-6">
144
- <div class={cn(maxWidths[maxWidth], alignStyles[align])}>
145
- {showLogo && (
122
+ <div class={cn(maxWidths[maxWidth], 'text-center mx-auto')}>
123
+ {/* Logo Slot */}
124
+ {hasLogoSlot ? (
125
+ <div class="mb-[var(--space-stack-lg)]">
126
+ <slot name="logo" />
127
+ </div>
128
+ ) : (
146
129
  <Logo
147
- size={logoSize}
130
+ size="2xl"
148
131
  forceDark={isInvert}
149
- class={cn('mb-8', align === 'center' && 'mx-auto')}
132
+ class="mb-[var(--space-stack-lg)] mx-auto"
150
133
  />
151
134
  )}
152
135
 
136
+ {/* Heading Slot */}
153
137
  {hasHeadingSlot ? (
154
- <h2 class={cn(
155
- 'font-display text-3xl md:text-4xl lg:text-5xl font-bold text-balance mb-4 md:mb-6',
156
- headingStyles[variant]
138
+ <div class={cn(
139
+ 'mb-[var(--space-heading-gap)]',
140
+ '[&>h2]:font-display [&>h2]:text-3xl md:[&>h2]:text-4xl lg:[&>h2]:text-5xl [&>h2]:font-bold [&>h2]:text-balance',
141
+ isInvert ? '[&>h2]:text-on-invert' : '[&>h2]:text-foreground'
157
142
  )}>
158
143
  <slot name="heading" />
159
- </h2>
144
+ </div>
160
145
  ) : (
161
146
  <h2
162
147
  class={cn(
163
- 'font-display text-3xl md:text-4xl lg:text-5xl font-bold text-balance mb-4 md:mb-6',
148
+ 'font-display text-3xl md:text-4xl lg:text-5xl font-bold text-balance mb-[var(--space-heading-gap)]',
164
149
  headingStyles[variant]
165
150
  )}
166
151
  set:html={processHeading(displayHeading, displayHighlight)}
167
152
  />
168
153
  )}
169
154
 
155
+ {/* Description Slot */}
170
156
  {hasDescriptionSlot ? (
171
- <p class={cn('text-lg md:text-xl mb-8 md:mb-10', descriptionStyles[variant])}>
157
+ <div class={cn(
158
+ 'text-lg md:text-xl mb-[var(--space-stack-lg)]',
159
+ '[&>p]:text-lg md:[&>p]:text-xl',
160
+ isInvert ? 'text-on-invert-secondary [&>p]:text-on-invert-secondary' : 'text-foreground-muted [&>p]:text-foreground-muted'
161
+ )}>
172
162
  <slot name="description" />
173
- </p>
163
+ </div>
174
164
  ) : (
175
- <p class={cn('text-lg md:text-xl mb-8 md:mb-10', descriptionStyles[variant])}>
165
+ <p class={cn('text-lg md:text-xl mb-[var(--space-stack-lg)]', descriptionStyles[variant])}>
176
166
  {displayDescription}
177
167
  </p>
178
168
  )}
179
169
 
170
+ {/* Actions Slot */}
180
171
  {hasActionsSlot ? (
181
- <div class={cn(
182
- 'flex flex-col gap-4 sm:flex-row',
183
- align === 'center' && 'items-center justify-center'
184
- )}>
172
+ <div class="flex flex-col gap-4 sm:flex-row items-center justify-center">
185
173
  <slot name="actions" />
186
174
  </div>
187
175
  ) : showCopyCommand && (
188
- <div class={cn(
189
- 'flex flex-col gap-4 sm:flex-row',
190
- align === 'center' && 'items-center justify-center'
191
- )}>
176
+ <div class="flex flex-col gap-4 sm:flex-row items-center justify-center">
192
177
  <button
193
178
  type="button"
194
179
  id={buttonId}
@@ -100,7 +100,7 @@ const t = useTranslations(locale);
100
100
  // Get navigation items from i18n routes or custom nav
101
101
  const defaultNav = getNavRoutes(locale);
102
102
  const navItems: NavItem[] = nav || defaultNav.map(route => ({
103
- label: route.label,
103
+ label: t(route.label as any) || route.label,
104
104
  href: route.path,
105
105
  }));
106
106
 
@@ -111,7 +111,7 @@ const isInvert = colorScheme === 'invert';
111
111
  // Get navigation items from i18n routes or custom nav
112
112
  const defaultNav = getNavRoutes(locale);
113
113
  const navItems: NavItem[] = nav || [...extraNav, ...defaultNav.map(route => ({
114
- label: route.label,
114
+ label: t(route.label as any) || route.label,
115
115
  href: route.path,
116
116
  }))];
117
117
 
@@ -241,29 +241,32 @@ export type NavRoute = {
241
241
  routeId: RouteId;
242
242
  label: string;
243
243
  order: number;
244
+ path: string;
244
245
  };
245
246
 
246
247
  /**
247
248
  * Get routes that should appear in navigation, sorted by order
248
249
  *
249
- * @returns Array of navigation routes with their translation keys
250
+ * @param locale - The locale to get paths for (defaults to defaultLocale)
251
+ * @returns Array of navigation routes with their translation keys and localized paths
250
252
  *
251
253
  * @example
252
- * const navRoutes = getNavRoutes();
254
+ * const navRoutes = getNavRoutes('en');
253
255
  * // → [
254
- * // { routeId: 'components', label: 'nav.components', order: 1 },
255
- * // { routeId: 'blog', label: 'nav.blog', order: 2 },
256
- * // { routeId: 'about', label: 'nav.about', order: 3 },
257
- * // { routeId: 'contact', label: 'nav.contact', order: 4 },
256
+ * // { routeId: 'components', label: 'nav.components', order: 1, path: '/components' },
257
+ * // { routeId: 'blog', label: 'nav.blog', order: 2, path: '/blog' },
258
+ * // { routeId: 'about', label: 'nav.about', order: 3, path: '/about' },
259
+ * // { routeId: 'contact', label: 'nav.contact', order: 4, path: '/contact' },
258
260
  * // ]
259
261
  */
260
- export function getNavRoutes(): NavRoute[] {
262
+ export function getNavRoutes(locale: Locale = defaultLocale): NavRoute[] {
261
263
  return Object.entries(routes)
262
264
  .filter(([_, route]) => route.nav?.show === true)
263
265
  .map(([routeId, route]) => ({
264
266
  routeId: routeId as RouteId,
265
267
  label: route.nav!.label,
266
268
  order: route.nav!.order,
269
+ path: getLocalizedPath(routeId as RouteId, locale),
267
270
  }))
268
271
  .sort((a, b) => a.order - b.order);
269
272
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import PageLayout from '@/layouts/PageLayout.astro';
7
7
  import Icon from '@/components/ui/Icon.astro';
8
+ import Badge from '@/components/ui/Badge.astro';
8
9
  import Card from '@/components/ui/Card.astro';
9
10
  import { Hero } from '@/components/hero';
10
11
  import { locales, defaultLocale, isValidLocale, type Locale } from '@/i18n/config';
@@ -42,21 +43,16 @@ const t = useTranslations(locale);
42
43
  lang={locale}
43
44
  routeId="about"
44
45
  >
45
- <Hero
46
- layout="single"
47
- align="center"
48
- size="sm"
49
- showGrid
50
- badge={t('about.hero.badge')}
51
- badgeIcon
52
- titleElement="h1"
53
- >
54
- <Fragment slot="title">
46
+ <Hero layout="centered" size="sm" showGrid>
47
+ <Badge slot="badge" pill pulse>{t('about.hero.badge')}</Badge>
48
+
49
+ <h1 slot="title">
55
50
  {t('about.hero.title')} <span class="text-brand-500">{t('about.hero.titleHighlight')}</span>
56
- </Fragment>
57
- <Fragment slot="description">
51
+ </h1>
52
+
53
+ <p slot="description">
58
54
  {t('about.hero.description')}
59
- </Fragment>
55
+ </p>
60
56
  </Hero>
61
57
 
62
58
  <!-- Mission Section -->
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import PageLayout from '@/layouts/PageLayout.astro';
7
7
  import Icon from '@/components/ui/Icon.astro';
8
+ import Badge from '@/components/ui/Badge.astro';
8
9
  import Button from '@/components/ui/Button.astro';
9
10
  import Input from '@/components/ui/Input.astro';
10
11
  import Textarea from '@/components/ui/Textarea.astro';
@@ -46,21 +47,16 @@ const t = useTranslations(locale);
46
47
  lang={locale}
47
48
  routeId="contact"
48
49
  >
49
- <Hero
50
- layout="single"
51
- align="center"
52
- size="sm"
53
- showGrid
54
- badge={t('contact.hero.badge')}
55
- badgeIcon
56
- titleElement="h1"
57
- >
58
- <Fragment slot="title">
50
+ <Hero layout="centered" size="sm" showGrid>
51
+ <Badge slot="badge" pill pulse>{t('contact.hero.badge')}</Badge>
52
+
53
+ <h1 slot="title">
59
54
  {t('contact.hero.title')} <span class="text-brand-500">{t('contact.hero.titleHighlight')}</span>
60
- </Fragment>
61
- <Fragment slot="description">
55
+ </h1>
56
+
57
+ <p slot="description">
62
58
  {t('contact.hero.description')}
63
- </Fragment>
59
+ </p>
64
60
  </Hero>
65
61
 
66
62
  <!-- Contact Form Section -->
@@ -6,6 +6,7 @@
6
6
  import LandingLayout from '@/layouts/LandingLayout.astro';
7
7
  import { Hero } from '@/components/hero';
8
8
  import Button from '@/components/ui/Button.astro';
9
+ import Badge from '@/components/ui/Badge.astro';
9
10
  import Icon from '@/components/ui/Icon.astro';
10
11
  import SocialProof from '@/components/ui/SocialProof.astro';
11
12
  import TerminalDemo from '@/components/ui/TerminalDemo.tsx';
@@ -44,23 +45,16 @@ const ctaLink = '#cta';
44
45
  lang={locale}
45
46
  routeId="home"
46
47
  >
47
- <Hero
48
- layout="split"
49
- align="left"
50
- size="lg"
51
- showGrid
52
- showBlob
53
- blobPosition="right"
54
- badge={t('hero.badge')}
55
- badgeIcon
56
- >
57
- <Fragment slot="title">
48
+ <Hero layout="split" size="lg" showGrid showBlob blobPosition="right">
49
+ <Badge slot="badge" pill pulse>{t('hero.badge')}</Badge>
50
+
51
+ <h1 slot="title">
58
52
  {t('hero.title')} <span class="text-brand-500">{t('hero.titleHighlight')}</span>
59
- </Fragment>
53
+ </h1>
60
54
 
61
- <Fragment slot="description">
55
+ <p slot="description">
62
56
  {t('hero.description')}
63
- </Fragment>
57
+ </p>
64
58
 
65
59
  <Fragment slot="actions">
66
60
  <Button size="lg" href={ctaLink}>
@@ -81,7 +75,7 @@ const ctaLink = '#cta';
81
75
  <TerminalDemo slot="aside" client:load />
82
76
 
83
77
  <SocialProof
84
- slot="social-proof"
78
+ class="mt-[var(--space-stack-lg)]"
85
79
  text={t('hero.socialProof')}
86
80
  avatars={[
87
81
  'https://i.pravatar.cc/150?img=12',
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import PageLayout from '@/layouts/PageLayout.astro';
7
7
  import Icon from '@/components/ui/Icon.astro';
8
+ import Badge from '@/components/ui/Badge.astro';
8
9
  import Card from '@/components/ui/Card.astro';
9
10
  import { Hero } from '@/components/hero';
10
11
  import { defaultLocale } from '@/i18n/config';
@@ -20,21 +21,16 @@ const t = useTranslations(locale);
20
21
  lang={locale}
21
22
  routeId="about"
22
23
  >
23
- <Hero
24
- layout="single"
25
- align="center"
26
- size="sm"
27
- showGrid
28
- badge={t('about.hero.badge')}
29
- badgeIcon
30
- titleElement="h1"
31
- >
32
- <Fragment slot="title">
24
+ <Hero layout="centered" size="sm" showGrid>
25
+ <Badge slot="badge" pill pulse>{t('about.hero.badge')}</Badge>
26
+
27
+ <h1 slot="title">
33
28
  {t('about.hero.title')} <span class="text-brand-500">{t('about.hero.titleHighlight')}</span>
34
- </Fragment>
35
- <Fragment slot="description">
29
+ </h1>
30
+
31
+ <p slot="description">
36
32
  {t('about.hero.description')}
37
- </Fragment>
33
+ </p>
38
34
  </Hero>
39
35
 
40
36
  <!-- Mission Section -->
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import PageLayout from '@/layouts/PageLayout.astro';
7
7
  import Icon from '@/components/ui/Icon.astro';
8
+ import Badge from '@/components/ui/Badge.astro';
8
9
  import Button from '@/components/ui/Button.astro';
9
10
  import Input from '@/components/ui/Input.astro';
10
11
  import Textarea from '@/components/ui/Textarea.astro';
@@ -24,21 +25,16 @@ const t = useTranslations(locale);
24
25
  lang={locale}
25
26
  routeId="contact"
26
27
  >
27
- <Hero
28
- layout="single"
29
- align="center"
30
- size="sm"
31
- showGrid
32
- badge={t('contact.hero.badge')}
33
- badgeIcon
34
- titleElement="h1"
35
- >
36
- <Fragment slot="title">
28
+ <Hero layout="centered" size="sm" showGrid>
29
+ <Badge slot="badge" pill pulse>{t('contact.hero.badge')}</Badge>
30
+
31
+ <h1 slot="title">
37
32
  {t('contact.hero.title')} <span class="text-brand-500">{t('contact.hero.titleHighlight')}</span>
38
- </Fragment>
39
- <Fragment slot="description">
33
+ </h1>
34
+
35
+ <p slot="description">
40
36
  {t('contact.hero.description')}
41
- </Fragment>
37
+ </p>
42
38
  </Hero>
43
39
 
44
40
  <!-- Contact Form Section -->
@@ -6,6 +6,7 @@
6
6
  import LandingLayout from '@/layouts/LandingLayout.astro';
7
7
  import { Hero } from '@/components/hero';
8
8
  import Button from '@/components/ui/Button.astro';
9
+ import Badge from '@/components/ui/Badge.astro';
9
10
  import Icon from '@/components/ui/Icon.astro';
10
11
  import SocialProof from '@/components/ui/SocialProof.astro';
11
12
  import TerminalDemo from '@/components/ui/TerminalDemo.tsx';
@@ -29,23 +30,16 @@ const ctaLink = '#cta';
29
30
  lang={locale}
30
31
  routeId="home"
31
32
  >
32
- <Hero
33
- layout="split"
34
- align="left"
35
- size="lg"
36
- showGrid
37
- showBlob
38
- blobPosition="right"
39
- badge={t('hero.badge')}
40
- badgeIcon
41
- >
42
- <Fragment slot="title">
33
+ <Hero layout="split" size="lg" showGrid showBlob blobPosition="right">
34
+ <Badge slot="badge" pill pulse>{t('hero.badge')}</Badge>
35
+
36
+ <h1 slot="title">
43
37
  {t('hero.title')} <span class="text-brand-500">{t('hero.titleHighlight')}</span>
44
- </Fragment>
38
+ </h1>
45
39
 
46
- <Fragment slot="description">
40
+ <p slot="description">
47
41
  {t('hero.description')}
48
- </Fragment>
42
+ </p>
49
43
 
50
44
  <Fragment slot="actions">
51
45
  <Button size="lg" href={ctaLink}>
@@ -66,7 +60,7 @@ const ctaLink = '#cta';
66
60
  <TerminalDemo slot="aside" client:load />
67
61
 
68
62
  <SocialProof
69
- slot="social-proof"
63
+ class="mt-[var(--space-stack-lg)]"
70
64
  text={t('hero.socialProof')}
71
65
  avatars={[
72
66
  'https://i.pravatar.cc/150?img=12',