keystone-design-bootstrap 1.0.4 → 1.0.6

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": "keystone-design-bootstrap",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -34,7 +34,7 @@ export const styles = sortCx({
34
34
 
35
35
  colors: {
36
36
  primary: {
37
- root: "bg-primary text-primary_contrast hover:opacity-90",
37
+ root: "bg-brand-solid text-white hover:bg-brand-solid_hover transition-colors",
38
38
  },
39
39
  secondary: {
40
40
  root: "bg-white text-fg-primary border-2 hover:opacity-90 transition-all",
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import React, { ReactNode } from 'react';
4
+ import { Carousel } from '../carousel/carousel';
5
+ import { ChevronLeft, ChevronRight } from '@untitledui/icons';
6
+
7
+ interface CarouselSectionWrapperProps {
8
+ title?: string;
9
+ children: ReactNode;
10
+ hasItems: boolean;
11
+ emptyMessage?: string;
12
+ }
13
+
14
+ export const CarouselSectionWrapper = ({
15
+ title = "",
16
+ children,
17
+ hasItems,
18
+ emptyMessage = "No items available"
19
+ }: CarouselSectionWrapperProps) => {
20
+ if (!hasItems) {
21
+ return (
22
+ <>
23
+ {title && (
24
+ <div className="mb-12">
25
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
26
+ {title}
27
+ </h2>
28
+ </div>
29
+ )}
30
+ <div className="text-center py-12">
31
+ <p className="font-body text-base text-tertiary">{emptyMessage}</p>
32
+ </div>
33
+ </>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <Carousel.Root opts={{ align: "start", loop: true }}>
39
+ <div className="flex items-center justify-between mb-12">
40
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
41
+ {title}
42
+ </h2>
43
+ <div className="flex gap-2">
44
+ <Carousel.PrevTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
45
+ <ChevronLeft className="w-6 h-6 text-fg-primary" />
46
+ </Carousel.PrevTrigger>
47
+ <Carousel.NextTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
48
+ <ChevronRight className="w-6 h-6 text-fg-primary" />
49
+ </Carousel.NextTrigger>
50
+ </div>
51
+ </div>
52
+
53
+ <Carousel.Content className="-ml-4">
54
+ {children}
55
+ </Carousel.Content>
56
+ </Carousel.Root>
57
+ );
58
+ };
59
+
@@ -153,6 +153,7 @@ export { SlideoutMenu } from './slideout-menus/slideout-menu';
153
153
  export * from './rating/rating-stars';
154
154
  export * from './rating/rating-badge';
155
155
  export * from './carousel/carousel';
156
+ export { CarouselSectionWrapper } from './carousel/carousel-section-wrapper';
156
157
 
157
158
  // Re-export combobox (still exists in select directory but not wrapped with theming)
158
159
  export { ComboBox } from './select/combobox';
@@ -1,60 +1,117 @@
1
1
  "use client";
2
2
 
3
- import { PhotoWithFallback } from '../elements';
3
+ import React, { useState } from 'react';
4
+ import { PhotoWithFallback, PaginationPageDefault } from '../elements';
4
5
  import type { BlogPost } from '../../types/api/blog-post';
5
6
 
6
7
  interface BlogGalleryProps {
7
8
  blogPosts?: BlogPost[] | null;
9
+ postsPerPage?: number;
10
+ className?: string;
8
11
  }
9
12
 
10
13
  export const BlogGallery = ({
11
14
  blogPosts: postsData,
15
+ postsPerPage = 12,
16
+ className = "",
12
17
  }: BlogGalleryProps) => {
13
18
  const posts = Array.isArray(postsData) ? postsData : [];
19
+ const [currentPage, setCurrentPage] = useState(1);
20
+
21
+ // Get featured post (first post with featured: true or first post)
22
+ const featuredPost = posts.find((post) => post.featured) || posts[0];
23
+ const nonFeaturedPosts = posts.filter((post) => post.id !== featuredPost?.id);
24
+
25
+ // Calculate pagination for non-featured posts
26
+ const totalPages = Math.ceil(nonFeaturedPosts.length / postsPerPage);
27
+ const startIndex = (currentPage - 1) * postsPerPage;
28
+ const endIndex = startIndex + postsPerPage;
29
+ const paginatedNonFeaturedPosts = nonFeaturedPosts.slice(startIndex, endIndex);
14
30
 
15
31
  return (
16
- <section>
32
+ <section className={className}>
17
33
  <div className="mx-auto max-w-container px-4 md:px-8">
18
34
  {posts.length > 0 ? (
19
- <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
20
- {posts.map((post: any) => {
21
- const excerpt = post.summary || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
22
-
23
- return (
24
- <div key={post.id} className="flex flex-col">
25
- <div className="w-full h-64 mb-6 overflow-hidden">
26
- <PhotoWithFallback
27
- item={post}
28
- fallbackId={post.id}
29
- className="w-full h-full object-cover"
30
- />
31
- </div>
32
-
33
- <p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
34
- {new Date(post.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
35
+ <>
36
+ {/* Featured Post - Full Width */}
37
+ {featuredPost && (
38
+ <a
39
+ href={`/blog/${featuredPost.slug}`}
40
+ className="block mb-16 group"
41
+ >
42
+ <div className="w-full h-96 md:h-[500px] mb-8 overflow-hidden">
43
+ <PhotoWithFallback
44
+ item={featuredPost}
45
+ fallbackId={featuredPost.id}
46
+ className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
47
+ />
48
+ </div>
49
+
50
+ <div className="max-w-3xl">
51
+ <p className="text-xs font-body font-normal uppercase tracking-widest mb-4" style={{ color: 'var(--color-text-brand-secondary)' }}>
52
+ {featuredPost.published_at ? new Date(featuredPost.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) : 'Recent'}
35
53
  </p>
36
54
 
37
- <h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
38
- {post.title}
39
- </h3>
55
+ <h2 className="font-display text-3xl md:text-4xl font-normal text-fg-primary mb-6 group-hover:underline">
56
+ {featuredPost.title}
57
+ </h2>
40
58
 
41
- {excerpt && (
42
- <p className="font-body text-base leading-relaxed text-tertiary mb-6 flex-grow">
43
- {excerpt}
59
+ {featuredPost.excerpt_markdown && (
60
+ <p className="font-display text-lg leading-relaxed text-tertiary">
61
+ {featuredPost.excerpt_markdown.replace(/[#*\[\]()]/g, '').trim()}
44
62
  </p>
45
63
  )}
46
-
47
- <a
48
- href={`/blog/${post.slug}`}
49
- className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
50
- style={{ color: 'var(--color-text-brand-accent)' }}
51
- >
52
- Read more
53
- </a>
54
64
  </div>
55
- );
56
- })}
57
- </div>
65
+ </a>
66
+ )}
67
+
68
+ {/* Posts Grid */}
69
+ {paginatedNonFeaturedPosts.length > 0 && (
70
+ <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3 mb-16">
71
+ {paginatedNonFeaturedPosts.map((post: any) => {
72
+ const excerpt = post.excerpt_markdown || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
73
+
74
+ return (
75
+ <a key={post.id} href={`/blog/${post.slug}`} className="flex flex-col group">
76
+ <div className="w-full h-64 mb-6 overflow-hidden">
77
+ <PhotoWithFallback
78
+ item={post}
79
+ fallbackId={post.id}
80
+ className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
81
+ />
82
+ </div>
83
+
84
+ <p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
85
+ {post.published_at ? new Date(post.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) : 'Recent'}
86
+ </p>
87
+
88
+ <h3 className="font-display text-2xl font-normal text-fg-primary mb-4 group-hover:underline">
89
+ {post.title}
90
+ </h3>
91
+
92
+ {excerpt && (
93
+ <p className="font-body text-base leading-relaxed text-tertiary mb-4 flex-grow line-clamp-3">
94
+ {excerpt}
95
+ </p>
96
+ )}
97
+ </a>
98
+ );
99
+ })}
100
+ </div>
101
+ )}
102
+
103
+ {/* Pagination */}
104
+ {totalPages > 1 && (
105
+ <div className="flex justify-center">
106
+ <PaginationPageDefault
107
+ rounded
108
+ page={currentPage}
109
+ total={totalPages}
110
+ onPageChange={setCurrentPage}
111
+ />
112
+ </div>
113
+ )}
114
+ </>
58
115
  ) : (
59
116
  <div className="text-center py-12">
60
117
  <p className="font-body text-base text-tertiary">No posts available</p>
@@ -11,11 +11,7 @@ import type { BlogPost } from '../../types/api/blog-post';
11
11
 
12
12
  interface BlogGalleryProps {
13
13
  blogPosts?: BlogPost[] | null;
14
- label?: string;
15
- title?: string;
16
- subtitle?: string;
17
14
  postsPerPage?: number;
18
- backgroundColor?: string;
19
15
  className?: string;
20
16
  }
21
17
 
@@ -27,11 +23,7 @@ const defaultSortByOptions = [
27
23
 
28
24
  export const BlogGallery = ({
29
25
  blogPosts: postsData,
30
- label = "",
31
- title = "",
32
- subtitle = "",
33
26
  postsPerPage = 12,
34
- backgroundColor = "bg-primary",
35
27
  className = "",
36
28
  }: BlogGalleryProps) => {
37
29
  const sortByOptions = defaultSortByOptions;
@@ -68,26 +60,8 @@ export const BlogGallery = ({
68
60
  }, [sortBy]);
69
61
 
70
62
  return (
71
- <div className={`${backgroundColor} ${className}`}>
72
- {/* Hero Section */}
73
- <section className={`${backgroundColor} py-16 md:py-24`}>
74
- <div className="mx-auto w-full max-w-container px-4 md:px-8">
75
- <div className="flex w-full max-w-3xl flex-col">
76
- {label && (
77
- <span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
78
- )}
79
- <h2 className="mt-3 text-display-md font-semibold text-primary md:text-display-lg">
80
- {title}
81
- </h2>
82
- <p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
83
- {subtitle}
84
- </p>
85
- </div>
86
- </div>
87
- </section>
88
-
89
- {/* Main Content */}
90
- <main className="mx-auto flex w-full max-w-container flex-col gap-12 px-4 pb-16 md:gap-16 md:px-8 md:pb-24">
63
+ <section className={className}>
64
+ <div className="mx-auto flex w-full max-w-container flex-col gap-12 px-4 md:gap-16 md:px-8">
91
65
  {posts.length > 0 ? (
92
66
  <>
93
67
  {/* Featured Post - Desktop */}
@@ -196,7 +170,7 @@ export const BlogGallery = ({
196
170
  <p className="text-gray-500">No blog posts available</p>
197
171
  </div>
198
172
  )}
199
- </main>
200
- </div>
173
+ </div>
174
+ </section>
201
175
  );
202
176
  };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import React from 'react';
4
- import { PhotoWithFallback } from '../elements';
4
+ import { PhotoWithFallback, Carousel, CarouselSectionWrapper } from '../elements';
5
5
  import type { BlogPost } from '../../types/api/blog-post';
6
6
 
7
7
  interface BlogSectionProps {
@@ -13,10 +13,10 @@ interface BlogSectionProps {
13
13
  export const BlogSection = ({
14
14
  blogPosts: postsData,
15
15
  title = "",
16
- maxPosts = 3,
16
+ maxPosts,
17
17
  }: BlogSectionProps) => {
18
18
 
19
- const postsArray = Array.isArray(postsData) ? postsData.slice(0, maxPosts) : [];
19
+ const postsArray = Array.isArray(postsData) ? (maxPosts ? postsData.slice(0, maxPosts) : postsData) : [];
20
20
 
21
21
  const formatDate = (dateString?: string) => {
22
22
  if (!dateString) return 'Recent';
@@ -34,24 +34,22 @@ export const BlogSection = ({
34
34
  return (
35
35
  <section>
36
36
  <div className="mx-auto max-w-container px-4 md:px-8">
37
- <div className="mb-12">
38
- <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
39
- {title}
40
- </h2>
41
- </div>
42
-
43
- {postsArray.length > 0 ? (
44
- <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
45
- {postsArray.map((post: BlogPost) => {
46
- const excerpt = (post as any).summary || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
47
-
48
- return (
49
- <div key={post.id} className="flex flex-col">
37
+ <CarouselSectionWrapper
38
+ title={title}
39
+ hasItems={postsArray.length > 0}
40
+ emptyMessage="No posts available"
41
+ >
42
+ {postsArray.map((post: BlogPost) => {
43
+ const excerpt = (post as any).summary || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
44
+
45
+ return (
46
+ <Carousel.Item key={post.id} className="pl-4 md:basis-1/2 lg:basis-1/3">
47
+ <a href={`/blog/${post.slug}`} className="flex flex-col h-full group">
50
48
  <div className="w-full h-64 mb-6 overflow-hidden">
51
49
  <PhotoWithFallback
52
50
  item={post}
53
51
  fallbackId={post.id}
54
- className="w-full h-full object-cover"
52
+ className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
55
53
  />
56
54
  </div>
57
55
 
@@ -59,32 +57,20 @@ export const BlogSection = ({
59
57
  {formatDate(post.published_at)}
60
58
  </p>
61
59
 
62
- <h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
60
+ <h3 className="font-display text-2xl font-normal text-fg-primary mb-4 group-hover:underline">
63
61
  {post.title}
64
62
  </h3>
65
63
 
66
64
  {excerpt && (
67
- <p className="font-body text-base leading-relaxed text-tertiary mb-6 flex-grow">
65
+ <p className="font-body text-base leading-relaxed text-tertiary mb-4 flex-grow line-clamp-3">
68
66
  {excerpt}
69
67
  </p>
70
68
  )}
71
-
72
- <a
73
- href={`/blog/${post.slug}`}
74
- className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
75
- style={{ color: 'var(--color-text-brand-accent)' }}
76
- >
77
- Read more
78
- </a>
79
- </div>
80
- );
81
- })}
82
- </div>
83
- ) : (
84
- <div className="text-center py-12">
85
- <p className="font-body text-base text-tertiary">No posts available</p>
86
- </div>
87
- )}
69
+ </a>
70
+ </Carousel.Item>
71
+ );
72
+ })}
73
+ </CarouselSectionWrapper>
88
74
  </div>
89
75
  </section>
90
76
  );
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState } from 'react';
4
- import { Form, PhotoWithFallback } from '../elements';
4
+ import { Form, PhotoWithFallback, Button } from '../elements';
5
5
  import type { WebsitePhotos } from '../../types/api/website-photos';
6
6
 
7
7
  interface ContactSectionProps {
@@ -125,13 +125,14 @@ const ContactSection = ({
125
125
  </label>
126
126
  </div>
127
127
 
128
- <button
129
- type="submit"
130
- className="w-full font-body text-base uppercase tracking-wide py-3 px-6 rounded-sm hover:opacity-90 transition-opacity"
131
- style={{ backgroundColor: '#1E1E1E', color: '#FFFFFF' }}
128
+ <Button
129
+ type="submit"
130
+ color="primary"
131
+ size="xl"
132
+ className="w-full font-body text-base uppercase tracking-wide rounded-sm"
132
133
  >
133
134
  Send message
134
- </button>
135
+ </Button>
135
136
  </Form>
136
137
  </div>
137
138
 
@@ -145,14 +145,25 @@ export function HeaderNavigation({
145
145
  {companyName}
146
146
  </Link>
147
147
 
148
- {/* Right: Contact Button */}
149
- <div className="flex items-center gap-6">
148
+ {/* Right: CTA Buttons */}
149
+ <div className="flex items-center gap-3">
150
+ {props?.cta_button?.secondary_label && props?.cta_button?.secondary_href && (
151
+ <Button
152
+ href={props.cta_button.secondary_href}
153
+ size="sm"
154
+ color="primary"
155
+ className="font-body text-sm uppercase tracking-wide px-6 py-2 rounded-sm"
156
+ >
157
+ {props.cta_button.secondary_label}
158
+ </Button>
159
+ )}
150
160
  <Button
151
- href="/contact"
161
+ href={props?.cta_button?.href || "/contact"}
152
162
  size="sm"
153
- className="bg-fg-primary text-white font-body text-sm uppercase tracking-wide px-6 py-2 rounded-sm hover:opacity-90"
163
+ color="primary"
164
+ className="font-body text-sm uppercase tracking-wide px-6 py-2 rounded-sm"
154
165
  >
155
- Contact
166
+ {props?.cta_button?.label || "Contact"}
156
167
  </Button>
157
168
  </div>
158
169
  </div>
@@ -333,24 +344,52 @@ export function HeaderNavigation({
333
344
  </nav>
334
345
 
335
346
  {/* Mobile Menu Footer */}
336
- <div className="border-t border-secondary px-4 py-4">
337
- <select className="w-full font-body text-sm text-fg-primary bg-transparent border-b border-secondary py-2">
338
- <option>English</option>
339
- </select>
347
+ <div className="border-t border-secondary px-4 py-6">
348
+ <div className="flex flex-col gap-3">
349
+ {props?.cta_button?.secondary_label && props?.cta_button?.secondary_href && (
350
+ <Button
351
+ href={props.cta_button.secondary_href}
352
+ color="primary"
353
+ className="w-full font-body text-sm uppercase tracking-wide py-3 rounded-sm"
354
+ onClick={() => setIsMobileMenuOpen(false)}
355
+ >
356
+ {props.cta_button.secondary_label}
357
+ </Button>
358
+ )}
359
+ <Button
360
+ href={props?.cta_button?.href || "/contact"}
361
+ color="primary"
362
+ className="w-full font-body text-sm uppercase tracking-wide py-3 rounded-sm"
363
+ onClick={() => setIsMobileMenuOpen(false)}
364
+ >
365
+ {props?.cta_button?.label || "Contact"}
366
+ </Button>
367
+ </div>
340
368
  </div>
341
369
  </div>
342
370
  </div>
343
371
  )}
344
372
 
345
373
  {/* Sticky Contact Button (Mobile) */}
346
- <div className="fixed bottom-0 left-0 right-0 z-40 md:hidden" style={{ backgroundColor: '#1E1E1E' }}>
347
- <Button
348
- href="/contact"
349
- className="w-full font-body text-sm uppercase tracking-wide py-4 rounded-none"
350
- style={{ backgroundColor: '#1E1E1E', color: '#FFFFFF' }}
351
- >
352
- Contact
353
- </Button>
374
+ <div className="fixed bottom-0 left-0 right-0 z-40 md:hidden bg-fg-primary">
375
+ <div className="flex gap-0">
376
+ {props?.cta_button?.secondary_label && props?.cta_button?.secondary_href && (
377
+ <Button
378
+ href={props.cta_button.secondary_href}
379
+ color="primary"
380
+ className="flex-1 font-body text-sm uppercase tracking-wide py-4 rounded-none border-r border-gray-700"
381
+ >
382
+ {props.cta_button.secondary_label}
383
+ </Button>
384
+ )}
385
+ <Button
386
+ href={props?.cta_button?.href || "/contact"}
387
+ color="primary"
388
+ className={`${props?.cta_button?.secondary_label ? 'flex-1' : 'w-full'} font-body text-sm uppercase tracking-wide py-4 rounded-none`}
389
+ >
390
+ {props?.cta_button?.label || "Contact"}
391
+ </Button>
392
+ </div>
354
393
  </div>
355
394
  </>
356
395
  );
@@ -246,6 +246,15 @@ export function HeaderNavigation({
246
246
  </div>
247
247
 
248
248
  <div className="hidden items-center gap-3 md:flex">
249
+ {cta_button?.secondary_label && cta_button?.secondary_href && (
250
+ <Button
251
+ href={cta_button.secondary_href}
252
+ color="secondary"
253
+ size="lg"
254
+ >
255
+ {cta_button.secondary_label}
256
+ </Button>
257
+ )}
249
258
  <Button
250
259
  href={cta_button?.href || "/contact"}
251
260
  color="primary"
@@ -254,7 +263,6 @@ export function HeaderNavigation({
254
263
  {cta_button?.label || "Get Started"}
255
264
  </Button>
256
265
  </div>
257
-
258
266
  <AriaDialogTrigger>
259
267
  <AriaButton
260
268
  aria-label="Toggle navigation menu"
@@ -315,6 +323,15 @@ export function HeaderNavigation({
315
323
  </ul>
316
324
 
317
325
  <div className="flex flex-col gap-3 border-t border-secondary px-4 py-6">
326
+ {cta_button?.secondary_label && cta_button?.secondary_href && (
327
+ <Button
328
+ href={cta_button.secondary_href}
329
+ color="secondary"
330
+ size="lg"
331
+ >
332
+ {cta_button.secondary_label}
333
+ </Button>
334
+ )}
318
335
  <Button
319
336
  href={cta_button?.href || "/contact"}
320
337
  color="primary"
@@ -46,7 +46,6 @@ const parseBusinessHours = (hours: string | Record<string, { open: string; close
46
46
  };
47
47
 
48
48
  export const LocationDetailsSection = ({
49
- config,
50
49
  location,
51
50
  }: LocationDetailsSectionProps) => {
52
51
  if (!location) return null;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { PhotoWithFallback } from '../elements';
3
+ import React from 'react';
4
+ import { PhotoWithFallback, Carousel, CarouselSectionWrapper } from '../elements';
4
5
  import type { Testimonial } from '../../types/api/testimonial';
5
6
  import { getAvatarUrl } from '../../utils/photo-helpers';
6
7
 
@@ -16,32 +17,27 @@ export const TestimonialsHome = ({
16
17
  perPage,
17
18
  }: TestimonialsHomeProps) => {
18
19
  const testimonials = Array.isArray(testimonialsData) ? testimonialsData : [];
19
- const displayTestimonials = perPage ? testimonials.slice(0, perPage) : testimonials.slice(0, 3);
20
+ const displayTestimonials = perPage ? testimonials.slice(0, perPage) : testimonials;
20
21
 
21
22
  return (
22
23
  <section>
23
24
  <div className="mx-auto max-w-container px-4 md:px-8">
24
- <div className="mb-12 text-center">
25
- <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
26
- {title}
27
- </h2>
28
- </div>
29
-
30
- {displayTestimonials && displayTestimonials.length > 0 ? (
31
- <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
32
- {displayTestimonials.map((testimonial: any, i: number) => {
33
- const quote = testimonial.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
34
- const reviewerName = testimonial.reviewer_name || 'Customer';
35
- const username = `@${reviewerName.toLowerCase().replace(/\s+/g, '')}`;
36
- const avatarUrl = getAvatarUrl(testimonial.photo_attachments, testimonial.id, reviewerName);
37
- const rating = testimonial.rating || 5;
38
- const isVerified = testimonial.is_verified !== false;
39
-
40
- return (
41
- <div
42
- key={testimonial.id || i}
43
- className="bg-white p-8 flex flex-col"
44
- >
25
+ <CarouselSectionWrapper
26
+ title={title}
27
+ hasItems={displayTestimonials.length > 0}
28
+ emptyMessage="No testimonials available"
29
+ >
30
+ {displayTestimonials.map((testimonial: any, i: number) => {
31
+ const quote = testimonial.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
32
+ const reviewerName = testimonial.reviewer_name || 'Customer';
33
+ const username = `@${reviewerName.toLowerCase().replace(/\s+/g, '')}`;
34
+ const avatarUrl = getAvatarUrl(testimonial.photo_attachments, testimonial.id, reviewerName);
35
+ const rating = testimonial.rating || 5;
36
+ const isVerified = testimonial.is_verified !== false;
37
+
38
+ return (
39
+ <Carousel.Item key={testimonial.id || i} className="pl-4 md:basis-1/2 lg:basis-1/3">
40
+ <div className="bg-white p-8 flex flex-col h-full">
45
41
  <div className="flex gap-1 mb-6">
46
42
  {Array.from({ length: 5 }).map((_, starIdx) => (
47
43
  <svg
@@ -87,14 +83,10 @@ export const TestimonialsHome = ({
87
83
  </div>
88
84
  </div>
89
85
  </div>
90
- );
91
- })}
92
- </div>
93
- ) : (
94
- <div className="text-center py-12">
95
- <p className="font-body text-base text-tertiary">No testimonials available</p>
96
- </div>
97
- )}
86
+ </Carousel.Item>
87
+ );
88
+ })}
89
+ </CarouselSectionWrapper>
98
90
  </div>
99
91
  </section>
100
92
  );
@@ -9,6 +9,7 @@
9
9
  /* Aman color palette - warm, elegant tones */
10
10
  --color-bg-primary: #F9F7F0; /* Light warm beige */
11
11
  --color-bg-secondary: #F9F7F0;
12
+ --color-bg-primary_hover: #F0EDE4; /* Slightly darker beige on hover */
12
13
  --background-color-primary: #F9F7F0;
13
14
  --background-color-secondary: #F9F7F0;
14
15
 
@@ -22,9 +23,20 @@
22
23
  --color-fg-secondary: #505050;
23
24
  --color-fg-tertiary: #787878;
24
25
 
25
- /* Brand accent - elegant gold/bronze */
26
- --color-text-brand-accent: #B48C64;
26
+ /* Brand colors - elegant gold/bronze for primary buttons and accents */
27
+ --color-brand-500: #B48C64; /* Elegant gold/bronze */
28
+ --color-brand-600: #8C6440; /* Darker bronze for primary button background */
29
+ --color-brand-700: #6D4E32; /* Even darker for hover */
30
+ --color-fg-brand-primary: #8C6440;
31
+ --color-fg-brand-secondary: #B48C64;
27
32
  --color-text-brand-secondary: #8C6440;
33
+ --color-text-brand-accent: #B48C64; /* Used for links and accents in Aman components */
34
+
35
+ /* Button backgrounds - override semantic tokens */
36
+ --color-bg-brand-solid: #8C6440; /* Primary button (Contact, Send Message) */
37
+ --color-bg-brand-solid_hover: #6D4E32; /* Primary button hover */
38
+ --background-color-brand-solid: #8C6440;
39
+ --background-color-brand-solid_hover: #6D4E32;
28
40
 
29
41
  /* Border colors */
30
42
  --color-border-primary: #E5E2D9;
@@ -35,6 +35,7 @@ export interface CompanyInformation {
35
35
  support_email?: string;
36
36
  sales_email?: string;
37
37
  business_hours?: string;
38
+ external_management_url?: string;
38
39
  created_at: string;
39
40
  updated_at: string;
40
41
  photo_attachments?: PhotoAttachment[];