astro-component-kit 0.3.0 → 0.4.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.
Files changed (115) hide show
  1. package/README.md +159 -176
  2. package/package.json +2 -2
  3. package/src/components/index.ts +16 -202
  4. package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
  5. package/src/components/lib/buttons/RippleButton.astro +199 -0
  6. package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
  7. package/src/components/lib/buttons/index.ts +4 -0
  8. package/src/components/lib/cards/GithubRepoCard.astro +176 -0
  9. package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
  10. package/src/components/lib/cards/StatsTrendCard.astro +149 -0
  11. package/src/components/lib/cards/index.ts +3 -0
  12. package/src/components/lib/charts/ActivityRings.astro +105 -0
  13. package/src/components/lib/charts/BoxPlotChart.astro +109 -0
  14. package/src/components/lib/charts/BubbleChart.astro +101 -0
  15. package/src/components/lib/charts/BulletChart.astro +160 -0
  16. package/src/components/lib/charts/BumpChart.astro +92 -0
  17. package/src/components/lib/charts/CandlestickChart.astro +105 -0
  18. package/src/components/lib/charts/ChartFrame.astro +373 -0
  19. package/src/components/lib/charts/DivergingBarChart.astro +98 -0
  20. package/src/components/lib/charts/DumbbellChart.astro +92 -0
  21. package/src/components/lib/charts/ForecastLineChart.astro +125 -0
  22. package/src/components/lib/charts/FunnelChart.astro +122 -0
  23. package/src/components/lib/charts/GaugeChart.astro +111 -0
  24. package/src/components/lib/charts/GoalProgressChart.astro +97 -0
  25. package/src/components/lib/charts/GroupedBarChart.astro +125 -0
  26. package/src/components/lib/charts/HeatmapChart.astro +131 -0
  27. package/src/components/lib/charts/HistogramChart.astro +111 -0
  28. package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
  29. package/src/components/lib/charts/KpiStatTile.astro +116 -0
  30. package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
  31. package/src/components/lib/charts/LineChart.astro +160 -0
  32. package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
  33. package/src/components/lib/charts/LollipopChart.astro +100 -0
  34. package/src/components/lib/charts/MeterGauge.astro +122 -0
  35. package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
  36. package/src/components/lib/charts/PieChart.astro +135 -0
  37. package/src/components/lib/charts/PolarAreaChart.astro +85 -0
  38. package/src/components/lib/charts/PopulationPyramid.astro +122 -0
  39. package/src/components/lib/charts/PunchCardChart.astro +89 -0
  40. package/src/components/lib/charts/RadarChart.astro +94 -0
  41. package/src/components/lib/charts/RadarMetricChart.astro +168 -0
  42. package/src/components/lib/charts/RadialBarChart.astro +124 -0
  43. package/src/components/lib/charts/RangeAreaChart.astro +113 -0
  44. package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
  45. package/src/components/lib/charts/SankeyChart.astro +128 -0
  46. package/src/components/lib/charts/ScatterPlot.astro +139 -0
  47. package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
  48. package/src/components/lib/charts/SlopeChart.astro +105 -0
  49. package/src/components/lib/charts/SparklineChart.astro +87 -0
  50. package/src/components/lib/charts/StackedAreaChart.astro +130 -0
  51. package/src/components/lib/charts/StackedBarChart.astro +136 -0
  52. package/src/components/lib/charts/SunburstChart.astro +103 -0
  53. package/src/components/lib/charts/TreemapChart.astro +94 -0
  54. package/src/components/lib/charts/WaffleChart.astro +121 -0
  55. package/src/components/lib/charts/WaterfallChart.astro +118 -0
  56. package/src/components/lib/charts/chart-tooltip.ts +75 -0
  57. package/src/components/lib/charts/chart-utils.ts +271 -0
  58. package/src/components/lib/charts/index.ts +46 -0
  59. package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
  60. package/src/components/lib/data/MetricComparisonRow.astro +109 -0
  61. package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
  62. package/src/components/lib/data/index.ts +3 -0
  63. package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
  64. package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
  65. package/src/components/lib/feedback/index.ts +2 -0
  66. package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
  67. package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
  68. package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
  69. package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
  70. package/src/components/lib/forms/index.ts +5 -0
  71. package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
  72. package/src/components/lib/layout/HolyGrailGrid.astro +125 -0
  73. package/src/components/lib/layout/index.ts +3 -1
  74. package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
  75. package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
  76. package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
  77. package/src/components/lib/navigation/index.ts +3 -0
  78. package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
  79. package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
  80. package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
  81. package/src/components/lib/overlays/index.ts +3 -0
  82. package/src/components/lib/sections/BlogGridSection.astro +131 -0
  83. package/src/components/lib/sections/CareersOpenings.astro +139 -0
  84. package/src/components/lib/sections/ChangelogSection.astro +144 -0
  85. package/src/components/lib/sections/ContactSplitMap.astro +139 -0
  86. package/src/components/lib/sections/CtaNewsletterSection.astro +163 -0
  87. package/src/components/lib/sections/CtaSplitImage.astro +147 -0
  88. package/src/components/lib/sections/FaqTwoColumn.astro +119 -0
  89. package/src/components/lib/sections/FeatureAlternatingRows.astro +190 -0
  90. package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
  91. package/src/components/lib/sections/FeatureTabsShowcase.astro +180 -0
  92. package/src/components/lib/sections/FooterMegaSection.astro +155 -0
  93. package/src/components/lib/sections/HeroDashboardPreview.astro +180 -0
  94. package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
  95. package/src/components/lib/sections/HeroShowcaseSplit.astro +304 -0
  96. package/src/components/lib/sections/HeroTerminal.astro +251 -0
  97. package/src/components/lib/sections/HeroWaitlist.astro +229 -0
  98. package/src/components/lib/sections/HowItWorksSteps.astro +178 -0
  99. package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
  100. package/src/components/lib/sections/LogoCloudSection.astro +96 -0
  101. package/src/components/lib/sections/LogoWallGrid.astro +91 -0
  102. package/src/components/lib/sections/PricingSinglePlan.astro +157 -0
  103. package/src/components/lib/sections/PricingToggleCards.astro +171 -0
  104. package/src/components/lib/sections/StatsCounterBand.astro +122 -0
  105. package/src/components/lib/sections/StatsDashboardSection.astro +83 -0
  106. package/src/components/lib/sections/TestimonialCarousel.astro +165 -0
  107. package/src/components/lib/sections/TestimonialFeatured.astro +120 -0
  108. package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
  109. package/src/components/lib/sections/index.ts +29 -0
  110. package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
  111. package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
  112. package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
  113. package/src/components/lib/typography/HackerDecodeText.astro +85 -0
  114. package/src/components/lib/typography/index.ts +4 -0
  115. package/src/styles/global.css +18 -1
