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,111 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, Ref } from "react";
4
+ import React from "react";
5
+ import type { TextAreaProps as AriaTextAreaProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
6
+ import { TextArea as AriaTextArea, TextField as AriaTextField } from "react-aria-components";
7
+ import { HintText } from '../input/hint-text';
8
+ import { Label } from '../input/label';
9
+ import { cx } from '../../../utils/cx';
10
+
11
+ // Creates a data URL for an SVG resize handle with a given color.
12
+ const getResizeHandleBg = (color: string) => {
13
+ return `url(data:image/svg+xml;base64,${btoa(`<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 2L2 10" stroke="${color}" stroke-linecap="round"/><path d="M11 7L7 11" stroke="${color}" stroke-linecap="round"/></svg>`)})`;
14
+ };
15
+
16
+ interface TextAreaBaseProps extends AriaTextAreaProps {
17
+ ref?: Ref<HTMLTextAreaElement>;
18
+ }
19
+
20
+ export const TextAreaBase = ({ className, ...props }: TextAreaBaseProps) => {
21
+ return (
22
+ <AriaTextArea
23
+ {...props}
24
+ style={
25
+ {
26
+ "--resize-handle-bg": getResizeHandleBg("#D5D7DA"),
27
+ "--resize-handle-bg-dark": getResizeHandleBg("#373A41"),
28
+ } as React.CSSProperties
29
+ }
30
+ className={(state) =>
31
+ cx(
32
+ "w-full scroll-py-3 rounded-lg bg-primary px-3.5 py-3 text-md text-primary shadow-xs ring-1 ring-primary transition duration-100 ease-linear ring-inset placeholder:text-placeholder autofill:rounded-lg autofill:text-primary focus:outline-hidden",
33
+
34
+ // Resize handle
35
+ "[&::-webkit-resizer]:bg-(image:--resize-handle-bg) [&::-webkit-resizer]:bg-contain dark:[&::-webkit-resizer]:bg-(image:--resize-handle-bg-dark)",
36
+
37
+ state.isFocused && !state.isDisabled && "ring-2 ring-brand",
38
+ state.isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled ring-disabled",
39
+ state.isInvalid && "ring-error_subtle",
40
+ state.isInvalid && state.isFocused && "ring-2 ring-error",
41
+
42
+ typeof className === "function" ? className(state) : className,
43
+ )
44
+ }
45
+ />
46
+ );
47
+ };
48
+
49
+ TextAreaBase.displayName = "TextAreaBase";
50
+
51
+ interface TextFieldProps extends AriaTextFieldProps {
52
+ /** Label text for the textarea */
53
+ label?: string;
54
+ /** Helper text displayed below the textarea */
55
+ hint?: ReactNode;
56
+ /** Tooltip message displayed after the label. */
57
+ tooltip?: string;
58
+ /** Class name for the textarea wrapper */
59
+ textAreaClassName?: TextAreaBaseProps["className"];
60
+ /** Ref for the textarea wrapper */
61
+ ref?: Ref<HTMLDivElement>;
62
+ /** Ref for the textarea */
63
+ textAreaRef?: TextAreaBaseProps["ref"];
64
+ /** Whether to hide required indicator from label. */
65
+ hideRequiredIndicator?: boolean;
66
+ /** Placeholder text. */
67
+ placeholder?: string;
68
+ /** Visible height of textarea in rows . */
69
+ rows?: number;
70
+ /** Visible width of textarea in columns. */
71
+ cols?: number;
72
+ }
73
+
74
+ export const TextArea = ({
75
+ label,
76
+ hint,
77
+ tooltip,
78
+ textAreaRef,
79
+ hideRequiredIndicator,
80
+ textAreaClassName,
81
+ placeholder,
82
+ className,
83
+ rows,
84
+ cols,
85
+ ...props
86
+ }: TextFieldProps) => {
87
+ return (
88
+ <AriaTextField
89
+ {...props}
90
+ className={(state) =>
91
+ cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
92
+ }
93
+ >
94
+ {({ isInvalid, isRequired }) => (
95
+ <>
96
+ {label && (
97
+ <Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired} tooltip={tooltip}>
98
+ {label}
99
+ </Label>
100
+ )}
101
+
102
+ <TextAreaBase placeholder={placeholder} className={textAreaClassName} ref={textAreaRef} rows={rows} cols={cols} />
103
+
104
+ {hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
105
+ </>
106
+ )}
107
+ </AriaTextField>
108
+ );
109
+ };
110
+
111
+ TextArea.displayName = "TextArea";
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import type { SwitchProps as AriaSwitchProps } from "react-aria-components";
5
+ import { Switch as AriaSwitch } from "react-aria-components";
6
+ import { cx } from '../../../utils/cx';
7
+
8
+ interface ToggleBaseProps {
9
+ size?: "sm" | "md";
10
+ slim?: boolean;
11
+ className?: string;
12
+ isHovered?: boolean;
13
+ isFocusVisible?: boolean;
14
+ isSelected?: boolean;
15
+ isDisabled?: boolean;
16
+ }
17
+
18
+ export const ToggleBase = ({ className, isHovered, isDisabled, isFocusVisible, isSelected, slim, size = "sm" }: ToggleBaseProps) => {
19
+ const styles = {
20
+ default: {
21
+ sm: {
22
+ root: "h-5 w-9 p-0.5",
23
+ switch: cx("size-4", isSelected && "translate-x-4"),
24
+ },
25
+ md: {
26
+ root: "h-6 w-11 p-0.5",
27
+ switch: cx("size-5", isSelected && "translate-x-5"),
28
+ },
29
+ },
30
+ slim: {
31
+ sm: {
32
+ root: "h-4 w-8",
33
+ switch: cx("size-4", isSelected && "translate-x-4"),
34
+ },
35
+ md: {
36
+ root: "h-5 w-10",
37
+ switch: cx("size-5", isSelected && "translate-x-5"),
38
+ },
39
+ },
40
+ };
41
+
42
+ const classes = slim ? styles.slim[size] : styles.default[size];
43
+
44
+ return (
45
+ <div
46
+ className={cx(
47
+ "cursor-pointer rounded-full bg-tertiary outline-focus-ring transition duration-150 ease-linear",
48
+ isSelected && "bg-brand-solid",
49
+ isSelected && isHovered && "bg-brand-solid_hover",
50
+ isDisabled && "cursor-not-allowed bg-disabled",
51
+ isFocusVisible && "outline-2 outline-offset-2",
52
+
53
+ slim && "ring-1 ring-secondary ring-inset",
54
+ slim && isSelected && "ring-transparent",
55
+ classes.root,
56
+ className,
57
+ )}
58
+ >
59
+ <div
60
+ style={{
61
+ transition: "transform 0.15s ease-in-out, translate 0.15s ease-in-out, border-color 0.1s linear, background-color 0.1s linear",
62
+ }}
63
+ className={cx(
64
+ "rounded-full bg-fg-white shadow-sm",
65
+ isDisabled && "bg-toggle-button-fg_disabled",
66
+
67
+ slim && "shadow-xs",
68
+ slim && "border border-toggle-border",
69
+ slim && isSelected && "border-toggle-slim-border_pressed",
70
+ slim && isSelected && isHovered && "border-toggle-slim-border_pressed-hover",
71
+
72
+ classes.switch,
73
+ )}
74
+ />
75
+ </div>
76
+ );
77
+ };
78
+
79
+ interface ToggleProps extends AriaSwitchProps {
80
+ size?: "sm" | "md";
81
+ label?: string;
82
+ hint?: ReactNode;
83
+ slim?: boolean;
84
+ }
85
+
86
+ export const Toggle = ({ label, hint, className, size = "sm", slim, ...ariaSwitchProps }: ToggleProps) => {
87
+ const sizes = {
88
+ sm: {
89
+ root: "gap-2",
90
+ textWrapper: "",
91
+ label: "text-sm font-medium",
92
+ hint: "text-sm",
93
+ },
94
+ md: {
95
+ root: "gap-3",
96
+ textWrapper: "gap-0.5",
97
+ label: "text-md font-medium",
98
+ hint: "text-md",
99
+ },
100
+ };
101
+
102
+ return (
103
+ <AriaSwitch
104
+ {...ariaSwitchProps}
105
+ className={(renderProps) =>
106
+ cx(
107
+ "flex w-max items-start",
108
+ renderProps.isDisabled && "cursor-not-allowed",
109
+ sizes[size].root,
110
+ typeof className === "function" ? className(renderProps) : className,
111
+ )
112
+ }
113
+ >
114
+ {({ isSelected, isDisabled, isFocusVisible, isHovered }) => (
115
+ <>
116
+ <ToggleBase
117
+ slim={slim}
118
+ size={size}
119
+ isHovered={isHovered}
120
+ isDisabled={isDisabled}
121
+ isFocusVisible={isFocusVisible}
122
+ isSelected={isSelected}
123
+ className={slim ? "mt-0.5" : ""}
124
+ />
125
+
126
+ {(label || hint) && (
127
+ <div className={cx("flex flex-col", sizes[size].textWrapper)}>
128
+ {label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
129
+ {hint && (
130
+ <span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
131
+ {hint}
132
+ </span>
133
+ )}
134
+ </div>
135
+ )}
136
+ </>
137
+ )}
138
+ </AriaSwitch>
139
+ );
140
+ };
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import type {
5
+ ButtonProps as AriaButtonProps,
6
+ TooltipProps as AriaTooltipProps,
7
+ TooltipTriggerComponentProps as AriaTooltipTriggerComponentProps,
8
+ } from "react-aria-components";
9
+ import { Button as AriaButton, OverlayArrow as AriaOverlayArrow, Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "react-aria-components";
10
+ import { cx } from '../../../utils/cx';
11
+
12
+ interface TooltipProps extends AriaTooltipTriggerComponentProps, Omit<AriaTooltipProps, "children"> {
13
+ /**
14
+ * The title of the tooltip.
15
+ */
16
+ title: ReactNode;
17
+ /**
18
+ * The description of the tooltip.
19
+ */
20
+ description?: ReactNode;
21
+ /**
22
+ * Whether to show the arrow on the tooltip.
23
+ *
24
+ * @default false
25
+ */
26
+ arrow?: boolean;
27
+ /**
28
+ * Delay in milliseconds before the tooltip is shown.
29
+ *
30
+ * @default 300
31
+ */
32
+ delay?: number;
33
+ }
34
+
35
+ export const Tooltip = ({
36
+ title,
37
+ description,
38
+ children,
39
+ arrow = false,
40
+ delay = 300,
41
+ closeDelay = 0,
42
+ trigger,
43
+ isDisabled,
44
+ isOpen,
45
+ defaultOpen,
46
+ offset = 6,
47
+ crossOffset,
48
+ placement = "top",
49
+ onOpenChange,
50
+ ...tooltipProps
51
+ }: TooltipProps) => {
52
+ const isTopOrBottomLeft = ["top left", "top end", "bottom left", "bottom end"].includes(placement);
53
+ const isTopOrBottomRight = ["top right", "top start", "bottom right", "bottom start"].includes(placement);
54
+ // Set negative cross offset for left and right placement to visually balance the tooltip.
55
+ const calculatedCrossOffset = isTopOrBottomLeft ? -12 : isTopOrBottomRight ? 12 : 0;
56
+
57
+ return (
58
+ <AriaTooltipTrigger {...{ trigger, delay, closeDelay, isDisabled, isOpen, defaultOpen, onOpenChange }}>
59
+ {children}
60
+
61
+ <AriaTooltip
62
+ {...tooltipProps}
63
+ offset={offset}
64
+ placement={placement}
65
+ crossOffset={crossOffset ?? calculatedCrossOffset}
66
+ className={({ isEntering, isExiting }) => cx(isEntering && "ease-out animate-in", isExiting && "ease-in animate-out")}
67
+ >
68
+ {({ isEntering, isExiting }) => (
69
+ <div
70
+ className={cx(
71
+ "z-50 flex max-w-xs origin-(--trigger-anchor-point) flex-col items-start gap-1 rounded-lg bg-primary-solid px-3 shadow-lg will-change-transform",
72
+ description ? "py-3" : "py-2",
73
+
74
+ isEntering &&
75
+ "ease-out animate-in fade-in zoom-in-95 in-placement-left:slide-in-from-right-0.5 in-placement-right:slide-in-from-left-0.5 in-placement-top:slide-in-from-bottom-0.5 in-placement-bottom:slide-in-from-top-0.5",
76
+ isExiting &&
77
+ "ease-in animate-out fade-out zoom-out-95 in-placement-left:slide-out-to-right-0.5 in-placement-right:slide-out-to-left-0.5 in-placement-top:slide-out-to-bottom-0.5 in-placement-bottom:slide-out-to-top-0.5",
78
+ )}
79
+ >
80
+ <span className="text-xs font-semibold text-white">{title}</span>
81
+
82
+ {description && <span className="text-xs font-medium text-tooltip-supporting-text">{description}</span>}
83
+
84
+ {arrow && (
85
+ <AriaOverlayArrow>
86
+ <svg
87
+ viewBox="0 0 100 100"
88
+ className="size-2.5 fill-bg-primary-solid in-placement-left:-rotate-90 in-placement-right:rotate-90 in-placement-top:rotate-0 in-placement-bottom:rotate-180"
89
+ >
90
+ <path d="M0,0 L35.858,35.858 Q50,50 64.142,35.858 L100,0 Z" />
91
+ </svg>
92
+ </AriaOverlayArrow>
93
+ )}
94
+ </div>
95
+ )}
96
+ </AriaTooltip>
97
+ </AriaTooltipTrigger>
98
+ );
99
+ };
100
+
101
+ interface TooltipTriggerProps extends AriaButtonProps {}
102
+
103
+ export const TooltipTrigger = ({ children, className, ...buttonProps }: TooltipTriggerProps) => {
104
+ return (
105
+ <AriaButton {...buttonProps} className={(values) => cx("h-max w-max outline-hidden", typeof className === "function" ? className(values) : className)}>
106
+ {children}
107
+ </AriaButton>
108
+ );
109
+ };
@@ -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,68 @@
1
+ import { useEffect } from "react";
2
+ import type { RefObject } from "react";
3
+
4
+ /**
5
+ * Checks if the ResizeObserver API is supported.
6
+ * @returns True if the ResizeObserver API is supported, false otherwise.
7
+ */
8
+ function hasResizeObserver() {
9
+ return typeof window.ResizeObserver !== "undefined";
10
+ }
11
+
12
+ /**
13
+ * The options for the useResizeObserver hook.
14
+ */
15
+ type useResizeObserverOptionsType<T> = {
16
+ /**
17
+ * The ref to the element to observe.
18
+ */
19
+ ref: RefObject<T | undefined | null> | undefined;
20
+ /**
21
+ * The box to observe.
22
+ */
23
+ box?: ResizeObserverBoxOptions;
24
+ /**
25
+ * The callback function to call when the size changes.
26
+ */
27
+ onResize: () => void;
28
+ };
29
+
30
+ /**
31
+ * A hook that observes the size of an element and calls a callback function when the size changes.
32
+ * @param options - The options for the hook.
33
+ */
34
+ export function useResizeObserver<T extends Element>(options: useResizeObserverOptionsType<T>) {
35
+ const { ref, box, onResize } = options;
36
+
37
+ useEffect(() => {
38
+ const element = ref?.current;
39
+ if (!element) {
40
+ return;
41
+ }
42
+
43
+ if (!hasResizeObserver()) {
44
+ window.addEventListener("resize", onResize, false);
45
+
46
+ return () => {
47
+ window.removeEventListener("resize", onResize, false);
48
+ };
49
+ } else {
50
+ const resizeObserverInstance = new window.ResizeObserver((entries) => {
51
+ if (!entries.length) {
52
+ return;
53
+ }
54
+
55
+ onResize();
56
+ });
57
+
58
+ resizeObserverInstance.observe(element, { box });
59
+
60
+ return () => {
61
+ if (element) {
62
+ resizeObserverInstance.unobserve(element);
63
+ }
64
+ };
65
+ }
66
+ }, [onResize, ref, box]);
67
+ }
68
+
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import type { SVGProps } from "react";
4
+ import { useId } from "react";
5
+ import { cx } from '../../utils/cx';
6
+
7
+ export const KeystoneLogoMinimal = (props: SVGProps<SVGSVGElement>) => {
8
+ const id = useId();
9
+
10
+ return (
11
+ <svg viewBox="0 0 38 38" fill="none" {...props} className={cx("size-8 origin-center scale-[1.2]", props.className)}>
12
+ <g filter={`url(#filter0-${id})`}>
13
+ <g clipPath={`url(#clip0-${id})`}>
14
+ <path
15
+ d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
16
+ fill="white"
17
+ />
18
+ <path
19
+ d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
20
+ fill={`url(#paint0_linear-${id})`}
21
+ fillOpacity="0.2"
22
+ />
23
+ {/* Clean pistachio circle */}
24
+ <circle cx="19" cy="19" r="8" fill={`url(#paint1_linear-${id})`} />
25
+ </g>
26
+ <path
27
+ d="M3.1 14.8C3.1 12.5581 3.10008 10.8828 3.20866 9.55376C3.31715 8.22593 3.53345 7.25268 3.96105 6.41348C4.71845 4.92699 5.92699 3.71845 7.41348 2.96105C8.25268 2.53345 9.22593 2.31715 10.5538 2.20866C11.8828 2.10008 13.5581 2.1 15.8 2.1H22.2C24.4419 2.1 26.1172 2.10008 27.4462 2.20866C28.7741 2.31715 29.7473 2.53345 30.5865 2.96105C32.073 3.71845 33.2816 4.92699 34.039 6.41348C34.4665 7.25268 34.6828 8.22593 34.7913 9.55376C34.8999 10.8828 34.9 12.5581 34.9 14.8V21.2C34.9 23.4419 34.8999 25.1172 34.7913 26.4462C34.6828 27.7741 34.4665 28.7473 34.039 29.5865C33.2816 31.073 32.073 32.2816 30.5865 33.039C29.7473 33.4665 28.7741 33.6828 27.4462 33.7913C26.1172 33.8999 24.4419 33.9 22.2 33.9H15.8C13.5581 33.9 11.8828 33.8999 10.5538 33.7913C9.22593 33.6828 8.25268 33.4665 7.41348 33.039C5.92699 32.2816 4.71845 31.073 3.96105 29.5865C3.53345 28.7473 3.31715 27.7741 3.20866 26.4462C3.10008 25.1172 3.1 23.4419 3.1 21.2V14.8Z"
28
+ stroke="#0A0D12"
29
+ strokeOpacity="0.12"
30
+ strokeWidth="0.2"
31
+ />
32
+ </g>
33
+
34
+ <defs>
35
+ <filter id={`filter0-${id}`} x="0" y="0" width="38" height="38" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
36
+ <feFlood floodOpacity="0" result="BackgroundImageFix" />
37
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
38
+ <feOffset dy="1" />
39
+ <feGaussianBlur stdDeviation="1" />
40
+ <feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.06 0" />
41
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
42
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
43
+ <feOffset dy="1" />
44
+ <feGaussianBlur stdDeviation="1.5" />
45
+ <feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
46
+ <feBlend mode="normal" in2="effect2_dropShadow" result="effect2_dropShadow" />
47
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
48
+ <feMorphology radius="0.5" operator="erode" in="SourceAlpha" result="effect3_dropShadow" />
49
+ <feOffset dy="1" />
50
+ <feGaussianBlur stdDeviation="0.5" />
51
+ <feComposite in2="hardAlpha" operator="out" />
52
+ <feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.13 0" />
53
+ <feBlend mode="normal" in2="effect2_dropShadow" result="effect3_dropShadow" />
54
+ <feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow" result="shape" />
55
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
56
+ <feOffset dy="-0.5" />
57
+ <feGaussianBlur stdDeviation="0.25" />
58
+ <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
59
+ <feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
60
+ <feBlend mode="normal" in2="shape" result="effect4_innerShadow" />
61
+ </filter>
62
+ <filter id={`filter1_dd-${id}`} x="8" y="8" width="22" height="22" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
63
+ <feFlood floodOpacity="0" result="BackgroundImageFix" />
64
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
65
+ <feOffset dy="1" />
66
+ <feGaussianBlur stdDeviation="1" />
67
+ <feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.06 0" />
68
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
69
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
70
+ <feOffset dy="1" />
71
+ <feGaussianBlur stdDeviation="1.5" />
72
+ <feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
73
+ <feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
74
+ <feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape" />
75
+ </filter>
76
+ <linearGradient id={`paint0_linear-${id}`} x1="19" y1="2" x2="19" y2="34" gradientUnits="userSpaceOnUse">
77
+ <stop stopColor="white" />
78
+ <stop offset="1" stopColor="#0A0D12" />
79
+ </linearGradient>
80
+ <linearGradient id={`paint1_linear-${id}`} x1="15" y1="26" x2="23" y2="10" gradientUnits="userSpaceOnUse">
81
+ <stop stopColor="#66D674" />
82
+ <stop offset="1" stopColor="#42D674" />
83
+ </linearGradient>
84
+ <clipPath id={`clip0-${id}`}>
85
+ <path
86
+ d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
87
+ fill="white"
88
+ />
89
+ </clipPath>
90
+ </defs>
91
+ </svg>
92
+ );
93
+ };
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import type { HTMLAttributes } from "react";
4
+ import { cx } from '../../utils/cx';
5
+ import { KeystoneLogoMinimal } from "./keystone-logo-minimal";
6
+
7
+ export const KeystoneLogo = (props: HTMLAttributes<HTMLOrSVGElement>) => {
8
+ return (
9
+ <div {...props} className={cx("flex h-8 w-max items-center justify-start overflow-visible", props.className)}>
10
+ {/* Minimal logo */}
11
+ <KeystoneLogoMinimal className="aspect-square h-full w-auto shrink-0" />
12
+
13
+ {/* Gap that adjusts to the height of the container */}
14
+ <div className="aspect-[0.4] h-full" />
15
+
16
+ {/* Clean text logo */}
17
+ <div className="flex items-center">
18
+ <span className="text-lg font-semibold text-fg-primary">Keystone</span>
19
+ </div>
20
+ </div>
21
+ );
22
+ };
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { PhotoWithFallback } from '../elements';
4
+ import type { CompanyInformation } from '../../types/api/company-information';
5
+ import type { WebsitePhotos } from '../../types/api/website-photos';
6
+
7
+ interface AboutHomeProps {
8
+ config?: any;
9
+ companyInformation?: CompanyInformation | null;
10
+ websitePhotos?: WebsitePhotos | null;
11
+ title?: string;
12
+ subtitle?: string;
13
+ ctaText?: string;
14
+ }
15
+
16
+ export const AboutHome = ({
17
+ config,
18
+ companyInformation,
19
+ websitePhotos,
20
+ title,
21
+ subtitle,
22
+ ctaText,
23
+ }: AboutHomeProps) => {
24
+ // Get section config
25
+ const homePageConfig = config?.pages?.find((p: any) =>
26
+ p.library_reference_name === 'Home' || p.slug === 'home'
27
+ );
28
+ const aboutSection = homePageConfig?.sections?.home_page_section_3_about;
29
+
30
+ // Use config values, then props, then defaults
31
+ const displayTitle = title || aboutSection?.title || '*TR* About Us';
32
+ const displaySubtitle = subtitle || aboutSection?.subtitle || '*TR* Learn more about our company';
33
+ const displayCtaText = ctaText || aboutSection?.cta_text || 'Learn more';
34
+
35
+ const description = (companyInformation as any)?.about_text_markdown || aboutSection?.description || '';
36
+
37
+ // Use brand photo for about section, fallback to about photo
38
+ const aboutImage = (websitePhotos as any)?.brand || websitePhotos?.about;
39
+
40
+ return (
41
+ <section>
42
+ <div className="mx-auto grid max-w-container grid-cols-1 gap-12 px-4 md:px-8 lg:grid-cols-2 lg:gap-16">
43
+ <div className="order-2 lg:order-1">
44
+ <PhotoWithFallback
45
+ photoUrl={aboutImage?.url || ''}
46
+ photoAlt={aboutImage?.alt || 'About'}
47
+ fallbackId="about-aman"
48
+ className="w-full h-full object-cover min-h-[300px] md:min-h-[400px]"
49
+ />
50
+ </div>
51
+
52
+ <div className="order-1 lg:order-2 flex flex-col justify-center">
53
+ <h2 className="font-display text-4xl font-normal leading-tight text-fg-primary md:text-5xl">
54
+ {displayTitle}
55
+ </h2>
56
+
57
+ {displaySubtitle && (
58
+ <p className="mt-4 font-body text-md text-tertiary uppercase tracking-wide">
59
+ {displaySubtitle}
60
+ </p>
61
+ )}
62
+
63
+ {description && (
64
+ <p className="mt-6 font-body text-lg leading-relaxed text-tertiary">
65
+ {description}
66
+ </p>
67
+ )}
68
+
69
+ {displayCtaText && (
70
+ <a
71
+ href="/about"
72
+ className="mt-8 inline-block font-body text-base underline underline-offset-4 hover:no-underline w-fit transition-colors"
73
+ style={{ color: 'var(--color-text-brand-accent)' }}
74
+ >
75
+ {displayCtaText}
76
+ </a>
77
+ )}
78
+ </div>
79
+ </div>
80
+ </section>
81
+ );
82
+ };
83
+
84
+ import { registerThemeVariant } from '../../lib/component-registry';
85
+ registerThemeVariant('about-home', 'aman', AboutHome);