keystone-design-bootstrap 1.0.3

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 (182) hide show
  1. package/README.md +179 -0
  2. package/package.json +59 -0
  3. package/src/contexts/ThemeContext.tsx +34 -0
  4. package/src/contexts/index.ts +1 -0
  5. package/src/design_system/elements/IconComponent.tsx +98 -0
  6. package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
  7. package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
  8. package/src/design_system/elements/avatar/avatar.tsx +131 -0
  9. package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
  10. package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
  11. package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
  12. package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
  13. package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
  14. package/src/design_system/elements/avatar/utils.ts +12 -0
  15. package/src/design_system/elements/badges/avatar.tsx +132 -0
  16. package/src/design_system/elements/badges/badge-groups.tsx +176 -0
  17. package/src/design_system/elements/badges/badge-types.ts +266 -0
  18. package/src/design_system/elements/badges/badges.tsx +430 -0
  19. package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
  20. package/src/design_system/elements/button-group/button-group.tsx +106 -0
  21. package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
  22. package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
  23. package/src/design_system/elements/buttons/button-utility.tsx +116 -0
  24. package/src/design_system/elements/buttons/button.aman.tsx +174 -0
  25. package/src/design_system/elements/buttons/button.tsx +271 -0
  26. package/src/design_system/elements/buttons/close-button.tsx +42 -0
  27. package/src/design_system/elements/buttons/round-button.tsx +29 -0
  28. package/src/design_system/elements/buttons/social-button.tsx +148 -0
  29. package/src/design_system/elements/buttons/social-logos.tsx +115 -0
  30. package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
  31. package/src/design_system/elements/carousel/carousel.tsx +308 -0
  32. package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
  33. package/src/design_system/elements/date-picker/calendar.tsx +101 -0
  34. package/src/design_system/elements/date-picker/cell.tsx +106 -0
  35. package/src/design_system/elements/date-picker/date-input.tsx +32 -0
  36. package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
  37. package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
  38. package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
  39. package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
  40. package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
  41. package/src/design_system/elements/form/form.tsx +10 -0
  42. package/src/design_system/elements/form/hook-form.tsx +75 -0
  43. package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
  44. package/src/design_system/elements/index.tsx +158 -0
  45. package/src/design_system/elements/input/hint-text.tsx +33 -0
  46. package/src/design_system/elements/input/input-group.tsx +133 -0
  47. package/src/design_system/elements/input/input.aman.tsx +172 -0
  48. package/src/design_system/elements/input/input.tsx +271 -0
  49. package/src/design_system/elements/input/label.tsx +50 -0
  50. package/src/design_system/elements/label/label.tsx +50 -0
  51. package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
  52. package/src/design_system/elements/map/GoogleMap.tsx +286 -0
  53. package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
  54. package/src/design_system/elements/modals/modal.tsx +41 -0
  55. package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
  56. package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
  57. package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
  58. package/src/design_system/elements/pagination/pagination.tsx +330 -0
  59. package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
  60. package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
  61. package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
  62. package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
  63. package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
  64. package/src/design_system/elements/rating/rating-badge.tsx +144 -0
  65. package/src/design_system/elements/rating/rating-stars.tsx +77 -0
  66. package/src/design_system/elements/select/combobox.tsx +152 -0
  67. package/src/design_system/elements/select/multi-select.tsx +363 -0
  68. package/src/design_system/elements/select/popover.tsx +34 -0
  69. package/src/design_system/elements/select/select-item.tsx +97 -0
  70. package/src/design_system/elements/select/select-native.tsx +69 -0
  71. package/src/design_system/elements/select/select.aman.tsx +75 -0
  72. package/src/design_system/elements/select/select.tsx +146 -0
  73. package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
  74. package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
  75. package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
  76. package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
  77. package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
  78. package/src/design_system/elements/tabs/tabs.tsx +225 -0
  79. package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
  80. package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
  81. package/src/design_system/elements/tags/tags.tsx +176 -0
  82. package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
  83. package/src/design_system/elements/textarea/textarea.tsx +111 -0
  84. package/src/design_system/elements/toggle/toggle.tsx +140 -0
  85. package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
  86. package/src/design_system/hooks/use-breakpoint.ts +37 -0
  87. package/src/design_system/hooks/use-resize-observer.ts +68 -0
  88. package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
  89. package/src/design_system/logo/keystone-logo.tsx +22 -0
  90. package/src/design_system/sections/about-home.aman.tsx +85 -0
  91. package/src/design_system/sections/about-home.tsx +115 -0
  92. package/src/design_system/sections/blog-cards.tsx +848 -0
  93. package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
  94. package/src/design_system/sections/blog-gallery.tsx +204 -0
  95. package/src/design_system/sections/blog-home.aman.tsx +84 -0
  96. package/src/design_system/sections/blog-home.tsx +153 -0
  97. package/src/design_system/sections/blog-post.aman.tsx +74 -0
  98. package/src/design_system/sections/blog-post.tsx +301 -0
  99. package/src/design_system/sections/blog-section.aman.tsx +101 -0
  100. package/src/design_system/sections/blog-section.tsx +179 -0
  101. package/src/design_system/sections/contact-home.tsx +25 -0
  102. package/src/design_system/sections/contact-section.aman.tsx +173 -0
  103. package/src/design_system/sections/contact-section.tsx +143 -0
  104. package/src/design_system/sections/faq-grid.aman.tsx +79 -0
  105. package/src/design_system/sections/faq-grid.tsx +102 -0
  106. package/src/design_system/sections/faq-home.aman.tsx +92 -0
  107. package/src/design_system/sections/faq-home.tsx +134 -0
  108. package/src/design_system/sections/feature-tab.tsx +43 -0
  109. package/src/design_system/sections/feature-text.tsx +284 -0
  110. package/src/design_system/sections/footer-home.aman.tsx +62 -0
  111. package/src/design_system/sections/footer-home.tsx +259 -0
  112. package/src/design_system/sections/generic-header-component.tsx +103 -0
  113. package/src/design_system/sections/header-navigation.aman.tsx +360 -0
  114. package/src/design_system/sections/header-navigation.tsx +334 -0
  115. package/src/design_system/sections/hero-faq.aman.tsx +38 -0
  116. package/src/design_system/sections/hero-faq.tsx +55 -0
  117. package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
  118. package/src/design_system/sections/hero-generic-text.tsx +51 -0
  119. package/src/design_system/sections/hero-home.aman.tsx +84 -0
  120. package/src/design_system/sections/hero-home.tsx +246 -0
  121. package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
  122. package/src/design_system/sections/hero-location-detail.tsx +72 -0
  123. package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
  124. package/src/design_system/sections/hero-service-detail.tsx +51 -0
  125. package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
  126. package/src/design_system/sections/hero-social-media.tsx +35 -0
  127. package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
  128. package/src/design_system/sections/hero-testimonials.tsx +55 -0
  129. package/src/design_system/sections/home-hero-component.tsx +228 -0
  130. package/src/design_system/sections/index.tsx +131 -0
  131. package/src/design_system/sections/job-gallery.aman.tsx +91 -0
  132. package/src/design_system/sections/job-gallery.tsx +183 -0
  133. package/src/design_system/sections/location-details-section.aman.tsx +179 -0
  134. package/src/design_system/sections/location-details-section.tsx +196 -0
  135. package/src/design_system/sections/location-grid.aman.tsx +76 -0
  136. package/src/design_system/sections/location-grid.tsx +123 -0
  137. package/src/design_system/sections/services-grid.aman.tsx +85 -0
  138. package/src/design_system/sections/services-grid.tsx +104 -0
  139. package/src/design_system/sections/services-home.aman.tsx +78 -0
  140. package/src/design_system/sections/services-home.tsx +131 -0
  141. package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
  142. package/src/design_system/sections/social-media-grid.tsx +189 -0
  143. package/src/design_system/sections/statistics-section.aman.tsx +79 -0
  144. package/src/design_system/sections/statistics-section.tsx +97 -0
  145. package/src/design_system/sections/team-grid.aman.tsx +85 -0
  146. package/src/design_system/sections/team-grid.tsx +88 -0
  147. package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
  148. package/src/design_system/sections/testimonials-home.tsx +90 -0
  149. package/src/design_system/sections/values-section.aman.tsx +73 -0
  150. package/src/design_system/sections/values-section.tsx +128 -0
  151. package/src/design_system/utils/icon-mapping.tsx +28 -0
  152. package/src/index.ts +7 -0
  153. package/src/lib/component-registry.ts +53 -0
  154. package/src/lib/hooks/index.ts +8 -0
  155. package/src/lib/hooks/use-breakpoint.ts +37 -0
  156. package/src/lib/hooks/use-clipboard.ts +79 -0
  157. package/src/lib/hooks/use-resize-observer.ts +68 -0
  158. package/src/lib/server-api.ts +115 -0
  159. package/src/styles/style-overrides.aman.css +101 -0
  160. package/src/styles/theme.css +224 -0
  161. package/src/styles/typography.css +430 -0
  162. package/src/themes/index.ts +23 -0
  163. package/src/types/api/blog-post.ts +53 -0
  164. package/src/types/api/company-information.ts +44 -0
  165. package/src/types/api/contact.ts +63 -0
  166. package/src/types/api/faq.ts +37 -0
  167. package/src/types/api/job-posting.ts +34 -0
  168. package/src/types/api/location.ts +36 -0
  169. package/src/types/api/photos.ts +28 -0
  170. package/src/types/api/service.ts +37 -0
  171. package/src/types/api/social-post.ts +28 -0
  172. package/src/types/api/team-member.ts +29 -0
  173. package/src/types/api/testimonial.ts +29 -0
  174. package/src/types/api/website-photos.ts +22 -0
  175. package/src/types/config.ts +21 -0
  176. package/src/types/index.ts +21 -0
  177. package/src/utils/countries.tsx +1351 -0
  178. package/src/utils/cx.ts +25 -0
  179. package/src/utils/gradient-placeholder.ts +59 -0
  180. package/src/utils/is-react-component.ts +33 -0
  181. package/src/utils/markdown-toc.ts +54 -0
  182. package/src/utils/photo-helpers.ts +94 -0