@@ -0,0 +1,203 @@
1
+ ---
2
+ /**
3
+ * FloatingFeedbackWidget — Fixed corner floating pill that expands into an interactive feedback survey card.
4
+ *
5
+ * @param {string} title - Widget title. Default is 'Was this helpful?'.
6
+ * @param {string} position - Screen anchor ('bottom-right' | 'bottom-left'). Default is 'bottom-right'.
7
+ */
8
+ interface Props {
9
+ title?: string;
10
+ position?: 'bottom-right' | 'bottom-left';
11
+ }
12
+
13
+ const {
14
+ title = 'Send Feedback',
15
+ position = 'bottom-right',
16
+ } = Astro.props;
17
+ ---
18
+
19
+ <div class:list={['fb-widget', `fb-widget--${position}`]} data-feedback-widget>
20
+ <button type="button" class="fb-widget__trigger" aria-label="Open feedback modal" data-fb-trigger>
21
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
22
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
23
+ </svg>
24
+ <span>Feedback</span>
25
+ </button>
26
+
27
+ <div class="fb-widget__card" data-fb-card hidden>
28
+ <div class="fb-widget__head">
29
+ <h5 class="fb-widget__title">{title}</h5>
30
+ <button type="button" class="fb-widget__close" aria-label="Close" data-fb-close>×</button>
31
+ </div>
32
+
33
+ <div class="fb-widget__emojis">
34
+ <button type="button" class="emoji-btn" aria-label="Sad">🙁</button>
35
+ <button type="button" class="emoji-btn" aria-label="Neutral">😐</button>
36
+ <button type="button" class="emoji-btn emoji-btn--selected" aria-label="Happy">😊</button>
37
+ <button type="button" class="emoji-btn" aria-label="Love it">🤩</button>
38
+ </div>
39
+
40
+ <textarea
41
+ placeholder="Any details to share? (optional)"
42
+ class="fb-widget__textarea"
43
+ rows="2"
44
+ ></textarea>
45
+
46
+ <button type="button" class="fb-widget__send" data-fb-send>
47
+ Send Feedback
48
+ </button>
49
+ </div>
50
+ </div>
51
+
52
+ <style>
53
+ .fb-widget {
54
+ position: fixed;
55
+ z-index: 90;
56
+ font-family: var(--font-sans, system-ui, sans-serif);
57
+ }
58
+
59
+ .fb-widget--bottom-right { bottom: 1.5rem; right: 1.5rem; }
60
+ .fb-widget--bottom-left { bottom: 1.5rem; left: 1.5rem; }
61
+
62
+ .fb-widget__trigger {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 8px;
66
+ padding: 0.6rem 1.1rem;
67
+ background: rgba(14, 14, 18, 0.9);
68
+ backdrop-filter: blur(16px);
69
+ border: 1px solid rgba(255, 255, 255, 0.12);
70
+ border-radius: var(--r-full, 9999px);
71
+ color: var(--c-text-1, #ffffff);
72
+ font-size: 0.82rem;
73
+ font-weight: 700;
74
+ cursor: pointer;
75
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
76
+ transition: transform 0.2s ease, border-color 0.2s ease;
77
+ }
78
+
79
+ .fb-widget__trigger:hover {
80
+ transform: translateY(-2px);
81
+ border-color: rgba(184, 134, 11, 0.4);
82
+ color: var(--c-primary-light, #d4a017);
83
+ }
84
+
85
+ .fb-widget__card {
86
+ position: absolute;
87
+ bottom: calc(100% + 12px);
88
+ right: 0;
89
+ width: 280px;
90
+ padding: 1.2rem;
91
+ background: rgba(12, 12, 16, 0.95);
92
+ backdrop-filter: blur(24px);
93
+ border: 1px solid rgba(255, 255, 255, 0.12);
94
+ border-radius: var(--r-xl, 20px);
95
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 12px;
99
+ }
100
+
101
+ .fb-widget__head {
102
+ display: flex;
103
+ justify-content: space-between;
104
+ align-items: center;
105
+ }
106
+
107
+ .fb-widget__title {
108
+ font-size: 0.88rem;
109
+ font-weight: 800;
110
+ color: var(--c-text-1, #ffffff);
111
+ margin: 0;
112
+ }
113
+
114
+ .fb-widget__close {
115
+ background: transparent;
116
+ border: none;
117
+ color: var(--c-text-3, #737373);
118
+ font-size: 1.2rem;
119
+ line-height: 1;
120
+ cursor: pointer;
121
+ }
122
+
123
+ .fb-widget__emojis {
124
+ display: flex;
125
+ justify-content: space-between;
126
+ gap: 6px;
127
+ }
128
+
129
+ .emoji-btn {
130
+ flex: 1;
131
+ font-size: 1.4rem;
132
+ background: rgba(255, 255, 255, 0.03);
133
+ border: 1px solid rgba(255, 255, 255, 0.08);
134
+ border-radius: var(--r-md, 10px);
135
+ padding: 6px 0;
136
+ cursor: pointer;
137
+ transition: transform 0.2s ease, background 0.2s ease;
138
+ }
139
+
140
+ .emoji-btn:hover,
141
+ .emoji-btn--selected {
142
+ transform: scale(1.15);
143
+ background: rgba(184, 134, 11, 0.2);
144
+ border-color: rgba(184, 134, 11, 0.4);
145
+ }
146
+
147
+ .fb-widget__textarea {
148
+ width: 100%;
149
+ background: rgba(255, 255, 255, 0.03);
150
+ border: 1px solid rgba(255, 255, 255, 0.08);
151
+ border-radius: var(--r-sm, 8px);
152
+ color: var(--c-text-1, #ffffff);
153
+ font-size: 0.8rem;
154
+ padding: 8px;
155
+ resize: none;
156
+ outline: none;
157
+ font-family: inherit;
158
+ }
159
+
160
+ .fb-widget__send {
161
+ padding: 0.6rem;
162
+ background: linear-gradient(135deg, #d4a017, #b8860b);
163
+ border: none;
164
+ border-radius: var(--r-sm, 8px);
165
+ color: #050505;
166
+ font-size: 0.8rem;
167
+ font-weight: 800;
168
+ cursor: pointer;
169
+ transition: opacity 0.2s;
170
+ }
171
+
172
+ .fb-widget__send:hover {
173
+ opacity: 0.9;
174
+ }
175
+ </style>
176
+
177
+ <script>
178
+ function setupFeedbackWidgets() {
179
+ document.querySelectorAll('[data-feedback-widget]').forEach((widget) => {
180
+ const trigger = widget.querySelector('[data-fb-trigger]');
181
+ const card = widget.querySelector('[data-fb-card]') as HTMLElement;
182
+ const close = widget.querySelector('[data-fb-close]');
183
+ const send = widget.querySelector('[data-fb-send]');
184
+ if (!trigger || !card) return;
185
+
186
+ trigger.addEventListener('click', () => {
187
+ card.hidden = !card.hidden;
188
+ });
189
+
190
+ close?.addEventListener('click', () => {
191
+ card.hidden = true;
192
+ });
193
+
194
+ send?.addEventListener('click', () => {
195
+ card.innerHTML = `<p style="text-align:center;font-size:0.85rem;color:#10b981;font-weight:700;margin:1rem 0;">✓ Thank you for your feedback!</p>`;
196
+ setTimeout(() => { card.hidden = true; }, 2000);
197
+ });
198
+ });
199
+ }
200
+
201
+ setupFeedbackWidgets();
202
+ document.addEventListener('astro:page-load', setupFeedbackWidgets);
203
+ </script>
@@ -0,0 +1,172 @@
1
+ ---
2
+ /**
3
+ * ImageCompareSlider — Interactive dual-image split compare slider with draggable separator bar.
4
+ *
5
+ * @param {string} beforeImage - URL of original / before image.
6
+ * @param {string} afterImage - URL of modified / after image.
7
+ * @param {string} beforeLabel - Label tag. Default is 'Before'.
8
+ * @param {string} afterLabel - Label tag. Default is 'After'.
9
+ * @param {number} initialPosition - Starting separator position (0 to 100). Default is 50.
10
+ */
11
+ interface Props {
12
+ beforeImage?: string;
13
+ afterImage?: string;
14
+ beforeLabel?: string;
15
+ afterLabel?: string;
16
+ initialPosition?: number;
17
+ }
18
+
19
+ const {
20
+ beforeImage = 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=800&auto=format&fit=crop&q=80',
21
+ afterImage = 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&auto=format&fit=crop&q=80',
22
+ beforeLabel = 'Before (Wireframe)',
23
+ afterLabel = 'After (Production)',
24
+ initialPosition = 50,
25
+ } = Astro.props;
26
+ ---
27
+
28
+ <div class="img-compare" data-img-compare style={`--pos: ${initialPosition}%;`}>
29
+ <div class="img-compare__layer img-compare__layer--after">
30
+ <img src={afterImage} alt={afterLabel} loading="lazy" />
31
+ <span class="img-compare__tag img-compare__tag--after">{afterLabel}</span>
32
+ </div>
33
+
34
+ <div class="img-compare__layer img-compare__layer--before" data-compare-before>
35
+ <img src={beforeImage} alt={beforeLabel} loading="lazy" />
36
+ <span class="img-compare__tag img-compare__tag--before">{beforeLabel}</span>
37
+ </div>
38
+
39
+ <div class="img-compare__handle" data-compare-handle aria-hidden="true">
40
+ <div class="handle-line"></div>
41
+ <div class="handle-button">
42
+ <span>‹</span>
43
+ <span>›</span>
44
+ </div>
45
+ </div>
46
+
47
+ <input
48
+ type="range"
49
+ min="0"
50
+ max="100"
51
+ value={initialPosition}
52
+ class="img-compare__range"
53
+ aria-label="Drag to compare images"
54
+ data-compare-range
55
+ />
56
+ </div>
57
+
58
+ <style>
59
+ .img-compare {
60
+ position: relative;
61
+ width: 100%;
62
+ max-width: 600px;
63
+ aspect-ratio: 16 / 10;
64
+ overflow: hidden;
65
+ border-radius: var(--r-xl, 20px);
66
+ border: 1px solid rgba(255, 255, 255, 0.1);
67
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
68
+ user-select: none;
69
+ font-family: var(--font-sans, system-ui, sans-serif);
70
+ }
71
+
72
+ .img-compare__layer {
73
+ position: absolute;
74
+ inset: 0;
75
+ width: 100%;
76
+ height: 100%;
77
+ }
78
+
79
+ .img-compare__layer img {
80
+ width: 100%;
81
+ height: 100%;
82
+ object-fit: cover;
83
+ display: block;
84
+ pointer-events: none;
85
+ }
86
+
87
+ .img-compare__layer--before {
88
+ clip-path: polygon(0 0, var(--pos) 0, var(--pos) 100%, 0 100%);
89
+ }
90
+
91
+ .img-compare__tag {
92
+ position: absolute;
93
+ top: 14px;
94
+ padding: 4px 10px;
95
+ background: rgba(0, 0, 0, 0.65);
96
+ backdrop-filter: blur(8px);
97
+ color: #ffffff;
98
+ font-size: 0.72rem;
99
+ font-weight: 700;
100
+ border-radius: var(--r-full, 9999px);
101
+ border: 1px solid rgba(255, 255, 255, 0.15);
102
+ pointer-events: none;
103
+ }
104
+
105
+ .img-compare__tag--before { left: 14px; }
106
+ .img-compare__tag--after { right: 14px; }
107
+
108
+ .img-compare__handle {
109
+ position: absolute;
110
+ top: 0;
111
+ bottom: 0;
112
+ left: var(--pos);
113
+ transform: translateX(-50%);
114
+ pointer-events: none;
115
+ z-index: 5;
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: center;
119
+ justify-content: center;
120
+ }
121
+
122
+ .handle-line {
123
+ width: 2px;
124
+ height: 100%;
125
+ background: #ffffff;
126
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.8);
127
+ }
128
+
129
+ .handle-button {
130
+ position: absolute;
131
+ top: 50%;
132
+ transform: translateY(-50%);
133
+ width: 36px;
134
+ height: 36px;
135
+ border-radius: 50%;
136
+ background: #ffffff;
137
+ color: #050505;
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ gap: 4px;
142
+ font-size: 0.95rem;
143
+ font-weight: 900;
144
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
145
+ }
146
+
147
+ .img-compare__range {
148
+ position: absolute;
149
+ inset: 0;
150
+ width: 100%;
151
+ height: 100%;
152
+ opacity: 0;
153
+ cursor: ew-resize;
154
+ z-index: 10;
155
+ }
156
+ </style>
157
+
158
+ <script>
159
+ function setupImageCompare() {
160
+ document.querySelectorAll('[data-img-compare]').forEach((comp) => {
161
+ const range = comp.querySelector('[data-compare-range]') as HTMLInputElement;
162
+ if (!range) return;
163
+
164
+ range.addEventListener('input', () => {
165
+ (comp as HTMLElement).style.setProperty('--pos', `${range.value}%`);
166
+ });
167
+ });
168
+ }
169
+
170
+ setupImageCompare();
171
+ document.addEventListener('astro:page-load', setupImageCompare);
172
+ </script>
@@ -21,3 +21,6 @@ export { default as SplashIntroOverlay } from './SplashIntroOverlay.astro';
21
21
  export { default as SpotlightIndicator } from './SpotlightIndicator.astro';
22
22
  export { default as ToastStackOverlay } from './ToastStackOverlay.astro';
23
23
  export { default as TooltipActionTray } from './TooltipActionTray.astro';
24
+ export { default as CookiePreferencesDrawer } from './CookiePreferencesDrawer.astro';
25
+ export { default as ImageCompareSlider } from './ImageCompareSlider.astro';
26
+ export { default as FloatingFeedbackWidget } from './FloatingFeedbackWidget.astro';
@@ -0,0 +1,131 @@
1
+ ---
2
+ /**
3
+ * BlogGridSection — Latest articles section with one featured post and a grid of
4
+ * smaller posts. Covers are CSS gradients by default (pass `image` for photos).
5
+ *
6
+ * @param {string} title - Section heading.
7
+ * @param {{label: string, href: string}} viewAll - Link to the full blog.
8
+ * @param {Array<{title: string, excerpt: string, category: string, date: string, readTime: string, author: string, href: string, image?: string}>} posts - Posts (first is featured).
9
+ * @param {string} variant - Card style (cards|minimal|overlay).
10
+ * @param {string} size - Vertical rhythm (sm|md|lg).
11
+ * @param {string} className - Extra CSS class.
12
+ */
13
+ interface Post { title: string; excerpt: string; category: string; date: string; readTime: string; author: string; href: string; image?: string }
14
+ interface Props {
15
+ title?: string;
16
+ viewAll?: { label: string; href: string };
17
+ posts?: Post[];
18
+ variant?: 'cards' | 'minimal' | 'overlay';
19
+ size?: 'sm' | 'md' | 'lg';
20
+ className?: string;
21
+ }
22
+
23
+ const {
24
+ title = 'From the blog',
25
+ viewAll = { label: 'View all posts', href: '#' },
26
+ posts = [
27
+ { title: 'Building zero-JS charts with SVG and Astro', excerpt: 'How we replaced a 200 kB charting library with build-time SVG, without losing tooltips or accessibility.', category: 'Engineering', date: '2026-10-28', readTime: '8 min', author: 'Marcus Lee', href: '#' },
28
+ { title: 'Designing for dark mode first', excerpt: 'Contrast, elevation and color tokens that hold up at night.', category: 'Design', date: '2026-10-21', readTime: '5 min', author: 'Sofía Ruiz', href: '#' },
29
+ { title: 'The accessibility checklist we run on every component', excerpt: 'Focus, keyboard, contrast and screen readers — our exact process.', category: 'Accessibility', date: '2026-10-14', readTime: '6 min', author: 'Priya Natarajan', href: '#' },
30
+ { title: 'Landing page anatomy: 7 sections that convert', excerpt: 'A data-backed look at hero, proof, pricing and FAQ placement.', category: 'Growth', date: '2026-10-07', readTime: '7 min', author: 'Alicia Gómez', href: '#' },
31
+ ],
32
+ variant = 'cards',
33
+ size = 'md',
34
+ className = '',
35
+ } = Astro.props;
36
+
37
+ const id = `bgs-${Math.random().toString(36).slice(2, 8)}`;
38
+ const gradients = [
39
+ 'radial-gradient(circle at 20% 20%, rgba(57,135,229,.65), transparent 55%), radial-gradient(circle at 80% 80%, rgba(144,133,233,.55), transparent 50%), #101018',
40
+ 'radial-gradient(circle at 70% 30%, rgba(212,160,23,.6), transparent 50%), #15120a',
41
+ 'radial-gradient(circle at 30% 70%, rgba(25,158,112,.6), transparent 55%), #0b1411',
42
+ 'radial-gradient(circle at 60% 40%, rgba(213,81,129,.55), transparent 55%), #150b10',
43
+ ];
44
+ const fmtDate = (d: string) => new Date(`${d}T00:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' });
45
+ ---
46
+
47
+ <section class={`bgs bgs--${variant} bgs--${size} ${className}`} aria-labelledby={id}>
48
+ <div class="bgs__inner">
49
+ <header class="bgs__head">
50
+ <h2 id={id} class="bgs__title">{title}</h2>
51
+ {viewAll && (
52
+ <a class="bgs__all" href={viewAll.href}>
53
+ {viewAll.label}
54
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
55
+ </a>
56
+ )}
57
+ </header>
58
+ <ul class="bgs__grid">
59
+ {posts.map((p, i) => (
60
+ <li class:list={['bgs__post', { 'bgs__post--featured': i === 0 }]}>
61
+ <article class="bgs__card">
62
+ <div class="bgs__cover" style={p.image ? undefined : `background:${gradients[i % gradients.length]}`}>
63
+ {p.image && <img src={p.image} alt="" loading="lazy" decoding="async" />}
64
+ </div>
65
+ <div class="bgs__body">
66
+ <p class="bgs__meta">
67
+ <span class="bgs__cat">{p.category}</span>
68
+ <time datetime={p.date}>{fmtDate(p.date)}</time>
69
+ <span aria-hidden="true">·</span>
70
+ <span>{p.readTime} read</span>
71
+ </p>
72
+ <h3 class="bgs__post-title"><a href={p.href}>{p.title}</a></h3>
73
+ <p class="bgs__excerpt">{p.excerpt}</p>
74
+ <p class="bgs__author">By {p.author}</p>
75
+ </div>
76
+ </article>
77
+ </li>
78
+ ))}
79
+ </ul>
80
+ </div>
81
+ </section>
82
+
83
+ <style>
84
+ .bgs {
85
+ --bgs-py: 5.5rem;
86
+ padding: var(--bgs-py) var(--sp-6, 1.5rem);
87
+ background: var(--c-bg, #050505);
88
+ }
89
+ .bgs--sm { --bgs-py: 3.5rem; }
90
+ .bgs--lg { --bgs-py: 7.5rem; }
91
+ .bgs__inner { max-width: 1180px; margin: 0 auto; }
92
+ .bgs__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }
93
+ .bgs__title { margin: 0; font-size: clamp(1.75rem, 4vw, 2.5rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
94
+ .bgs__all { display: inline-flex; align-items: center; gap: 0.4rem; min-height: 44px; font-weight: 600; color: var(--c-primary-light, #d4a017); text-decoration: none; }
95
+ .bgs__all:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; border-radius: 4px; }
96
+ .bgs__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; }
97
+ .bgs__post--featured { grid-column: 1 / -1; }
98
+ .bgs__card { position: relative; height: 100%; display: flex; flex-direction: column; border-radius: var(--r-lg, 20px); overflow: hidden; transition: transform 0.25s ease, border-color 0.25s ease; }
99
+ .bgs--cards .bgs__card { background: rgba(255, 255, 255, 0.025); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.07)); }
100
+ .bgs__card:hover, .bgs__card:focus-within { transform: translateY(-3px); border-color: rgba(255, 255, 255, 0.18); }
101
+ .bgs__post--featured .bgs__card { display: grid; grid-template-columns: 1.2fr 1fr; }
102
+ .bgs__cover { aspect-ratio: 16 / 9; position: relative; }
103
+ .bgs__post--featured .bgs__cover { aspect-ratio: auto; min-height: 280px; }
104
+ .bgs__cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
105
+ .bgs--minimal .bgs__cover { border-radius: var(--r-lg, 20px); }
106
+ .bgs__body { padding: 1.5rem; display: flex; flex-direction: column; gap: 0.6rem; }
107
+ .bgs--minimal .bgs__body { padding: 1.25rem 0 0; }
108
+ .bgs__post--featured .bgs__body { justify-content: center; padding: clamp(1.5rem, 4vw, 2.75rem); }
109
+ .bgs--overlay .bgs__card { min-height: 320px; }
110
+ .bgs--overlay .bgs__cover { position: absolute; inset: 0; aspect-ratio: auto; }
111
+ .bgs--overlay .bgs__cover::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent 70%); }
112
+ .bgs--overlay .bgs__body { position: relative; margin-top: auto; }
113
+ .bgs--overlay .bgs__post--featured .bgs__card { display: flex; }
114
+ .bgs__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0; font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
115
+ .bgs__cat { padding: 0.15rem 0.55rem; border-radius: 999px; background: rgba(255, 255, 255, 0.07); color: var(--c-text-1, #fff); font-weight: 600; font-size: 0.75rem; }
116
+ .bgs__post-title { margin: 0; font-size: 1.1875rem; font-weight: 700; line-height: 1.3; letter-spacing: -0.015em; }
117
+ .bgs__post--featured .bgs__post-title { font-size: clamp(1.5rem, 3vw, 2rem); }
118
+ .bgs__post-title a { color: var(--c-text-1, #fff); text-decoration: none; }
119
+ .bgs__post-title a::after { content: ''; position: absolute; inset: 0; }
120
+ .bgs__post-title a:focus-visible { outline: none; }
121
+ .bgs__card:has(a:focus-visible) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
122
+ .bgs__excerpt { margin: 0; font-size: 0.9375rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); }
123
+ .bgs__author { margin: auto 0 0; padding-top: 0.5rem; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); }
124
+ @media (max-width: 900px) {
125
+ .bgs__grid { grid-template-columns: 1fr 1fr; }
126
+ .bgs__post--featured .bgs__card { grid-template-columns: 1fr; }
127
+ .bgs__post--featured .bgs__cover { min-height: 200px; }
128
+ }
129
+ @media (max-width: 600px) { .bgs__grid { grid-template-columns: 1fr; } }
130
+ @media (prefers-reduced-motion: reduce) { .bgs__card { transition: none; } }
131
+ </style>
@@ -0,0 +1,139 @@
1
+ ---
2
+ /**
3
+ * CareersOpenings — Job listings section grouped by department, with location,
4
+ * type and remote badges, plus perks strip and a general-application CTA.
5
+ *
6
+ * @param {string} title - Section heading.
7
+ * @param {string} subtitle - Section description.
8
+ * @param {Array<{title: string, department: string, location: string, type: string, remote?: boolean, href: string}>} jobs - Open roles.
9
+ * @param {string[]} perks - Short perk labels.
10
+ * @param {{label: string, href: string}} cta - General application link.
11
+ * @param {string} variant - Row style (list|cards|minimal).
12
+ * @param {string} size - Vertical rhythm (sm|md|lg).
13
+ * @param {string} className - Extra CSS class.
14
+ */
15
+ interface Job { title: string; department: string; location: string; type: string; remote?: boolean; href: string }
16
+ interface Props {
17
+ title?: string;
18
+ subtitle?: string;
19
+ jobs?: Job[];
20
+ perks?: string[];
21
+ cta?: { label: string; href: string };
22
+ variant?: 'list' | 'cards' | 'minimal';
23
+ size?: 'sm' | 'md' | 'lg';
24
+ className?: string;
25
+ }
26
+
27
+ const {
28
+ title = 'Join the team',
29
+ subtitle = 'We’re a small, remote-first crew building tools for the open web.',
30
+ jobs = [
31
+ { title: 'Senior Frontend Engineer', department: 'Engineering', location: 'Europe', type: 'Full-time', remote: true, href: '#' },
32
+ { title: 'Design Systems Engineer', department: 'Engineering', location: 'Americas', type: 'Full-time', remote: true, href: '#' },
33
+ { title: 'Product Designer', department: 'Design', location: 'Barcelona', type: 'Full-time', href: '#' },
34
+ { title: 'Developer Advocate', department: 'Marketing', location: 'Anywhere', type: 'Contract', remote: true, href: '#' },
35
+ ],
36
+ perks = ['Remote-first', '4-day work week', 'Learning budget', 'Equity for everyone'],
37
+ cta = { label: 'Send an open application', href: '#' },
38
+ variant = 'list',
39
+ size = 'md',
40
+ className = '',
41
+ } = Astro.props;
42
+
43
+ const id = `co-${Math.random().toString(36).slice(2, 8)}`;
44
+ const departments = [...new Set(jobs.map((j) => j.department))];
45
+ ---
46
+
47
+ <section class={`co co--${variant} co--${size} ${className}`} aria-labelledby={id}>
48
+ <div class="co__inner">
49
+ <header class="co__head">
50
+ <div>
51
+ <h2 id={id} class="co__title">{title}</h2>
52
+ <p class="co__subtitle">{subtitle}</p>
53
+ </div>
54
+ <span class="co__count">{jobs.length} open roles</span>
55
+ </header>
56
+
57
+ <ul class="co__perks" aria-label="Perks">
58
+ {perks.map((p) => <li>{p}</li>)}
59
+ </ul>
60
+
61
+ {departments.map((d) => (
62
+ <div class="co__dept">
63
+ <h3 class="co__dept-title">{d}</h3>
64
+ <ul class="co__jobs">
65
+ {jobs.filter((j) => j.department === d).map((j) => (
66
+ <li>
67
+ <a class="co__job" href={j.href}>
68
+ <span class="co__job-title">{j.title}</span>
69
+ <span class="co__meta">
70
+ <span class="co__tag">
71
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z" /><circle cx="12" cy="10" r="3" /></svg>
72
+ {j.location}
73
+ </span>
74
+ <span class="co__tag">{j.type}</span>
75
+ {j.remote && <span class="co__tag co__tag--remote">Remote</span>}
76
+ </span>
77
+ <svg class="co__arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M7 17 17 7M8 7h9v9" /></svg>
78
+ </a>
79
+ </li>
80
+ ))}
81
+ </ul>
82
+ </div>
83
+ ))}
84
+
85
+ {cta && (
86
+ <p class="co__cta">Don’t see your role? <a href={cta.href}>{cta.label}</a></p>
87
+ )}
88
+ </div>
89
+ </section>
90
+
91
+ <style>
92
+ .co {
93
+ --co-py: 5.5rem;
94
+ padding: var(--co-py) var(--sp-6, 1.5rem);
95
+ background: var(--c-bg, #050505);
96
+ }
97
+ .co--sm { --co-py: 3.5rem; }
98
+ .co--lg { --co-py: 7.5rem; }
99
+ .co__inner { max-width: 960px; margin: 0 auto; }
100
+ .co__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
101
+ .co__title { margin: 0 0 0.5rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
102
+ .co__subtitle { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
103
+ .co__count { padding: 0.35rem 0.85rem; border-radius: 999px; background: rgba(52, 195, 143, 0.12); color: #7ee2b8; font-size: 0.8125rem; font-weight: 650; }
104
+ .co__perks { list-style: none; margin: 0 0 2.5rem; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
105
+ .co__perks li { padding: 0.4rem 0.85rem; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.1); font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
106
+ .co__dept + .co__dept { margin-top: 2rem; }
107
+ .co__dept-title { margin: 0 0 0.75rem; font-size: 0.8125rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-primary-light, #d4a017); }
108
+ .co__jobs { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
109
+ .co--list .co__jobs { gap: 0; border-top: 1px solid rgba(255, 255, 255, 0.07); }
110
+ .co__job {
111
+ display: grid;
112
+ grid-template-columns: 1fr auto 24px;
113
+ align-items: center;
114
+ gap: 1rem;
115
+ min-height: 64px;
116
+ padding: 0.85rem 1rem;
117
+ color: inherit;
118
+ text-decoration: none;
119
+ transition: background 0.2s ease, border-color 0.2s ease;
120
+ }
121
+ .co--list .co__job { border-bottom: 1px solid rgba(255, 255, 255, 0.07); }
122
+ .co--cards .co__job { border-radius: var(--r-md, 14px); background: rgba(255, 255, 255, 0.025); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.07)); }
123
+ .co--minimal .co__job { padding-inline: 0; }
124
+ .co__job:hover { background: rgba(255, 255, 255, 0.035); }
125
+ .co__job:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; border-radius: 10px; }
126
+ .co__job-title { font-size: 1.0625rem; font-weight: 600; color: var(--c-text-1, #fff); }
127
+ .co__meta { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: flex-end; }
128
+ .co__tag { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.2rem 0.6rem; border-radius: 999px; background: rgba(255, 255, 255, 0.05); font-size: 0.75rem; color: var(--c-text-2, #a3a3a3); }
129
+ .co__tag--remote { background: rgba(57, 135, 229, 0.14); color: #a9cdf7; }
130
+ .co__arrow { color: var(--c-text-3, #8a8a8a); transition: transform 0.2s ease, color 0.2s ease; }
131
+ .co__job:hover .co__arrow { transform: translate(2px, -2px); color: var(--c-text-1, #fff); }
132
+ .co__cta { margin: 2.5rem 0 0; text-align: center; color: var(--c-text-2, #a3a3a3); }
133
+ .co__cta a { color: var(--c-primary-light, #d4a017); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
134
+ @media (max-width: 640px) {
135
+ .co__job { grid-template-columns: 1fr 24px; }
136
+ .co__meta { grid-column: 1; grid-row: 2; justify-content: flex-start; }
137
+ .co__arrow { grid-row: 1 / span 2; grid-column: 2; }
138
+ }
139
+ </style>