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,113 @@
1
+ "use client";
2
+
3
+ import { PhotoWithFallback } from '../elements';
4
+ import type { Testimonial } from '../../types/api/testimonial';
5
+ import { getAvatarUrl } from '../../utils/photo-helpers';
6
+
7
+ interface TestimonialsHomeProps {
8
+ config: {
9
+ pages?: any[];
10
+ };
11
+ testimonials?: Testimonial[] | null;
12
+ pageName?: string;
13
+ sectionKey?: string;
14
+ }
15
+
16
+ export const TestimonialsHome = ({
17
+ config,
18
+ testimonials: testimonialsData,
19
+ pageName = 'Home',
20
+ sectionKey = 'home_page_section_4_testimonials',
21
+ }: TestimonialsHomeProps) => {
22
+ const testimonials = Array.isArray(testimonialsData) ? testimonialsData : [];
23
+
24
+ const testimonialsSection = pageName === 'Home'
25
+ const title = 'What our clients say';
26
+
27
+ const perPage = undefined;
28
+ const displayTestimonials = perPage ? testimonials.slice(0, perPage) : testimonials.slice(0, 3);
29
+
30
+ return (
31
+ <section>
32
+ <div className="mx-auto max-w-container px-4 md:px-8">
33
+ <div className="mb-12 text-center">
34
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
35
+ {title}
36
+ </h2>
37
+ </div>
38
+
39
+ {displayTestimonials && displayTestimonials.length > 0 ? (
40
+ <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
41
+ {displayTestimonials.map((testimonial: any, i: number) => {
42
+ const quote = testimonial.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
43
+ const reviewerName = testimonial.reviewer_name || 'Customer';
44
+ const username = `@${reviewerName.toLowerCase().replace(/\s+/g, '')}`;
45
+ const avatarUrl = getAvatarUrl(testimonial.photo_attachments, testimonial.id, reviewerName);
46
+ const rating = testimonial.rating || 5;
47
+ const isVerified = testimonial.is_verified !== false;
48
+
49
+ return (
50
+ <div
51
+ key={testimonial.id || i}
52
+ className="bg-white p-8 flex flex-col"
53
+ >
54
+ <div className="flex gap-1 mb-6">
55
+ {Array.from({ length: 5 }).map((_, starIdx) => (
56
+ <svg
57
+ key={starIdx}
58
+ className="w-5 h-5"
59
+ style={{
60
+ fill: starIdx < rating ? "var(--color-text-brand-secondary)" : "none",
61
+ stroke: starIdx < rating ? "none" : "#E5E0D8"
62
+ }}
63
+ strokeWidth="2"
64
+ viewBox="0 0 24 24"
65
+ >
66
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
67
+ </svg>
68
+ ))}
69
+ </div>
70
+
71
+ <p className="font-display text-lg leading-relaxed text-tertiary mb-6 flex-grow">
72
+ "{quote}"
73
+ </p>
74
+
75
+ <div className="flex items-center gap-3">
76
+ <div className="w-12 h-12 rounded-full overflow-hidden flex-shrink-0">
77
+ <PhotoWithFallback
78
+ photoUrl={avatarUrl}
79
+ photoAlt={reviewerName}
80
+ fallbackId={testimonial.id || i}
81
+ className="w-full h-full object-cover"
82
+ />
83
+ </div>
84
+ <div className="flex-1">
85
+ <div className="flex items-center gap-1.5">
86
+ <p className="font-body text-sm font-medium text-fg-primary">
87
+ {reviewerName}
88
+ </p>
89
+ {isVerified && (
90
+ <svg className="w-4 h-4 text-fg-primary" viewBox="0 0 20 20" fill="currentColor">
91
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
92
+ </svg>
93
+ )}
94
+ </div>
95
+ <p className="font-body text-xs" style={{ color: 'var(--color-text-brand-secondary)' }}>{username}</p>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ );
100
+ })}
101
+ </div>
102
+ ) : (
103
+ <div className="text-center py-12">
104
+ <p className="font-body text-base text-tertiary">No testimonials available</p>
105
+ </div>
106
+ )}
107
+ </div>
108
+ </section>
109
+ );
110
+ };
111
+
112
+ import { registerThemeVariant } from '../../lib/component-registry';
113
+ registerThemeVariant('testimonials-home', 'aman', TestimonialsHome);
@@ -0,0 +1,90 @@
1
+ import { StarIcon, AvatarLabelGroup, VerifiedTick } from '../elements';
2
+ import { getAvatarUrl } from '../../utils/photo-helpers';
3
+ import type { Testimonial } from '../../types/api/testimonial';
4
+
5
+
6
+ interface TestimonialsHomeProps {
7
+ config: {
8
+ pages?: any[];
9
+ };
10
+ testimonials: Testimonial[];
11
+ pageName?: string;
12
+ sectionKey?: string;
13
+ }
14
+
15
+ export const TestimonialsHome = ({
16
+ config,
17
+ testimonials: testimonialsData,
18
+ pageName = 'Home',
19
+ sectionKey = 'home_page_section_4_testimonials',
20
+ }: TestimonialsHomeProps) => {
21
+ const testimonials = Array.isArray(testimonialsData) ? testimonialsData : [];
22
+
23
+ // Extract values from config
24
+ const testimonialsSection = pageName === 'Home'
25
+ const title = "What our customers say";
26
+ const subtitle = "";
27
+
28
+ const perPage = undefined;
29
+ const displayTestimonials = perPage ? testimonials.slice(0, perPage) : testimonials;
30
+
31
+ return (
32
+ <section className="flex flex-col items-center gap-16 bg-primary py-16 lg:py-24">
33
+ <div className="flex max-w-container flex-col items-center gap-4 px-4 text-center lg:gap-5 lg:px-8">
34
+ <h1 className="text-display-sm font-semibold text-primary lg:text-display-md">
35
+ {title}
36
+ </h1>
37
+ <p className="text-lg text-tertiary lg:text-xl">
38
+ {subtitle}
39
+ </p>
40
+ </div>
41
+
42
+ {displayTestimonials && displayTestimonials.length > 0 ? (
43
+ <div className="grid max-w-container grid-cols-1 gap-5 px-4 lg:grid-cols-3 lg:gap-6 lg:px-8">
44
+ {displayTestimonials.map((testimonial, index) => {
45
+ const reviewerName = testimonial.reviewer_name || 'Customer';
46
+ const quote = testimonial.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
47
+ const username = `@${reviewerName.toLowerCase().replace(/\s+/g, '')}`;
48
+ const avatarUrl = getAvatarUrl(testimonial.photo_attachments, testimonial.id, reviewerName);
49
+
50
+ return (
51
+ <div
52
+ key={testimonial.id || index}
53
+ className="flex flex-col items-start gap-8 rounded-xl bg-secondary p-6 lg:justify-between lg:p-8"
54
+ >
55
+ <div className="flex flex-col items-start gap-4">
56
+ <div aria-hidden="true" className="flex gap-1">
57
+ <StarIcon />
58
+ <StarIcon />
59
+ <StarIcon />
60
+ <StarIcon />
61
+ <StarIcon />
62
+ </div>
63
+ <blockquote className="text-md font-medium text-primary">{quote}</blockquote>
64
+ </div>
65
+ <a href="#" className="group flex outline-hidden">
66
+ <AvatarLabelGroup
67
+ size="lg"
68
+ src={avatarUrl}
69
+ alt={reviewerName}
70
+ title={
71
+ <span className="relative flex items-center gap-1">
72
+ {reviewerName}
73
+ <VerifiedTick size="lg" />
74
+ </span>
75
+ }
76
+ subtitle={<span className="underline decoration-1 underline-offset-4">{username}</span>}
77
+ />
78
+ </a>
79
+ </div>
80
+ );
81
+ })}
82
+ </div>
83
+ ) : (
84
+ <div className="text-center">
85
+ <p className="text-gray-500">No testimonials available</p>
86
+ </div>
87
+ )}
88
+ </section>
89
+ );
90
+ };
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import type { FC } from 'react';
4
+ import { mapIcon } from '../utils/icon-mapping';
5
+
6
+ interface ValuesSectionProps {
7
+ config: {
8
+ pages?: any[];
9
+ };
10
+ pageName?: string;
11
+ sectionKey?: string;
12
+ }
13
+
14
+ export const ValuesSection = ({
15
+ config,
16
+ pageName = 'About',
17
+ sectionKey = 'about_page_section_3_values',
18
+ }: ValuesSectionProps) => {
19
+ const title = 'Our Values';
20
+ const subtitle = '';
21
+ const values: any[] = [];
22
+
23
+ // Check if any icons are missing - if so, use stars for all
24
+ const anyIconMissing = values.some((value: any) => value.icon && !mapIcon(value.icon));
25
+ const useStarForAll = anyIconMissing;
26
+
27
+ return (
28
+ <section>
29
+ <div className="mx-auto max-w-container px-4 md:px-8">
30
+ <div className="mb-12 text-center">
31
+ {subtitle && (
32
+ <p className="text-xs font-body font-normal uppercase tracking-widest mb-4" style={{ color: 'var(--color-text-brand-secondary)' }}>
33
+ {subtitle}
34
+ </p>
35
+ )}
36
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
37
+ {title}
38
+ </h2>
39
+ </div>
40
+
41
+ <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
42
+ {values.map((value: any, i: number) => {
43
+ // Use star for all if any icon is missing, otherwise use the specified icon
44
+ const IconComponent = useStarForAll ? mapIcon('star') : (mapIcon(value.icon) || mapIcon('star'));
45
+
46
+ return (
47
+ <div key={i} className="text-center">
48
+ {IconComponent && (
49
+ <div className="mb-6 flex justify-center">
50
+ <div className="w-12 h-12 flex items-center justify-center text-fg-primary">
51
+ <IconComponent className="w-full h-full" />
52
+ </div>
53
+ </div>
54
+ )}
55
+ <h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
56
+ {value.title}
57
+ </h3>
58
+ {value.description && (
59
+ <p className="font-body text-base leading-relaxed text-tertiary">
60
+ {value.description}
61
+ </p>
62
+ )}
63
+ </div>
64
+ );
65
+ })}
66
+ </div>
67
+ </div>
68
+ </section>
69
+ );
70
+ };
71
+
72
+ import { registerThemeVariant } from '../../lib/component-registry';
73
+ registerThemeVariant('values-section', 'aman', ValuesSection);
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import React from 'react';
4
+ import type { FC } from 'react';
5
+ import { FeaturedIcon } from '../elements';
6
+ import { mapIcon } from '../utils/icon-mapping';
7
+
8
+ type FeaturedIconColor = "success" | "gray" | "brand" | "error" | "warning";
9
+
10
+
11
+ interface ValuesSectionProps {
12
+ // Config data - component extracts all values from it
13
+ config: {
14
+ pages?: any[];
15
+ };
16
+ // Optional: page name and section key (defaults to Home page)
17
+ pageName?: string;
18
+ sectionKey?: string;
19
+ }
20
+
21
+ export const ValuesSection = ({
22
+ config,
23
+ pageName = 'Home',
24
+ sectionKey = 'home_page_section_3_about',
25
+ }: ValuesSectionProps) => {
26
+ // Extract values from config
27
+ const aboutSection = pageName === 'Home'
28
+ const values = (undefined || []).map((value: any) => ({
29
+ title: value.title,
30
+ description: value.description,
31
+ icon: value.icon,
32
+ color: value.color,
33
+ }));
34
+ const hasValues = values.length > 0;
35
+
36
+ // Only show label if there are values or if explicitly provided
37
+ const label = hasValues
38
+ ? ("Our values")
39
+ : (undefined || null);
40
+ const title = "How we work";
41
+ const subtitle = "Our shared values keep us connected and guide us as one team.";
42
+ const description = undefined; // For sections without values
43
+
44
+ const backgroundColor = "bg-secondary";
45
+ const className = "";
46
+ return (
47
+ <section className={`${backgroundColor} py-16 md:py-24 ${className}`}>
48
+ <div className="mx-auto w-full max-w-container px-4 md:px-8">
49
+ <div className="mx-auto flex w-full max-w-3xl flex-col items-center text-center">
50
+ {label && (
51
+ <span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
52
+ )}
53
+ {title && (
54
+ <h2 className={`${label ? 'mt-3' : ''} text-display-sm font-semibold text-primary md:text-display-md`}>
55
+ {title}
56
+ </h2>
57
+ )}
58
+ {subtitle && (
59
+ <p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
60
+ {subtitle}
61
+ </p>
62
+ )}
63
+ {description && !hasValues && (
64
+ <p className="mt-4 text-md text-tertiary md:mt-6 md:text-lg">
65
+ {description}
66
+ </p>
67
+ )}
68
+ </div>
69
+
70
+ {values.length > 0 ? (
71
+ <div className="mt-12 md:mt-16">
72
+ {(() => {
73
+ // Check if any icons are missing - if so, use stars for all
74
+ const anyIconMissing = values.some((value: { icon?: string | FC<{ className?: string }> }) =>
75
+ typeof value.icon === 'string' && value.icon && !mapIcon(value.icon)
76
+ );
77
+ const useStarForAll = anyIconMissing;
78
+
79
+ return (
80
+ <ul className="grid w-full grid-cols-1 justify-items-center gap-x-8 gap-y-10 sm:grid-cols-2 md:gap-y-16 lg:grid-cols-3">
81
+ {values.map((value: { title: string; description: string; icon?: string | FC<{ className?: string }>; color?: FeaturedIconColor }, index: number) => {
82
+ // Use star for all if any icon is missing, otherwise use the specified icon
83
+ const IconComponent = useStarForAll ? mapIcon('star') : (mapIcon(value.icon) || mapIcon('star'));
84
+ if (!IconComponent) return null;
85
+
86
+ const IconWrapper = ({ className }: { className?: string }) => (
87
+ <div className={className}>
88
+ <IconComponent className="w-full h-full" />
89
+ </div>
90
+ );
91
+
92
+ return (
93
+ <li key={index}>
94
+ <div className="flex max-w-sm flex-col items-center gap-4 text-center">
95
+ <FeaturedIcon
96
+ icon={IconWrapper}
97
+ size="lg"
98
+ color={(value.color as FeaturedIconColor) || 'gray'}
99
+ theme="modern"
100
+ className="hidden md:inline-flex"
101
+ />
102
+ <FeaturedIcon
103
+ icon={IconWrapper}
104
+ size="md"
105
+ color={(value.color as FeaturedIconColor) || 'gray'}
106
+ theme="modern"
107
+ className="inline-flex md:hidden"
108
+ />
109
+
110
+ <div>
111
+ <h3 className="text-lg font-semibold text-primary">{value.title}</h3>
112
+ <p className="mt-1 text-md text-tertiary">{value.description}</p>
113
+ </div>
114
+ </div>
115
+ </li>
116
+ );
117
+ })}
118
+ </ul>
119
+ );
120
+ })()}
121
+ </div>
122
+ ) : null}
123
+ </div>
124
+ </section>
125
+ );
126
+ };
127
+
128
+
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { Heart, Star01, Users01, Clock, CheckCircle } from '@untitledui/icons';
3
+
4
+ /**
5
+ * Icon mapping utility for converting icon string names to icon components.
6
+ * Used across design system components to ensure consistent icon rendering.
7
+ */
8
+ export const iconMap: Record<string, React.FC<{ className?: string }>> = {
9
+ heart: Heart,
10
+ star: Star01,
11
+ users: Users01,
12
+ clock: Clock,
13
+ check: CheckCircle,
14
+ };
15
+
16
+ /**
17
+ * Maps an icon (string, component, or ReactNode) to an icon component.
18
+ * If it's already a component, returns it as-is.
19
+ * If it's a string, looks it up in the iconMap.
20
+ * If it's null or other ReactNode, returns undefined.
21
+ */
22
+ export function mapIcon(icon: string | React.FC<{ className?: string }> | React.ReactNode | undefined | null): React.FC<{ className?: string }> | undefined {
23
+ if (!icon) return undefined;
24
+ if (typeof icon === 'function') return icon;
25
+ if (typeof icon === 'string') return iconMap[icon.toLowerCase()];
26
+ return undefined; // For other ReactNode types
27
+ }
28
+
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Keystone Design System
2
+ // Main entry point for @keystone/design-system package
3
+
4
+ export * from './design_system/sections';
5
+ export * from './design_system/elements';
6
+ export * from './lib/hooks';
7
+ export * from './contexts';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Component Registry
3
+ * Runtime registry for component theme variants
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import { Theme } from '../themes';
10
+
11
+ type ComponentType = React.ComponentType<any>;
12
+
13
+ const registry = new Map<string, Map<string, ComponentType>>();
14
+
15
+ export function registerThemeVariant(
16
+ componentName: string,
17
+ theme: string,
18
+ component: ComponentType
19
+ ) {
20
+ if (!registry.has(componentName)) {
21
+ registry.set(componentName, new Map());
22
+ }
23
+ registry.get(componentName)!.set(theme, component);
24
+ }
25
+
26
+ export function getThemedComponent(
27
+ componentName: string,
28
+ theme: Theme = 'classic'
29
+ ): ComponentType {
30
+ const variants = registry.get(componentName);
31
+
32
+ if (!variants || variants.size === 0) {
33
+ throw new Error(`No theme variants registered for "${componentName}"`);
34
+ }
35
+
36
+ if (variants.has(theme)) {
37
+ return variants.get(theme)!;
38
+ }
39
+
40
+ if (theme === 'classic') {
41
+ throw new Error(`No classic variant for "${componentName}" - use base component`);
42
+ }
43
+
44
+ throw new Error(`No variant available for "${componentName}.${theme}"`);
45
+ }
46
+
47
+ export function getRegistry() {
48
+ const result: Record<string, string[]> = {};
49
+ registry.forEach((variants, name) => {
50
+ result[name] = Array.from(variants.keys());
51
+ });
52
+ return result;
53
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Utility hooks for client-side UI functionality
3
+ */
4
+
5
+ // Generic React hooks for UI functionality
6
+ export { useBreakpoint } from './use-breakpoint';
7
+ export { useClipboard } from './use-clipboard';
8
+ export { useResizeObserver } from './use-resize-observer';
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ const screens = {
6
+ sm: "640px",
7
+ md: "768px",
8
+ lg: "1024px",
9
+ xl: "1280px",
10
+ "2xl": "1536px",
11
+ };
12
+
13
+ /**
14
+ * Checks whether a particular Tailwind CSS viewport size applies.
15
+ *
16
+ * @param size The size to check, which must either be included in Tailwind CSS's
17
+ * list of default screen sizes, or added to the Tailwind CSS config file.
18
+ *
19
+ * @returns A boolean indicating whether the viewport size applies.
20
+ */
21
+ export const useBreakpoint = (size: "sm" | "md" | "lg" | "xl" | "2xl") => {
22
+ const [matches, setMatches] = useState(typeof window !== "undefined" ? window.matchMedia(`(min-width: ${screens[size]})`).matches : true);
23
+
24
+ useEffect(() => {
25
+ const breakpoint = window.matchMedia(`(min-width: ${screens[size]})`);
26
+
27
+ setMatches(breakpoint.matches);
28
+
29
+ const handleChange = (value: MediaQueryListEvent) => setMatches(value.matches);
30
+
31
+ breakpoint.addEventListener("change", handleChange);
32
+ return () => breakpoint.removeEventListener("change", handleChange);
33
+ }, [size]);
34
+
35
+ return matches;
36
+ };
37
+
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+
5
+ const DEFAULT_TIMEOUT = 2000;
6
+
7
+ type UseClipboardReturnType = {
8
+ /**
9
+ * The state indicating whether the text has been copied.
10
+ * If a string is provided, it will be used as the identifier for the copied state.
11
+ */
12
+ copied: string | boolean;
13
+ /**
14
+ * Function to copy text to the clipboard using the modern clipboard API.
15
+ * Falls back to the fallback function if the modern API fails.
16
+ *
17
+ * @param {string} text - The text to be copied.
18
+ * @param {string} [id] - Optional identifier to set the copied state.
19
+ * @returns {Promise<Object>} - A promise that resolves to an object containing:
20
+ * - `success` (boolean): Whether the copy operation was successful.
21
+ * - `error` (Error | undefined): The error object if the copy operation failed.
22
+ */
23
+ copy: (text: string, id?: string) => Promise<{ success: boolean; error?: Error }>;
24
+ };
25
+
26
+ /**
27
+ * Custom hook to copy text to the clipboard.
28
+ *
29
+ * @returns {UseClipboardReturnType} - An object containing the copied state and the copy function.
30
+ */
31
+ export const useClipboard = (): UseClipboardReturnType => {
32
+ const [copied, setCopied] = useState<string | boolean>(false);
33
+
34
+ // Fallback function for older browsers
35
+ const fallback = (text: string, id?: string) => {
36
+ try {
37
+ // Textarea to copy the text to the clipboard
38
+ const textArea = document.createElement("textarea");
39
+ textArea.value = text;
40
+ textArea.style.position = "absolute";
41
+ textArea.style.left = "-99999px";
42
+
43
+ document.body.appendChild(textArea);
44
+ textArea.select();
45
+
46
+ const success = document.execCommand("copy");
47
+ textArea.remove();
48
+
49
+ setCopied(id || true);
50
+ setTimeout(() => setCopied(false), DEFAULT_TIMEOUT);
51
+
52
+ return success ? { success: true } : { success: false, error: new Error("execCommand returned false") };
53
+ } catch (err) {
54
+ return {
55
+ success: false,
56
+ error: err instanceof Error ? err : new Error("Fallback copy failed"),
57
+ };
58
+ }
59
+ };
60
+
61
+ const copy = useCallback(async (text: string, id?: string) => {
62
+ if (navigator.clipboard && window.isSecureContext) {
63
+ try {
64
+ await navigator.clipboard.writeText(text);
65
+
66
+ setCopied(id || true);
67
+ setTimeout(() => setCopied(false), DEFAULT_TIMEOUT);
68
+
69
+ return { success: true };
70
+ } catch {
71
+ // If modern method fails, try fallback
72
+ return fallback(text, id);
73
+ }
74
+ }
75
+ return fallback(text);
76
+ }, []);
77
+
78
+ return { copied, copy };
79
+ };