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
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Keystone Design Bootstrap
2
+
3
+ A comprehensive design system for Keystone customer websites. Provides themed sections, elements, and utilities for building consistent, server-rendered Next.js sites.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @keystone-pzjr/design-bootstrap
9
+ ```
10
+
11
+ ## Package Exports
12
+
13
+ ```typescript
14
+ // Sections (hero, footer, header, testimonials, etc.)
15
+ import { HeroHome, FooterHome, TestimonialsHome } from '@keystone-pzjr/design-bootstrap/sections'
16
+
17
+ // Elements (buttons, inputs, carousels, etc.)
18
+ import { Button, Input, Carousel } from '@keystone-pzjr/design-bootstrap/elements'
19
+
20
+ // Hooks
21
+ import { useBreakpoint } from '@keystone-pzjr/design-bootstrap/hooks'
22
+
23
+ // Theme context
24
+ import { ThemeProvider } from '@keystone-pzjr/design-bootstrap/contexts'
25
+
26
+ // Server API utilities
27
+ import { getServices, getTestimonials } from '@keystone-pzjr/design-bootstrap/lib/server-api'
28
+
29
+ // Types
30
+ import type { Service, Testimonial } from '@keystone-pzjr/design-bootstrap/types'
31
+
32
+ // Themes
33
+ import { themes } from '@keystone-pzjr/design-bootstrap/themes'
34
+ ```
35
+
36
+ ## Styles
37
+
38
+ Import CSS in your `app/globals.css`:
39
+
40
+ ```css
41
+ @import "@keystone-pzjr/design-bootstrap/styles/theme.css";
42
+ @import "@keystone-pzjr/design-bootstrap/styles/typography.css";
43
+ @import "@keystone-pzjr/design-bootstrap/styles/style-overrides.aman.css";
44
+ ```
45
+
46
+ ## Theme System
47
+
48
+ ### Available Themes
49
+
50
+ - **`aman`** - Warm, elegant serif theme (Playfair Display + Inter)
51
+ - **`classic`** - Clean, modern sans-serif theme (Inter)
52
+
53
+ Themes are defined in `src/themes/index.ts`.
54
+
55
+ ### Component Variants
56
+
57
+ Components automatically load theme variants based on the active theme:
58
+
59
+ - `hero-home.tsx` - Base/classic variant
60
+ - `hero-home.aman.tsx` - Aman theme variant
61
+
62
+ The `createThemedExport()` function in `sections/index.tsx` handles this automatically.
63
+
64
+ ### Tailwind Classes
65
+
66
+ The design system provides semantic Tailwind classes:
67
+
68
+ **Backgrounds:**
69
+ - `bg-primary` - Main page background
70
+ - `bg-secondary` - Section backgrounds
71
+ - `bg-fg-primary` - Dark backgrounds (buttons, accents)
72
+
73
+ **Text:**
74
+ - `text-fg-primary` - Primary text (dark)
75
+ - `text-secondary` - Secondary text
76
+ - `text-tertiary` - Muted text
77
+ - `text-white` - White text
78
+
79
+ **Borders:**
80
+ - `border-primary` - Primary borders
81
+ - `border-secondary` - Subtle borders
82
+
83
+ **Fonts:**
84
+ - `font-display` - Display font for headings
85
+ - `font-body` - Body font for text
86
+
87
+ ### Theme Customization
88
+
89
+ Override CSS variables in your site's `styles/custom-overrides.css`:
90
+
91
+ ```css
92
+ [data-theme="aman"] {
93
+ --color-bg-primary: #F9F7F0;
94
+ --color-text-primary: #1E1E1E;
95
+ --font-body: "Inter", sans-serif;
96
+ --font-display: "Playfair Display", serif;
97
+ }
98
+ ```
99
+
100
+ **CSS Cascade:**
101
+ 1. `theme.css` - Base design system
102
+ 2. `style-overrides.aman.css` - Theme-specific overrides
103
+ 3. `custom-overrides.css` - Site-specific customizations (highest priority)
104
+
105
+ ## Using Sections
106
+
107
+ Sections are pre-built page components that accept data via props:
108
+
109
+ ```typescript
110
+ // In your page.tsx (Server Component)
111
+ import { HeroHome, TestimonialsHome } from '@keystone-pzjr/design-bootstrap/sections'
112
+ import { getTestimonials } from '@keystone-pzjr/design-bootstrap/lib/server-api'
113
+
114
+ export default async function HomePage() {
115
+ const testimonials = await getTestimonials()
116
+
117
+ return (
118
+ <main>
119
+ <HeroHome config={config} />
120
+ <TestimonialsHome testimonials={testimonials} config={config} />
121
+ </main>
122
+ )
123
+ }
124
+ ```
125
+
126
+ ### Available Sections
127
+
128
+ **Heroes:** `HeroHome`, `HeroServiceDetail`, `HeroLocationDetail`, `HeroFAQ`, `HeroTestimonials`, `HeroSocialMedia`, `HeroGenericText`
129
+
130
+ **Content:** `ServicesHome`, `ServicesGrid`, `LocationGrid`, `LocationDetailsSection`, `TeamGrid`, `JobGallery`, `AboutHome`, `ValuesSection`, `StatisticsSection`
131
+
132
+ **Interactive:** `TestimonialsHome`, `BlogSection`, `BlogGallery`, `BlogPost`, `FAQGrid`, `FAQHome`, `SocialMediaGrid`, `ContactSection`
133
+
134
+ **Navigation:** `HeaderNavigation`, `FooterHome`
135
+
136
+ ## Using Elements
137
+
138
+ Elements are reusable UI components:
139
+
140
+ ```typescript
141
+ import { Button, Input, Carousel } from '@keystone-pzjr/design-bootstrap/elements'
142
+
143
+ <Button variant="primary">Click Me</Button>
144
+ <Input label="Email" type="email" />
145
+ <Carousel items={photos} />
146
+ ```
147
+
148
+ ## Server API Functions
149
+
150
+ ```typescript
151
+ import {
152
+ getCompanyInformation,
153
+ getServices,
154
+ getService,
155
+ getLocations,
156
+ getLocation,
157
+ getTestimonials,
158
+ getFAQs,
159
+ getBlogPosts,
160
+ getBlogPost,
161
+ getTeamMembers,
162
+ getWebsitePhotos,
163
+ getJobPostings
164
+ } from '@keystone-pzjr/design-bootstrap/lib/server-api'
165
+ ```
166
+
167
+ All functions are async and designed for server-side use only.
168
+
169
+ ## Architecture
170
+
171
+ - **Server-Side Only**: All components fetch data server-side
172
+ - **Theme Variants**: Automatic component selection based on active theme
173
+ - **Type-Safe**: Full TypeScript support
174
+ - **CSS Variables**: Theme customization via CSS custom properties
175
+ - **Tailwind First**: Semantic utility classes for consistent styling
176
+
177
+ ## License
178
+
179
+ Private - Keystone PZJR
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "keystone-design-bootstrap",
3
+ "version": "1.0.3",
4
+ "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./sections": "./src/design_system/sections/index.tsx",
11
+ "./elements": "./src/design_system/elements/index.tsx",
12
+ "./logo": "./src/design_system/logo/keystone-logo.tsx",
13
+ "./hooks": "./src/lib/hooks/index.ts",
14
+ "./contexts": "./src/contexts/index.ts",
15
+ "./lib/server-api": "./src/lib/server-api.ts",
16
+ "./lib/component-registry": "./src/lib/component-registry.ts",
17
+ "./styles/*": "./src/styles/*",
18
+ "./types": "./src/types/index.ts",
19
+ "./themes": "./src/themes/index.ts",
20
+ "./utils/*": "./src/utils/*"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Keystone-PZJR/keystone-design-bootstrap.git"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "README.md",
29
+ "package.json"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "dev": "tsup --watch"
34
+ },
35
+ "peerDependencies": {
36
+ "next": ">=15.0.0",
37
+ "react": ">=19.0.0",
38
+ "react-dom": ">=19.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@untitledui/icons": "^0.0.19",
42
+ "clsx": "^2.1.1",
43
+ "embla-carousel-react": "^8.6.0",
44
+ "motion": "^12.23.12",
45
+ "react-aria": "^3.42.0",
46
+ "react-aria-components": "^1.12.0",
47
+ "react-markdown": "^10.1.0",
48
+ "react-stately": "^3.42.0",
49
+ "remark-gfm": "^4.0.1",
50
+ "tailwind-merge": "^3.3.1"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20",
54
+ "@types/react": "^19",
55
+ "@types/react-dom": "^19",
56
+ "tsup": "^8.5.1",
57
+ "typescript": "^5"
58
+ }
59
+ }
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+ import { Theme, isValidTheme } from '../themes';
5
+
6
+ interface ThemeContextValue {
7
+ theme: Theme;
8
+ }
9
+
10
+ const ThemeContext = createContext<ThemeContextValue>({ theme: 'classic' });
11
+
12
+ export function ThemeProvider({
13
+ theme,
14
+ children
15
+ }: {
16
+ theme: Theme;
17
+ children: React.ReactNode;
18
+ }) {
19
+ // Validate theme at runtime
20
+ if (!isValidTheme(theme)) {
21
+ console.warn(`Invalid theme "${theme}", falling back to "classic"`);
22
+ theme = 'classic';
23
+ }
24
+
25
+ return (
26
+ <ThemeContext.Provider value={{ theme }}>
27
+ {children}
28
+ </ThemeContext.Provider>
29
+ );
30
+ }
31
+
32
+ export function useTheme() {
33
+ return useContext(ThemeContext);
34
+ }
@@ -0,0 +1 @@
1
+ export * from './ThemeContext';
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+
3
+ interface IconComponentProps {
4
+ icon: string;
5
+ color?: string;
6
+ className?: string;
7
+ }
8
+
9
+ const IconComponent = ({ icon, color = 'blue', className = '' }: IconComponentProps) => {
10
+ const getIconColor = (colorName: string) => {
11
+ switch (colorName) {
12
+ case 'blue':
13
+ return 'text-blue-600';
14
+ case 'green':
15
+ return 'text-green-600';
16
+ case 'red':
17
+ return 'text-red-600';
18
+ case 'yellow':
19
+ return 'text-yellow-600';
20
+ case 'purple':
21
+ return 'text-purple-600';
22
+ case 'gray':
23
+ return 'text-gray-600';
24
+ case 'white':
25
+ return 'text-white';
26
+ default:
27
+ return 'text-blue-600';
28
+ }
29
+ };
30
+
31
+ const renderIcon = () => {
32
+ switch (icon) {
33
+ case 'star':
34
+ return (
35
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
36
+ <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
37
+ </svg>
38
+ );
39
+ case 'heart':
40
+ return (
41
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
42
+ <path fillRule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clipRule="evenodd"/>
43
+ </svg>
44
+ );
45
+ case 'home':
46
+ return (
47
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
48
+ <path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/>
49
+ </svg>
50
+ );
51
+ case 'person':
52
+ return (
53
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
54
+ <path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd"/>
55
+ </svg>
56
+ );
57
+ case 'phone':
58
+ return (
59
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
60
+ <path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.01 1.01l-.804 1.646a1 1 0 00.01 1.01l.74 4.435a1 1 0 01-.986.836H3a1 1 0 01-1-1V3z"/>
61
+ </svg>
62
+ );
63
+ case 'email':
64
+ return (
65
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
66
+ <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
67
+ <path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
68
+ </svg>
69
+ );
70
+ case 'location':
71
+ return (
72
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
73
+ <path fillRule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clipRule="evenodd"/>
74
+ </svg>
75
+ );
76
+ case 'clock':
77
+ return (
78
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
79
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd"/>
80
+ </svg>
81
+ );
82
+ default:
83
+ return (
84
+ <svg className={`w-full h-full ${getIconColor(color)}`} fill="currentColor" viewBox="0 0 20 20">
85
+ <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd"/>
86
+ </svg>
87
+ );
88
+ }
89
+ };
90
+
91
+ return (
92
+ <div className={className}>
93
+ {renderIcon()}
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export default IconComponent;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { type ReactNode } from "react";
4
+ import { cx } from '../../../utils/cx';
5
+ import { Avatar, type AvatarProps } from "./avatar";
6
+
7
+ const styles = {
8
+ sm: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-xs" },
9
+ md: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-sm" },
10
+ lg: { root: "gap-3", title: "text-md font-semibold", subtitle: "text-md" },
11
+ xl: { root: "gap-4", title: "text-lg font-semibold", subtitle: "text-md" },
12
+ };
13
+
14
+ interface AvatarLabelGroupProps extends AvatarProps {
15
+ size: "sm" | "md" | "lg" | "xl";
16
+ title: string | ReactNode;
17
+ subtitle: string | ReactNode;
18
+ }
19
+
20
+ export const AvatarLabelGroup = ({ title, subtitle, className, ...props }: AvatarLabelGroupProps) => {
21
+ return (
22
+ <figure className={cx("group flex min-w-0 flex-1 items-center", styles[props.size].root, className)}>
23
+ <Avatar {...props} />
24
+ <figcaption className="min-w-0 flex-1">
25
+ <p className={cx("text-primary", styles[props.size].title)}>{title}</p>
26
+ <p className={cx("truncate text-tertiary", styles[props.size].subtitle)}>{subtitle}</p>
27
+ </figcaption>
28
+ </figure>
29
+ );
30
+ };
@@ -0,0 +1,125 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { User01 } from "@untitledui/icons";
5
+ import { cx } from '../../../utils/cx';
6
+ import { type AvatarProps } from "./avatar";
7
+ import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
8
+
9
+ const styles = {
10
+ sm: {
11
+ root: "size-18 p-0.75",
12
+ rootWithPlaceholder: "p-1",
13
+ content: "",
14
+ icon: "size-9",
15
+ initials: "text-display-sm font-semibold",
16
+ badge: "bottom-0.5 right-0.5",
17
+ },
18
+ md: {
19
+ root: "size-24 p-1",
20
+ rootWithPlaceholder: "p-1.25",
21
+ content: "shadow-xl",
22
+ icon: "size-12",
23
+ initials: "text-display-md font-semibold",
24
+ badge: "bottom-1 right-1",
25
+ },
26
+ lg: {
27
+ root: "size-40 p-1.5",
28
+ rootWithPlaceholder: "p-1.75",
29
+ content: "shadow-2xl",
30
+ icon: "size-20",
31
+ initials: "text-display-xl font-semibold",
32
+ badge: "bottom-2 right-2",
33
+ },
34
+ };
35
+
36
+ const tickSizeMap = {
37
+ sm: "2xl",
38
+ md: "3xl",
39
+ lg: "4xl",
40
+ } as const;
41
+
42
+ interface AvatarProfilePhotoProps extends AvatarProps {
43
+ size: "sm" | "md" | "lg";
44
+ }
45
+
46
+ export const AvatarProfilePhoto = ({
47
+ contrastBorder = true,
48
+ size = "md",
49
+ src,
50
+ alt,
51
+ initials,
52
+ placeholder,
53
+ placeholderIcon: PlaceholderIcon,
54
+ verified,
55
+ badge,
56
+ status,
57
+ className,
58
+ }: AvatarProfilePhotoProps) => {
59
+ const [isFailed, setIsFailed] = useState(false);
60
+
61
+ const renderMainContent = () => {
62
+ if (src && !isFailed) {
63
+ return (
64
+ <img
65
+ src={src}
66
+ alt={alt}
67
+ onError={() => setIsFailed(true)}
68
+ className={cx(
69
+ "size-full rounded-full object-cover",
70
+ contrastBorder && "outline-1 -outline-offset-1 outline-avatar-contrast-border",
71
+ styles[size].content,
72
+ )}
73
+ />
74
+ );
75
+ }
76
+
77
+ if (initials) {
78
+ return (
79
+ <div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
80
+ <span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ if (PlaceholderIcon) {
86
+ return (
87
+ <div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
88
+ <PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
95
+ {placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />}
96
+ </div>
97
+ );
98
+ };
99
+
100
+ const renderBadgeContent = () => {
101
+ if (status) {
102
+ return <AvatarOnlineIndicator status={status} size={tickSizeMap[size]} className={styles[size].badge} />;
103
+ }
104
+
105
+ if (verified) {
106
+ return <VerifiedTick size={tickSizeMap[size]} className={cx("absolute", styles[size].badge)} />;
107
+ }
108
+
109
+ return badge;
110
+ };
111
+
112
+ return (
113
+ <div
114
+ className={cx(
115
+ "relative flex shrink-0 items-center justify-center rounded-full bg-primary ring-1 ring-secondary_alt",
116
+ styles[size].root,
117
+ (!src || isFailed) && styles[size].rootWithPlaceholder,
118
+ className,
119
+ )}
120
+ >
121
+ {renderMainContent()}
122
+ {renderBadgeContent()}
123
+ </div>
124
+ );
125
+ };
@@ -0,0 +1,131 @@
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 "./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
+ };