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,34 @@
1
+ "use client";
2
+
3
+ import { Plus } from "@untitledui/icons";
4
+ import type { ButtonProps as AriaButtonProps } from "react-aria-components";
5
+ import { Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from '../../tooltip/tooltip';
6
+ import { cx } from '../../../../utils/cx';
7
+
8
+ const sizes = {
9
+ xs: { root: "size-6", icon: "size-4" },
10
+ sm: { root: "size-8", icon: "size-4" },
11
+ md: { root: "size-10", icon: "size-5" },
12
+ };
13
+
14
+ interface AvatarAddButtonProps extends AriaButtonProps {
15
+ size: "xs" | "sm" | "md";
16
+ title?: string;
17
+ className?: string;
18
+ }
19
+
20
+ export const AvatarAddButton = ({ size, className, title = "Add user", ...props }: AvatarAddButtonProps) => (
21
+ <AriaTooltip title={title}>
22
+ <AriaTooltipTrigger
23
+ {...props}
24
+ aria-label={title}
25
+ className={cx(
26
+ "flex cursor-pointer items-center justify-center rounded-full border border-dashed border-primary bg-primary text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 disabled:border-gray-200 disabled:bg-secondary disabled:text-gray-200",
27
+ sizes[size].root,
28
+ className,
29
+ )}
30
+ >
31
+ <Plus className={cx("text-current transition-inherit-all", sizes[size].icon)} />
32
+ </AriaTooltipTrigger>
33
+ </AriaTooltip>
34
+ );
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { cx } from '../../../../utils/cx';
4
+
5
+ const sizes = {
6
+ xs: "size-2",
7
+ sm: "size-3",
8
+ md: "size-3.5",
9
+ lg: "size-4",
10
+ xl: "size-4.5",
11
+ "2xl": "size-5 ring-[1.67px]",
12
+ };
13
+
14
+ interface AvatarCompanyIconProps {
15
+ size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
16
+ src: string;
17
+ alt?: string;
18
+ }
19
+
20
+ export const AvatarCompanyIcon = ({ size, src, alt }: AvatarCompanyIconProps) => (
21
+ <img
22
+ src={src}
23
+ alt={alt}
24
+ className={cx("bg-primary-25 absolute -right-0.5 -bottom-0.5 rounded-full object-cover ring-[1.5px] ring-bg-primary", sizes[size])}
25
+ />
26
+ );
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { cx } from '../../../../utils/cx';
4
+
5
+ const sizes = {
6
+ xs: "size-1.5",
7
+ sm: "size-2",
8
+ md: "size-2.5",
9
+ lg: "size-3",
10
+ xl: "size-3.5",
11
+ "2xl": "size-4",
12
+ "3xl": "size-4.5",
13
+ "4xl": "size-5",
14
+ };
15
+
16
+ interface AvatarOnlineIndicatorProps {
17
+ size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
18
+ status: "online" | "offline";
19
+ className?: string;
20
+ }
21
+
22
+ export const AvatarOnlineIndicator = ({ size, status, className }: AvatarOnlineIndicatorProps) => (
23
+ <span
24
+ className={cx(
25
+ "absolute right-0 bottom-0 rounded-full ring-[1.5px] ring-bg-primary",
26
+ status === "online" ? "bg-fg-success-secondary" : "bg-fg-disabled_subtle",
27
+ sizes[size],
28
+ className,
29
+ )}
30
+ />
31
+ );
@@ -0,0 +1,4 @@
1
+ export * from "./avatar-add-button";
2
+ export * from "./avatar-company-icon";
3
+ export * from "./avatar-online-indicator";
4
+ export * from "./verified-tick";
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { cx } from '../../../../utils/cx';
4
+
5
+ const sizes = {
6
+ xs: { root: "size-2.5", tick: "size-[4.38px" },
7
+ sm: { root: "size-3", tick: "size-[5.25px]" },
8
+ md: { root: "size-3.5", tick: "size-[6.13px]" },
9
+ lg: { root: "size-4", tick: "size-[7px]" },
10
+ xl: { root: "size-4.5", tick: "size-[7.88px]" },
11
+ "2xl": { root: "size-5", tick: "size-[8.75px]" },
12
+ "3xl": { root: "size-6", tick: "size-[10.5px]" },
13
+ "4xl": { root: "size-8", tick: "size-[14px]" },
14
+ };
15
+
16
+ interface VerifiedTickProps {
17
+ size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
18
+ className?: string;
19
+ }
20
+
21
+ export const VerifiedTick = ({ size, className }: VerifiedTickProps) => (
22
+ <svg data-verified className={cx("z-10 text-utility-blue-500", sizes[size].root, className)} viewBox="0 0 10 10" fill="none">
23
+ <path
24
+ d="M7.72237 1.77098C7.81734 2.00068 7.99965 2.18326 8.2292 2.27858L9.03413 2.61199C9.26384 2.70714 9.44635 2.88965 9.5415 3.11936C9.63665 3.34908 9.63665 3.60718 9.5415 3.83689L9.20833 4.64125C9.11313 4.87106 9.113 5.12943 9.20863 5.35913L9.54122 6.16325C9.58839 6.27702 9.61268 6.39897 9.6127 6.52214C9.61272 6.6453 9.58847 6.76726 9.54134 6.88105C9.4942 6.99484 9.42511 7.09823 9.33801 7.18531C9.2509 7.27238 9.14749 7.34144 9.03369 7.38854L8.22934 7.72171C7.99964 7.81669 7.81706 7.99899 7.72174 8.22855L7.38833 9.03348C7.29318 9.26319 7.11067 9.4457 6.88096 9.54085C6.65124 9.636 6.39314 9.636 6.16343 9.54085L5.35907 9.20767C5.12935 9.11276 4.87134 9.11295 4.64177 9.20821L3.83684 9.54115C3.60725 9.63608 3.34937 9.636 3.11984 9.54092C2.89032 9.44585 2.70791 9.26356 2.6127 9.03409L2.27918 8.22892C2.18421 7.99923 2.0019 7.81665 1.77235 7.72133L0.967421 7.38792C0.737807 7.29281 0.555355 7.11041 0.460169 6.88083C0.364983 6.65125 0.364854 6.39327 0.45981 6.16359L0.792984 5.35924C0.8879 5.12952 0.887707 4.87151 0.792445 4.64193L0.459749 3.83642C0.41258 3.72265 0.388291 3.60069 0.388272 3.47753C0.388252 3.35436 0.412501 3.2324 0.459634 3.11861C0.506767 3.00482 0.57586 2.90144 0.662965 2.81436C0.75007 2.72728 0.853479 2.65822 0.967283 2.61113L1.77164 2.27795C2.00113 2.18306 2.1836 2.00099 2.27899 1.7717L2.6124 0.966768C2.70755 0.737054 2.89006 0.554547 3.11978 0.459397C3.34949 0.364246 3.60759 0.364246 3.83731 0.459397L4.64166 0.792571C4.87138 0.887487 5.12939 0.887293 5.35897 0.792031L6.16424 0.459913C6.39392 0.364816 6.65197 0.364836 6.88164 0.459968C7.11131 0.555099 7.29379 0.737554 7.38895 0.967208L7.72247 1.77238L7.72237 1.77098Z"
25
+ className="fill-current"
26
+ />
27
+ <path
28
+ fillRule="evenodd"
29
+ clipRule="evenodd"
30
+ d="M6.95829 3.68932C7.02509 3.58439 7.04747 3.45723 7.02051 3.3358C6.99356 3.21437 6.91946 3.10862 6.81454 3.04182C6.70961 2.97502 6.58245 2.95264 6.46102 2.97959C6.33959 3.00655 6.23384 3.08064 6.16704 3.18557L4.33141 6.06995L3.49141 5.01995C3.41375 4.92281 3.30069 4.8605 3.17709 4.84673C3.05349 4.83296 2.92949 4.86885 2.83235 4.94651C2.73522 5.02417 2.67291 5.13723 2.65914 5.26083C2.64536 5.38443 2.68125 5.50843 2.75891 5.60557L4.00891 7.16807C4.0555 7.22638 4.11533 7.27271 4.18344 7.30323C4.25154 7.33375 4.32595 7.34757 4.40047 7.34353C4.47499 7.3395 4.54747 7.31773 4.61188 7.28004C4.67629 7.24234 4.73077 7.18981 4.77079 7.12682L6.95829 3.68932Z"
31
+ fill="white"
32
+ />
33
+ </svg>
34
+ );
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Extracts the initials from a full name.
3
+ *
4
+ * @param name - The full name from which to extract initials.
5
+ * @returns The initials of the provided name. If the name contains only one word,
6
+ * it returns the first character of that word. If the name contains two words,
7
+ * it returns the first character of each word.
8
+ */
9
+ export const getInitials = (name: string) => {
10
+ const [firstName, lastName] = name.split(" ");
11
+ return firstName.charAt(0) + (lastName ? lastName.charAt(0) : "");
12
+ };
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { type FC, type ReactNode, useState } from "react";
4
+ import { User01 } from "@untitledui/icons";
5
+ import { cx } from '../../../utils/cx';
6
+ import { AvatarOnlineIndicator, VerifiedTick } from '../avatar/base-components';
7
+
8
+ type AvatarSize = "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
9
+
10
+ export interface AvatarProps {
11
+ size?: AvatarSize;
12
+ className?: string;
13
+ src?: string | null;
14
+ alt?: string;
15
+ /**
16
+ * Display a contrast border around the avatar.
17
+ */
18
+ contrastBorder?: boolean;
19
+ /**
20
+ * Display a badge (i.e. company logo).
21
+ */
22
+ badge?: ReactNode;
23
+ /**
24
+ * Display a status indicator.
25
+ */
26
+ status?: "online" | "offline";
27
+ /**
28
+ * Display a verified tick icon.
29
+ *
30
+ * @default false
31
+ */
32
+ verified?: boolean;
33
+
34
+ /**
35
+ * The initials of the user to display if no image is available.
36
+ */
37
+ initials?: string;
38
+ /**
39
+ * An icon to display if no image is available.
40
+ */
41
+ placeholderIcon?: FC<{ className?: string }>;
42
+ /**
43
+ * A placeholder to display if no image is available.
44
+ */
45
+ placeholder?: ReactNode;
46
+
47
+ /**
48
+ * Whether the avatar should show a focus ring when the parent group is in focus.
49
+ * For example, when the avatar is wrapped inside a link.
50
+ *
51
+ * @default false
52
+ */
53
+ focusable?: boolean;
54
+ }
55
+
56
+ const styles = {
57
+ xxs: { root: "size-4 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-3" },
58
+ xs: { root: "size-6 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-4" },
59
+ sm: { root: "size-8 outline-[0.75px] -outline-offset-[0.75px]", initials: "text-sm font-semibold", icon: "size-5" },
60
+ md: { root: "size-10 outline-1 -outline-offset-1", initials: "text-md font-semibold", icon: "size-6" },
61
+ lg: { root: "size-12 outline-1 -outline-offset-1", initials: "text-lg font-semibold", icon: "size-7" },
62
+ xl: { root: "size-14 outline-1 -outline-offset-1", initials: "text-xl font-semibold", icon: "size-8" },
63
+ "2xl": { root: "size-16 outline-1 -outline-offset-1", initials: "text-display-xs font-semibold", icon: "size-8" },
64
+ };
65
+
66
+ export const Avatar = ({
67
+ contrastBorder = true,
68
+ size = "md",
69
+ src,
70
+ alt,
71
+ initials,
72
+ placeholder,
73
+ placeholderIcon: PlaceholderIcon,
74
+ badge,
75
+ status,
76
+ verified,
77
+ focusable = false,
78
+ className,
79
+ }: AvatarProps) => {
80
+ const [isFailed, setIsFailed] = useState(false);
81
+
82
+ const renderMainContent = () => {
83
+ if (src && !isFailed) {
84
+ return <img data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
85
+ }
86
+
87
+ if (initials) {
88
+ return <span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>;
89
+ }
90
+
91
+ if (PlaceholderIcon) {
92
+ return <PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />;
93
+ }
94
+
95
+ return placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />;
96
+ };
97
+
98
+ const renderBadgeContent = () => {
99
+ if (status) {
100
+ return <AvatarOnlineIndicator status={status} size={size === "xxs" ? "xs" : size} />;
101
+ }
102
+
103
+ if (verified) {
104
+ return (
105
+ <VerifiedTick
106
+ size={size === "xxs" ? "xs" : size}
107
+ className={cx("absolute right-0 bottom-0", (size === "xxs" || size === "xs") && "-right-px -bottom-px")}
108
+ />
109
+ );
110
+ }
111
+
112
+ return badge;
113
+ };
114
+
115
+ return (
116
+ <div
117
+ data-avatar
118
+ className={cx(
119
+ "relative inline-flex shrink-0 items-center justify-center rounded-full bg-avatar-bg outline-transparent",
120
+ // Focus styles
121
+ focusable && "group-outline-focus-ring group-focus-visible:outline-2 group-focus-visible:outline-offset-2",
122
+ contrastBorder && "outline outline-avatar-contrast-border",
123
+ styles[size].root,
124
+ className,
125
+ )}
126
+ >
127
+ {renderMainContent()}
128
+ {renderBadgeContent()}
129
+ </div>
130
+ );
131
+ };
132
+
@@ -0,0 +1,176 @@
1
+ "use client";
2
+
3
+ import type { FC, ReactNode } from "react";
4
+ import { isValidElement } from "react";
5
+ import { ArrowRight } from "@untitledui/icons";
6
+ import { cx, sortCx } from '../../../utils/cx';
7
+ import { isReactComponent } from '../../../utils/is-react-component';
8
+
9
+ type Size = "md" | "lg";
10
+ type Color = "brand" | "warning" | "error" | "gray" | "success";
11
+ type Theme = "light" | "modern";
12
+ type Align = "leading" | "trailing";
13
+
14
+ const baseClasses: Record<Theme, { root?: string; addon?: string; icon?: string }> = {
15
+ light: {
16
+ root: "rounded-full ring-1 ring-inset",
17
+ addon: "rounded-full ring-1 ring-inset",
18
+ },
19
+ modern: {
20
+ root: "rounded-[10px] bg-primary text-secondary shadow-xs ring-1 ring-inset ring-primary hover:bg-secondary",
21
+ addon: "flex items-center rounded-md bg-primary shadow-xs ring-1 ring-inset ring-primary",
22
+ icon: "text-utility-gray-500",
23
+ },
24
+ };
25
+
26
+ const getSizeClasses = (
27
+ theme?: Theme,
28
+ text?: boolean,
29
+ icon?: boolean,
30
+ ): Record<Align, Record<Size, { root?: string; addon?: string; icon?: string; dot?: string }>> => ({
31
+ leading: {
32
+ md: {
33
+ root: cx("py-1 pr-2 pl-1 text-xs font-medium", !text && !icon && "pr-1"),
34
+ addon: cx("px-2 py-0.5", theme === "modern" && "gap-1 px-1.5", text && "mr-2"),
35
+ icon: "ml-1 size-4",
36
+ },
37
+ lg: {
38
+ root: cx("py-1 pr-2 pl-1 text-sm font-medium", !text && !icon && "pr-1"),
39
+ addon: cx("px-2.5 py-0.5", theme === "modern" && "gap-1.5 px-2", text && "mr-2"),
40
+ icon: "ml-1 size-4",
41
+ },
42
+ },
43
+ trailing: {
44
+ md: {
45
+ root: cx("py-1 pr-1 pl-3 text-xs font-medium", theme === "modern" && "pl-2.5"),
46
+ addon: cx("py-0.5 pr-1.5 pl-2", theme === "modern" && "pr-1.5 pl-2", text && "ml-2"),
47
+ icon: "ml-0.5 size-3 stroke-[3px]",
48
+ dot: "mr-1.5",
49
+ },
50
+ lg: {
51
+ root: "py-1 pr-1 pl-3 text-sm font-medium",
52
+ addon: cx("py-0.5 pr-2 pl-2.5", theme === "modern" && "pr-1.5 pl-2", text && "ml-2"),
53
+ icon: "ml-1 size-3 stroke-[3px]",
54
+ dot: "mr-2",
55
+ },
56
+ },
57
+ });
58
+
59
+ const colorClasses: Record<Theme, Record<Color, { root?: string; addon?: string; icon?: string; dot?: string }>> = sortCx({
60
+ light: {
61
+ brand: {
62
+ root: "bg-utility-brand-50 text-utility-brand-700 ring-utility-brand-200 hover:bg-utility-brand-100",
63
+ addon: "bg-primary text-current ring-utility-brand-200",
64
+ icon: "text-utility-brand-500",
65
+ },
66
+ gray: {
67
+ root: "bg-utility-gray-50 text-utility-gray-700 ring-utility-gray-200 hover:bg-utility-gray-100",
68
+ addon: "bg-primary text-current ring-utility-gray-200",
69
+ icon: "text-utility-gray-500",
70
+ },
71
+ error: {
72
+ root: "bg-utility-error-50 text-utility-error-700 ring-utility-error-200 hover:bg-utility-error-100",
73
+ addon: "bg-primary text-current ring-utility-error-200",
74
+ icon: "text-utility-error-500",
75
+ },
76
+ warning: {
77
+ root: "bg-utility-warning-50 text-utility-warning-700 ring-utility-warning-200 hover:bg-utility-warning-100",
78
+ addon: "bg-primary text-current ring-utility-warning-200",
79
+ icon: "text-utility-warning-500",
80
+ },
81
+ success: {
82
+ root: "bg-utility-success-50 text-utility-success-700 ring-utility-success-200 hover:bg-utility-success-100",
83
+ addon: "bg-primary text-current ring-utility-success-200",
84
+ icon: "text-utility-success-500",
85
+ },
86
+ },
87
+ modern: {
88
+ brand: {
89
+ dot: "bg-utility-brand-500 outline-3 -outline-offset-1 outline-utility-brand-100",
90
+ },
91
+ gray: {
92
+ dot: "bg-utility-gray-500 outline-3 -outline-offset-1 outline-utility-gray-100",
93
+ },
94
+ error: {
95
+ dot: "bg-utility-error-500 outline-3 -outline-offset-1 outline-utility-error-100",
96
+ },
97
+ warning: {
98
+ dot: "bg-utility-warning-500 outline-3 -outline-offset-1 outline-utility-warning-100",
99
+ },
100
+ success: {
101
+ dot: "bg-utility-success-500 outline-3 -outline-offset-1 outline-utility-success-100",
102
+ },
103
+ },
104
+ });
105
+
106
+ interface BadgeGroupProps {
107
+ children?: string | ReactNode;
108
+ addonText: string;
109
+ size?: Size;
110
+ color: Color;
111
+ theme?: Theme;
112
+ /**
113
+ * Alignment of the badge addon element.
114
+ */
115
+ align?: Align;
116
+ iconTrailing?: FC<{ className?: string }> | ReactNode;
117
+ className?: string;
118
+ }
119
+
120
+ export const BadgeGroup = ({
121
+ children,
122
+ addonText,
123
+ size = "md",
124
+ color = "brand",
125
+ theme = "light",
126
+ align = "leading",
127
+ className,
128
+ iconTrailing: IconTrailing = ArrowRight,
129
+ }: BadgeGroupProps) => {
130
+ const colors = colorClasses[theme][color];
131
+ const sizes = getSizeClasses(theme, !!children, !!IconTrailing)[align][size];
132
+
133
+ const rootClasses = cx(
134
+ "inline-flex w-max cursor-pointer items-center transition duration-100 ease-linear",
135
+ baseClasses[theme].root,
136
+ sizes.root,
137
+ colors.root,
138
+ className,
139
+ );
140
+ const addonClasses = cx("inline-flex items-center", baseClasses[theme].addon, sizes.addon, colors.addon);
141
+ const dotClasses = cx("inline-block size-2 shrink-0 rounded-full", sizes.dot, colors.dot);
142
+ const iconClasses = cx(baseClasses[theme].icon, sizes.icon, colors.icon);
143
+
144
+ if (align === "trailing") {
145
+ return (
146
+ <div className={rootClasses}>
147
+ {theme === "modern" && <span className={dotClasses} />}
148
+
149
+ {children}
150
+
151
+ <span className={addonClasses}>
152
+ {addonText}
153
+
154
+ {/* Trailing icon */}
155
+ {isReactComponent(IconTrailing) && <IconTrailing className={iconClasses} />}
156
+ {isValidElement(IconTrailing) && IconTrailing}
157
+ </span>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ return (
163
+ <div className={rootClasses}>
164
+ <span className={addonClasses}>
165
+ {theme === "modern" && <span className={dotClasses} />}
166
+ {addonText}
167
+ </span>
168
+
169
+ {children}
170
+
171
+ {/* Trailing icon */}
172
+ {isReactComponent(IconTrailing) && <IconTrailing className={iconClasses} />}
173
+ {isValidElement(IconTrailing) && IconTrailing}
174
+ </div>
175
+ );
176
+ };