@@ -0,0 +1,848 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps } from "react";
4
+ import { ArrowUpRight } from "@untitledui/icons";
5
+ import { Avatar, BadgeGroup, Badge, Button, PhotoWithFallback } from '../elements';
6
+ import { cx } from '../../utils/cx';
7
+ import { getAvatarUrl } from '../../utils/photo-helpers';
8
+ import type { BlogPost } from '../../types/api/blog-post';
9
+
10
+ // Helper to get first author from blog post
11
+ const getFirstAuthor = (post: BlogPost) => {
12
+ return Array.isArray(post.blog_post_authors) && post.blog_post_authors.length > 0
13
+ ? post.blog_post_authors[0]
14
+ : null;
15
+ };
16
+
17
+ // Helper to format published date
18
+ const formatPublishedDate = (publishedAt?: string) => {
19
+ return publishedAt
20
+ ? new Date(publishedAt).toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' })
21
+ : 'Recent';
22
+ };
23
+
24
+ // Helper to clean excerpt markdown
25
+ const cleanExcerpt = (excerpt?: string) => {
26
+ return excerpt?.replace(/[#*\[\]()]/g, '').trim() || '';
27
+ };
28
+
29
+ // Helper to get first tag from blog post
30
+ const getFirstTag = (post: BlogPost) => {
31
+ return Array.isArray(post.blog_post_tags) && post.blog_post_tags.length > 0
32
+ ? post.blog_post_tags[0]
33
+ : null;
34
+ };
35
+
36
+ // Helper to extract all blog post data for rendering
37
+ const getBlogPostData = (article: BlogPost) => {
38
+ const author = getFirstAuthor(article);
39
+ const authorName = author?.name || 'Author';
40
+ const authorHref = author?.slug ? `/blog/author/${author.slug}` : '/blog';
41
+ const authorAvatarUrl = getAvatarUrl(author?.photo_attachments, author?.id, authorName);
42
+ const tag = getFirstTag(article);
43
+ const tagName = tag?.name || 'Blog';
44
+ const tagHref = tag?.slug ? `/blog/tag/${tag.slug}` : '/blog';
45
+ const href = `/blog/${article.slug}`;
46
+ const title = article.title || 'Untitled Post';
47
+ const publishedAt = formatPublishedDate(article.published_at);
48
+ const summary = cleanExcerpt(article.excerpt_markdown);
49
+
50
+ return {
51
+ author,
52
+ authorName,
53
+ authorHref,
54
+ authorAvatarUrl,
55
+ tag,
56
+ tagName,
57
+ tagHref,
58
+ href,
59
+ title,
60
+ publishedAt,
61
+ summary,
62
+ };
63
+ };
64
+
65
+ export const BlogCardVertical = ({ article, imageClassName }: { article: BlogPost; imageClassName?: string }) => {
66
+ const { authorName, authorHref, authorAvatarUrl, href, title, summary, publishedAt } = getBlogPostData(article);
67
+
68
+ return (
69
+ <article className="flex flex-col gap-4">
70
+ <a href={href} className="overflow-hidden rounded-2xl" tabIndex={-1}>
71
+ <PhotoWithFallback
72
+ item={article}
73
+ fallbackId={article.id}
74
+ alt={title}
75
+ className={cx("aspect-[1.5] w-full object-cover transition duration-100 ease-linear hover:scale-105", imageClassName)}
76
+ />
77
+ </a>
78
+
79
+ <div className="flex flex-col gap-5">
80
+ <div className="flex flex-col gap-2">
81
+ <span className="text-sm font-semibold text-brand-secondary">Blog</span>
82
+ <div className="flex flex-col gap-1">
83
+ <a
84
+ href={href}
85
+ className="group/title flex justify-between gap-x-4 rounded-md text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
86
+ >
87
+ {title}
88
+ <ArrowUpRight
89
+ className="mt-0.5 size-6 shrink-0 text-fg-quaternary transition duration-100 ease-linear group-hover/title:text-fg-quaternary_hover"
90
+ aria-hidden="true"
91
+ />
92
+ </a>
93
+
94
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="flex gap-2">
99
+ <a href={authorHref} tabIndex={-1} className="flex">
100
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
101
+ </a>
102
+
103
+ <div>
104
+ <a
105
+ href={authorHref}
106
+ className="block rounded-xs text-sm font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
107
+ >
108
+ {authorName}
109
+ </a>
110
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </article>
115
+ );
116
+ };
117
+
118
+ export const BlogCardVerticalBadge = ({
119
+ article,
120
+ badgeTheme = "light",
121
+ imageClassName,
122
+ }: {
123
+ article: BlogPost;
124
+ badgeTheme?: ComponentProps<typeof BadgeGroup>["theme"];
125
+ imageClassName?: string;
126
+ }) => {
127
+ const { authorName, authorHref, authorAvatarUrl, href, title, summary, publishedAt } = getBlogPostData(article);
128
+
129
+ return (
130
+ <article className="flex flex-col gap-4">
131
+ <a href={href} className="overflow-hidden rounded-xl" tabIndex={-1}>
132
+ <PhotoWithFallback
133
+ item={article}
134
+ fallbackId={article.id}
135
+ alt={title}
136
+ className={cx("aspect-[1.5] w-full object-cover", imageClassName)}
137
+ />
138
+ </a>
139
+
140
+ <div className="flex flex-col gap-5">
141
+ <div className="flex flex-col items-start gap-3">
142
+ <BadgeGroup addonText="Blog" size="md" theme={badgeTheme} color="brand" className="pr-3" iconTrailing={null}>
143
+ 5 min read
144
+ </BadgeGroup>
145
+ <div className="flex flex-col gap-1">
146
+ <a
147
+ href={href}
148
+ className="flex justify-between gap-x-4 rounded-md text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
149
+ >
150
+ {title}
151
+ <ArrowUpRight className="mt-0.5 size-6 shrink-0 text-fg-quaternary" aria-hidden="true" />
152
+ </a>
153
+ <p className="line-clamp-2 text-md text-tertiary md:line-clamp-none">{summary}</p>
154
+ </div>
155
+ </div>
156
+
157
+ <div className="flex gap-2">
158
+ <a href={authorHref} tabIndex={-1} className="flex">
159
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
160
+ </a>
161
+
162
+ <div>
163
+ <a
164
+ href={authorHref}
165
+ className="block rounded-xs text-sm font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
166
+ >
167
+ {authorName}
168
+ </a>
169
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </article>
174
+ );
175
+ };
176
+
177
+ export const BlogCardVerticalCompact = ({
178
+ article,
179
+ imageClassName,
180
+ titleClassName,
181
+ className,
182
+ }: {
183
+ article: BlogPost;
184
+ imageClassName?: string;
185
+ titleClassName?: string;
186
+ className?: string;
187
+ }) => {
188
+ const { authorName, href, title, summary, publishedAt } = getBlogPostData(article);
189
+
190
+ return (
191
+ <article className={cx("flex flex-col gap-4", className)}>
192
+ <a href={href} className="overflow-hidden rounded-2xl" tabIndex={-1}>
193
+ <PhotoWithFallback
194
+ item={article}
195
+ fallbackId={article.id}
196
+ alt={title}
197
+ className={cx("aspect-[1.5] w-full object-cover", imageClassName)}
198
+ />
199
+ </a>
200
+
201
+ <div className="flex flex-col gap-6">
202
+ <div className="flex flex-col items-start gap-2">
203
+ <p className="text-sm font-semibold text-brand-secondary">
204
+ {authorName} • <time>{publishedAt}</time>
205
+ </p>
206
+ <div className="flex w-full flex-col gap-1">
207
+ <a
208
+ href={href}
209
+ className={cx(
210
+ "flex justify-between gap-x-4 rounded-md text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2",
211
+ titleClassName,
212
+ )}
213
+ >
214
+ {title}
215
+ <ArrowUpRight className="mt-0.5 size-6 shrink-0 text-fg-quaternary" aria-hidden="true" />
216
+ </a>
217
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </article>
222
+ );
223
+ };
224
+
225
+ export const BlogCardVerticalMinimal = ({ article, imageClassName, className }: { article: BlogPost; imageClassName?: string; className?: string }) => {
226
+ const { authorName, authorHref, href, title, summary, publishedAt } = getBlogPostData(article);
227
+
228
+ return (
229
+ <article className={cx("flex flex-col gap-4", className)}>
230
+ <div className="relative">
231
+ <a href={href} className="w-full" tabIndex={-1}>
232
+ <PhotoWithFallback
233
+ item={article}
234
+ fallbackId={article.id}
235
+ alt={title}
236
+ className={cx("aspect-[1.5] w-full object-cover", imageClassName)}
237
+ />
238
+ </a>
239
+ <div className="absolute inset-x-0 bottom-0 overflow-hidden bg-linear-to-b from-transparent to-black/40">
240
+ <div className="relative flex items-start justify-between bg-alpha-white/30 p-4 backdrop-blur-md before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-alpha-white/30 md:p-5">
241
+ <div>
242
+ <a
243
+ href={authorHref}
244
+ className="block rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
245
+ >
246
+ {authorName}
247
+ </a>
248
+ <time className="block text-sm text-white">{publishedAt}</time>
249
+ </div>
250
+ <a
251
+ href={href}
252
+ className="rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
253
+ >
254
+ Blog
255
+ </a>
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+ <div className="flex flex-col items-start gap-5">
261
+ <div className="flex flex-col gap-1">
262
+ <a
263
+ href={href}
264
+ className="flex justify-between gap-x-4 rounded-md text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
265
+ >
266
+ {title}
267
+ </a>
268
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
269
+ </div>
270
+
271
+ <Button href={href} color="link-color" size="lg" iconTrailing={ArrowUpRight}>
272
+ Read post
273
+ </Button>
274
+ </div>
275
+ </article>
276
+ );
277
+ };
278
+
279
+ export const BlogCardHorizontal = ({ article, imageClassName }: { article: BlogPost; imageClassName?: string }) => {
280
+ const { authorName, authorHref, authorAvatarUrl, tagName, tagHref, href, title, publishedAt, summary } = getBlogPostData(article);
281
+
282
+ return (
283
+ <article className="flex flex-col gap-4 xl:flex-row xl:items-start">
284
+ <a href={href} className="shrink-0 overflow-hidden rounded-2xl" tabIndex={-1}>
285
+ <PhotoWithFallback
286
+ item={article}
287
+ fallbackId={article.id}
288
+ alt={title}
289
+ className={cx("aspect-[1.5] w-full object-cover xl:w-80", imageClassName)}
290
+ />
291
+ </a>
292
+
293
+ <div className="flex flex-col gap-5">
294
+ <div className="flex flex-col gap-2">
295
+ <span className="text-sm font-semibold text-brand-secondary">{tagName}</span>
296
+
297
+ <div className="flex flex-col gap-1">
298
+ <a
299
+ href={href}
300
+ className="flex justify-between gap-x-4 rounded-md text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
301
+ >
302
+ {title}
303
+ </a>
304
+
305
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
306
+ </div>
307
+ </div>
308
+
309
+ <div className="flex gap-2">
310
+ <a href={authorHref} tabIndex={-1} className="flex">
311
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
312
+ </a>
313
+
314
+ <div>
315
+ <a
316
+ href={authorHref}
317
+ className="block rounded-xs text-sm font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
318
+ >
319
+ {authorName}
320
+ </a>
321
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </article>
326
+ );
327
+ };
328
+
329
+ export const BlogCardHorizontalBadge = ({ article }: { article: BlogPost }) => {
330
+ const { authorName, authorHref, authorAvatarUrl, tagName, href, title, publishedAt, summary } = getBlogPostData(article);
331
+
332
+ return (
333
+ <article className="flex flex-col gap-5 lg:flex-row lg:items-start">
334
+ <a href={href} className="shrink-0 overflow-hidden" tabIndex={-1}>
335
+ <PhotoWithFallback
336
+ item={article}
337
+ fallbackId={article.id}
338
+ alt={title}
339
+ className="h-60 w-full object-cover lg:h-50 lg:w-91.5"
340
+ />
341
+ </a>
342
+
343
+ <div className="flex flex-col gap-6">
344
+ <div className="flex flex-col gap-2">
345
+ <BadgeGroup addonText={tagName} size="md" theme="light" color="brand" className="pr-3" iconTrailing={null}>
346
+ Blog
347
+ </BadgeGroup>
348
+ <div className="flex flex-col gap-2">
349
+ <a
350
+ href={href}
351
+ className="rounded-xs text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
352
+ >
353
+ {title}
354
+ </a>
355
+
356
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
357
+ </div>
358
+ </div>
359
+
360
+ <div className="flex gap-2">
361
+ <a href={authorHref} tabIndex={-1} className="flex">
362
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
363
+ </a>
364
+
365
+ <div>
366
+ <a
367
+ href={authorHref}
368
+ className="block rounded-xs text-sm font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
369
+ >
370
+ {authorName}
371
+ </a>
372
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </article>
377
+ );
378
+ };
379
+
380
+ export const BlogCardHorizontalCompact = ({ article, imageClassName }: { article: BlogPost; imageClassName?: string }) => {
381
+ const { authorName, authorHref, href, title, publishedAt, summary } = getBlogPostData(article);
382
+
383
+ return (
384
+ <article className="flex flex-col gap-4 xl:flex-row xl:items-start">
385
+ <a href={href} className="shrink-0 overflow-hidden rounded-2xl" tabIndex={-1}>
386
+ <PhotoWithFallback
387
+ item={article}
388
+ fallbackId={article.id}
389
+ alt={title}
390
+ className={cx("aspect-[1.5] w-full object-cover xl:w-91.5", imageClassName)}
391
+ />
392
+ </a>
393
+
394
+ <div className="flex flex-col gap-6">
395
+ <div className="flex flex-col gap-2">
396
+ <p className="text-sm font-semibold text-brand-secondary">
397
+ <a href={authorHref} className="rounded-xs outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2">
398
+ {authorName}
399
+ </a>{" "}
400
+ • <time>{publishedAt}</time>
401
+ </p>
402
+ <div className="flex flex-col gap-1">
403
+ <a
404
+ href={href}
405
+ className="rounded-xs text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
406
+ >
407
+ {title}
408
+ </a>
409
+
410
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
411
+ </div>
412
+ </div>
413
+ </div>
414
+ </article>
415
+ );
416
+ };
417
+
418
+ export const BlogCardHorizontalMinimal = ({ article }: { article: BlogPost }) => {
419
+ const { authorName, authorHref, tagName, tagHref, href, title, publishedAt, summary } = getBlogPostData(article);
420
+
421
+ return (
422
+ <article className="flex flex-col gap-5 lg:flex-row lg:items-start">
423
+ <div className="relative shrink-0">
424
+ <a href={href} className="w-full" tabIndex={-1}>
425
+ <PhotoWithFallback
426
+ item={article}
427
+ fallbackId={article.id}
428
+ alt={title}
429
+ className="h-60 w-full object-cover lg:h-50 lg:w-80"
430
+ />
431
+ </a>
432
+ <div className="absolute inset-x-0 bottom-0 overflow-hidden bg-linear-to-b from-transparent to-black/40">
433
+ <div className="relative flex items-start justify-between bg-alpha-white/30 p-4 backdrop-blur-md before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-alpha-white/30">
434
+ <div>
435
+ <a
436
+ href={authorHref}
437
+ className="block rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
438
+ >
439
+ {authorName}
440
+ </a>
441
+ <time className="block text-sm text-white">{publishedAt}</time>
442
+ </div>
443
+ <a
444
+ href={tagHref}
445
+ className="rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
446
+ >
447
+ {tagName}
448
+ </a>
449
+ </div>
450
+ </div>
451
+ </div>
452
+
453
+ <div className="flex flex-col items-start gap-6">
454
+ <div className="flex flex-col gap-2">
455
+ <a
456
+ href={href}
457
+ className="block rounded-xs text-xl font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2 md:text-lg"
458
+ >
459
+ {title}
460
+ </a>
461
+ <p className="line-clamp-2 text-md text-tertiary lg:line-clamp-3">{summary}</p>
462
+ </div>
463
+
464
+ <Button href={href} color="link-color" size="lg" iconTrailing={ArrowUpRight}>
465
+ Read post
466
+ </Button>
467
+ </div>
468
+ </article>
469
+ );
470
+ };
471
+
472
+ export const BlogCardFullWidthVertical = ({ article }: { article: BlogPost }) => {
473
+ const { authorName, authorHref, authorAvatarUrl, tagName, tagHref, href, title, publishedAt, summary } = getBlogPostData(article);
474
+
475
+ return (
476
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset">
477
+ <a href={href} tabIndex={-1}>
478
+ <PhotoWithFallback
479
+ item={article}
480
+ fallbackId={article.id}
481
+ alt={title}
482
+ className="h-50 w-full object-cover md:h-60"
483
+ />
484
+ </a>
485
+
486
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
487
+ <div className="flex flex-col gap-2">
488
+ <Button color="link-color" href={tagHref}>
489
+ {tagName}
490
+ </Button>
491
+ <div className="flex flex-col gap-2">
492
+ <Button
493
+ color="link-gray"
494
+ href={href}
495
+ className="flex justify-between gap-4 text-xl font-semibold text-primary hover:text-brand-secondary md:text-display-xs"
496
+ iconTrailing={<ArrowUpRight className="size-6 shrink-0" aria-hidden="true" />}
497
+ >
498
+ {title}
499
+ </Button>
500
+ <p className="line-clamp-2 text-md text-tertiary md:line-clamp-3">{summary}</p>
501
+ </div>
502
+ </div>
503
+
504
+ <div className="flex gap-2">
505
+ <a href={authorHref} tabIndex={-1} className="flex">
506
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
507
+ </a>
508
+
509
+ <div>
510
+ <p className="text-sm font-semibold">
511
+ <Button color="link-color" href={authorHref} className="text-primary">
512
+ {authorName}
513
+ </Button>
514
+ </p>
515
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ </article>
520
+ );
521
+ };
522
+
523
+ export const BlogCardFullWidthVerticalAlt = ({ article }: { article: BlogPost }) => {
524
+ const { authorName, authorHref, authorAvatarUrl, tagName, tagHref, href, title, publishedAt, summary } = getBlogPostData(article);
525
+
526
+ return (
527
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset">
528
+ <a href={href} tabIndex={-1}>
529
+ <PhotoWithFallback
530
+ item={article}
531
+ fallbackId={article.id}
532
+ alt={title}
533
+ className="h-60 w-full object-cover"
534
+ />
535
+ </a>
536
+
537
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
538
+ <div className="flex flex-col gap-4">
539
+ <BadgeGroup addonText={tagName} size="md" theme="light" color="brand" className="pr-3" iconTrailing={null}>
540
+ Blog
541
+ </BadgeGroup>
542
+ <div className="flex flex-col gap-2">
543
+ <Button
544
+ color="link-gray"
545
+ href={href}
546
+ className="flex justify-between gap-4 text-xl font-semibold text-primary hover:text-brand-secondary md:text-display-xs"
547
+ iconTrailing={<ArrowUpRight className="size-6 shrink-0" aria-hidden="true" />}
548
+ >
549
+ {title}
550
+ </Button>
551
+ <p className="line-clamp-2 text-md text-tertiary md:line-clamp-3">{summary}</p>
552
+ </div>
553
+ </div>
554
+
555
+ <div className="flex gap-2">
556
+ <a href={authorHref} tabIndex={-1} className="flex">
557
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
558
+ </a>
559
+
560
+ <div>
561
+ <p className="text-sm font-semibold">
562
+ <Button color="link-color" href={authorHref} className="text-primary">
563
+ {authorName}
564
+ </Button>
565
+ </p>
566
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
567
+ </div>
568
+ </div>
569
+ </div>
570
+ </article>
571
+ );
572
+ };
573
+
574
+ export const BlogCardFullWidthVerticalCompact = ({ article }: { article: BlogPost }) => {
575
+ const { authorName, authorHref, href, title, publishedAt, summary } = getBlogPostData(article);
576
+
577
+ return (
578
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset">
579
+ <a href={href} tabIndex={-1}>
580
+ <PhotoWithFallback
581
+ item={article}
582
+ fallbackId={article.id}
583
+ alt={title}
584
+ className="h-60 w-full object-cover"
585
+ />
586
+ </a>
587
+
588
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
589
+ <div className="flex flex-col gap-2">
590
+ <p className="text-sm font-semibold text-brand-secondary">
591
+ <Button href={authorHref} color="link-color">
592
+ {authorName}
593
+ </Button>{" "}
594
+ • <time>{publishedAt}</time>
595
+ </p>
596
+ <div className="flex flex-col gap-2">
597
+ <Button
598
+ color="link-gray"
599
+ href={href}
600
+ className="flex justify-between gap-4 text-xl font-semibold text-primary hover:text-brand-secondary md:text-display-xs"
601
+ iconTrailing={<ArrowUpRight className="size-6 shrink-0" aria-hidden="true" />}
602
+ >
603
+ {title}
604
+ </Button>
605
+ <p className="line-clamp-2 text-md text-tertiary md:line-clamp-3">{summary}</p>
606
+ </div>
607
+ </div>
608
+ </div>
609
+ </article>
610
+ );
611
+ };
612
+
613
+ export const BlogCardFullWidthVerticalMinimal = ({ article }: { article: BlogPost }) => {
614
+ const { authorName, authorHref, tagName, tagHref, href, title, publishedAt, summary } = getBlogPostData(article);
615
+
616
+ return (
617
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset">
618
+ <div className="relative shrink-0">
619
+ <a href={href} className="w-full" tabIndex={-1}>
620
+ <PhotoWithFallback
621
+ item={article}
622
+ fallbackId={article.id}
623
+ alt={title}
624
+ className="h-60 w-full object-cover md:h-70"
625
+ />
626
+ </a>
627
+ <div className="absolute inset-x-0 bottom-0 overflow-hidden bg-linear-to-b from-transparent to-black/40">
628
+ <div className="relative flex items-start justify-between bg-alpha-white/30 p-4 backdrop-blur-md before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-alpha-white/30 md:p-6">
629
+ <div>
630
+ <p className="text-sm font-semibold">
631
+ <Button href={authorHref} color="link-gray" className="text-white">
632
+ {authorName}
633
+ </Button>
634
+ </p>
635
+ <time className="block text-sm text-white">{publishedAt}</time>
636
+ </div>
637
+ <p className="text-sm font-semibold">
638
+ <Button href={tagHref} color="link-gray" className="text-white">
639
+ {tagName}
640
+ </Button>
641
+ </p>
642
+ </div>
643
+ </div>
644
+ </div>
645
+
646
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
647
+ <div className="flex flex-col gap-2">
648
+ <div className="flex flex-col gap-2">
649
+ <Button
650
+ color="link-gray"
651
+ href={href}
652
+ className="flex justify-between gap-4 text-xl font-semibold text-primary hover:text-brand-secondary md:text-display-xs"
653
+ iconTrailing={<ArrowUpRight className="size-6 shrink-0" aria-hidden="true" />}
654
+ >
655
+ {title}
656
+ </Button>
657
+ <p className="line-clamp-2 text-md text-tertiary md:line-clamp-3">{summary}</p>
658
+ </div>
659
+ </div>
660
+
661
+ <Button href={href} color="link-color" size="lg" iconTrailing={ArrowUpRight}>
662
+ Read post
663
+ </Button>
664
+ </div>
665
+ </article>
666
+ );
667
+ };
668
+
669
+ export const BlogCardFullWidthHorizontal = ({ article }: { article: BlogPost }) => {
670
+ const { authorName, authorHref, authorAvatarUrl, tagName, tagHref, href, title, publishedAt, summary } = getBlogPostData(article);
671
+
672
+ return (
673
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset md:flex-row md:items-start">
674
+ <a href={href} className="shrink-0" tabIndex={-1}>
675
+ <PhotoWithFallback
676
+ item={article}
677
+ fallbackId={article.id}
678
+ alt={title}
679
+ className="h-60 w-full object-cover md:h-60 md:w-80"
680
+ />
681
+ </a>
682
+
683
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
684
+ <div className="flex flex-col gap-2">
685
+ <Button href={tagHref} color="link-color">
686
+ {tagName}
687
+ </Button>
688
+ <div className="flex flex-col gap-2">
689
+ <Button href={href} color="link-gray" size="xl" className="text-xl font-semibold text-primary md:text-lg">
690
+ {title}
691
+ </Button>
692
+
693
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
694
+ </div>
695
+ </div>
696
+
697
+ <div className="flex gap-2">
698
+ <a href={authorHref} tabIndex={-1} className="flex">
699
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
700
+ </a>
701
+
702
+ <div>
703
+ <p className="text-sm font-semibold">
704
+ <Button href={authorHref} color="link-gray" className="text-primary">
705
+ {authorName}
706
+ </Button>
707
+ </p>
708
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
709
+ </div>
710
+ </div>
711
+ </div>
712
+ </article>
713
+ );
714
+ };
715
+
716
+ export const BlogCardFullWidthHorizontalAlt = ({ article }: { article: BlogPost }) => {
717
+ const { authorName, authorHref, authorAvatarUrl, tagName, href, title, publishedAt, summary } = getBlogPostData(article);
718
+
719
+ return (
720
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset md:flex-row md:items-start">
721
+ <a href={href} className="shrink-0" tabIndex={-1}>
722
+ <PhotoWithFallback
723
+ item={article}
724
+ fallbackId={article.id}
725
+ alt={title}
726
+ className="h-60 w-full object-cover md:h-60.5 md:w-80"
727
+ />
728
+ </a>
729
+
730
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
731
+ <div className="flex flex-col gap-4">
732
+ <BadgeGroup addonText={tagName} size="md" theme="light" color="brand" className="pr-3" iconTrailing={null}>
733
+ Blog
734
+ </BadgeGroup>
735
+ <div className="flex flex-col gap-2">
736
+ <Button href={href} color="link-gray" size="xl" className="text-xl font-semibold text-primary md:text-lg">
737
+ {title}
738
+ </Button>
739
+
740
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
741
+ </div>
742
+ </div>
743
+
744
+ <div className="flex gap-2">
745
+ <a href={authorHref} tabIndex={-1} className="flex">
746
+ <Avatar focusable alt={authorName} src={authorAvatarUrl} size="md" />
747
+ </a>
748
+
749
+ <div>
750
+ <p className="text-sm font-semibold">
751
+ <Button href={authorHref} color="link-gray" className="text-primary">
752
+ {authorName}
753
+ </Button>
754
+ </p>
755
+ <time className="block text-sm text-tertiary">{publishedAt}</time>
756
+ </div>
757
+ </div>
758
+ </div>
759
+ </article>
760
+ );
761
+ };
762
+
763
+ export const BlogCardFullWidthHorizontalCompact = ({ article }: { article: BlogPost }) => {
764
+ const { authorName, authorHref, href, title, publishedAt, summary } = getBlogPostData(article);
765
+
766
+ return (
767
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset md:flex-row md:items-start">
768
+ <a href={href} className="shrink-0" tabIndex={-1}>
769
+ <PhotoWithFallback
770
+ item={article}
771
+ fallbackId={article.id}
772
+ alt={title}
773
+ className="h-60 w-full object-cover md:h-60 md:w-80"
774
+ />
775
+ </a>
776
+
777
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
778
+ <div className="flex flex-col gap-2">
779
+ <p className="text-sm font-semibold text-brand-secondary">
780
+ <Button href={authorHref} color="link-color">
781
+ {authorName}
782
+ </Button>{" "}
783
+ • <time>{publishedAt}</time>
784
+ </p>
785
+ <div className="flex flex-col gap-2">
786
+ <Button href={href} color="link-gray" size="xl" className="text-xl font-semibold text-primary md:text-lg">
787
+ {title}
788
+ </Button>
789
+
790
+ <p className="line-clamp-2 text-md text-tertiary">{summary}</p>
791
+ </div>
792
+ </div>
793
+ </div>
794
+ </article>
795
+ );
796
+ };
797
+
798
+ export const BlogCardFullWidthHorizontalMinimal = ({ article }: { article: BlogPost }) => {
799
+ const { authorName, authorHref, tagName, tagHref, href, title, publishedAt, summary } = getBlogPostData(article);
800
+
801
+ return (
802
+ <article className="flex flex-col overflow-hidden rounded-2xl ring-1 ring-secondary ring-inset md:flex-row md:items-start">
803
+ <div className="relative shrink-0">
804
+ <a href={href} className="w-full" tabIndex={-1}>
805
+ <PhotoWithFallback
806
+ item={article}
807
+ fallbackId={article.id}
808
+ alt={title}
809
+ className="h-60 w-full object-cover md:h-60 md:w-80"
810
+ />
811
+ </a>
812
+ <div className="absolute inset-x-0 bottom-0 overflow-hidden bg-linear-to-b from-transparent to-black/40">
813
+ <div className="relative flex items-start justify-between bg-alpha-white/30 p-4 backdrop-blur-md before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-alpha-white/30 md:p-6">
814
+ <div>
815
+ <p className="text-sm font-semibold">
816
+ <Button href={authorHref} color="link-gray" className="text-white">
817
+ {authorName}
818
+ </Button>
819
+ </p>
820
+ <time className="block text-sm text-white">{publishedAt}</time>
821
+ </div>
822
+ <p className="text-sm font-semibold">
823
+ <Button href={tagHref} color="link-gray" className="text-white">
824
+ {tagName}
825
+ </Button>
826
+ </p>
827
+ </div>
828
+ </div>
829
+ </div>
830
+
831
+ <div className="flex flex-col gap-6 p-5 pb-6 md:p-6">
832
+ <div className="flex flex-col gap-2">
833
+ <div className="flex flex-col gap-2">
834
+ <Button href={href} color="link-gray" size="xl" className="text-xl font-semibold text-primary md:text-lg">
835
+ {title}
836
+ </Button>
837
+
838
+ <p className="line-clamp-2 text-md text-tertiary md:line-clamp-3">{summary}</p>
839
+ </div>
840
+ </div>
841
+
842
+ <Button href={href} color="link-color" size="lg" iconTrailing={ArrowUpRight}>
843
+ Read post
844
+ </Button>
845
+ </div>
846
+ </article>
847
+ );
848
+ };