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.
- package/README.md +179 -0
- package/package.json +59 -0
- package/src/contexts/ThemeContext.tsx +34 -0
- package/src/contexts/index.ts +1 -0
- package/src/design_system/elements/IconComponent.tsx +98 -0
- package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
- package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
- package/src/design_system/elements/avatar/avatar.tsx +131 -0
- package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
- package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
- package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
- package/src/design_system/elements/avatar/utils.ts +12 -0
- package/src/design_system/elements/badges/avatar.tsx +132 -0
- package/src/design_system/elements/badges/badge-groups.tsx +176 -0
- package/src/design_system/elements/badges/badge-types.ts +266 -0
- package/src/design_system/elements/badges/badges.tsx +430 -0
- package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
- package/src/design_system/elements/button-group/button-group.tsx +106 -0
- package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
- package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
- package/src/design_system/elements/buttons/button-utility.tsx +116 -0
- package/src/design_system/elements/buttons/button.aman.tsx +174 -0
- package/src/design_system/elements/buttons/button.tsx +271 -0
- package/src/design_system/elements/buttons/close-button.tsx +42 -0
- package/src/design_system/elements/buttons/round-button.tsx +29 -0
- package/src/design_system/elements/buttons/social-button.tsx +148 -0
- package/src/design_system/elements/buttons/social-logos.tsx +115 -0
- package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
- package/src/design_system/elements/carousel/carousel.tsx +308 -0
- package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
- package/src/design_system/elements/date-picker/calendar.tsx +101 -0
- package/src/design_system/elements/date-picker/cell.tsx +106 -0
- package/src/design_system/elements/date-picker/date-input.tsx +32 -0
- package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
- package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
- package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
- package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
- package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
- package/src/design_system/elements/form/form.tsx +10 -0
- package/src/design_system/elements/form/hook-form.tsx +75 -0
- package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
- package/src/design_system/elements/index.tsx +158 -0
- package/src/design_system/elements/input/hint-text.tsx +33 -0
- package/src/design_system/elements/input/input-group.tsx +133 -0
- package/src/design_system/elements/input/input.aman.tsx +172 -0
- package/src/design_system/elements/input/input.tsx +271 -0
- package/src/design_system/elements/input/label.tsx +50 -0
- package/src/design_system/elements/label/label.tsx +50 -0
- package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
- package/src/design_system/elements/map/GoogleMap.tsx +286 -0
- package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
- package/src/design_system/elements/modals/modal.tsx +41 -0
- package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
- package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
- package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
- package/src/design_system/elements/pagination/pagination.tsx +330 -0
- package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
- package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
- package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
- package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
- package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
- package/src/design_system/elements/rating/rating-badge.tsx +144 -0
- package/src/design_system/elements/rating/rating-stars.tsx +77 -0
- package/src/design_system/elements/select/combobox.tsx +152 -0
- package/src/design_system/elements/select/multi-select.tsx +363 -0
- package/src/design_system/elements/select/popover.tsx +34 -0
- package/src/design_system/elements/select/select-item.tsx +97 -0
- package/src/design_system/elements/select/select-native.tsx +69 -0
- package/src/design_system/elements/select/select.aman.tsx +75 -0
- package/src/design_system/elements/select/select.tsx +146 -0
- package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
- package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
- package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
- package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
- package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
- package/src/design_system/elements/tabs/tabs.tsx +225 -0
- package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
- package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
- package/src/design_system/elements/tags/tags.tsx +176 -0
- package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
- package/src/design_system/elements/textarea/textarea.tsx +111 -0
- package/src/design_system/elements/toggle/toggle.tsx +140 -0
- package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
- package/src/design_system/hooks/use-breakpoint.ts +37 -0
- package/src/design_system/hooks/use-resize-observer.ts +68 -0
- package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
- package/src/design_system/logo/keystone-logo.tsx +22 -0
- package/src/design_system/sections/about-home.aman.tsx +85 -0
- package/src/design_system/sections/about-home.tsx +115 -0
- package/src/design_system/sections/blog-cards.tsx +848 -0
- package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
- package/src/design_system/sections/blog-gallery.tsx +204 -0
- package/src/design_system/sections/blog-home.aman.tsx +84 -0
- package/src/design_system/sections/blog-home.tsx +153 -0
- package/src/design_system/sections/blog-post.aman.tsx +74 -0
- package/src/design_system/sections/blog-post.tsx +301 -0
- package/src/design_system/sections/blog-section.aman.tsx +101 -0
- package/src/design_system/sections/blog-section.tsx +179 -0
- package/src/design_system/sections/contact-home.tsx +25 -0
- package/src/design_system/sections/contact-section.aman.tsx +173 -0
- package/src/design_system/sections/contact-section.tsx +143 -0
- package/src/design_system/sections/faq-grid.aman.tsx +79 -0
- package/src/design_system/sections/faq-grid.tsx +102 -0
- package/src/design_system/sections/faq-home.aman.tsx +92 -0
- package/src/design_system/sections/faq-home.tsx +134 -0
- package/src/design_system/sections/feature-tab.tsx +43 -0
- package/src/design_system/sections/feature-text.tsx +284 -0
- package/src/design_system/sections/footer-home.aman.tsx +62 -0
- package/src/design_system/sections/footer-home.tsx +259 -0
- package/src/design_system/sections/generic-header-component.tsx +103 -0
- package/src/design_system/sections/header-navigation.aman.tsx +360 -0
- package/src/design_system/sections/header-navigation.tsx +334 -0
- package/src/design_system/sections/hero-faq.aman.tsx +38 -0
- package/src/design_system/sections/hero-faq.tsx +55 -0
- package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
- package/src/design_system/sections/hero-generic-text.tsx +51 -0
- package/src/design_system/sections/hero-home.aman.tsx +84 -0
- package/src/design_system/sections/hero-home.tsx +246 -0
- package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
- package/src/design_system/sections/hero-location-detail.tsx +72 -0
- package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
- package/src/design_system/sections/hero-service-detail.tsx +51 -0
- package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
- package/src/design_system/sections/hero-social-media.tsx +35 -0
- package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
- package/src/design_system/sections/hero-testimonials.tsx +55 -0
- package/src/design_system/sections/home-hero-component.tsx +228 -0
- package/src/design_system/sections/index.tsx +131 -0
- package/src/design_system/sections/job-gallery.aman.tsx +91 -0
- package/src/design_system/sections/job-gallery.tsx +183 -0
- package/src/design_system/sections/location-details-section.aman.tsx +179 -0
- package/src/design_system/sections/location-details-section.tsx +196 -0
- package/src/design_system/sections/location-grid.aman.tsx +76 -0
- package/src/design_system/sections/location-grid.tsx +123 -0
- package/src/design_system/sections/services-grid.aman.tsx +85 -0
- package/src/design_system/sections/services-grid.tsx +104 -0
- package/src/design_system/sections/services-home.aman.tsx +78 -0
- package/src/design_system/sections/services-home.tsx +131 -0
- package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
- package/src/design_system/sections/social-media-grid.tsx +189 -0
- package/src/design_system/sections/statistics-section.aman.tsx +79 -0
- package/src/design_system/sections/statistics-section.tsx +97 -0
- package/src/design_system/sections/team-grid.aman.tsx +85 -0
- package/src/design_system/sections/team-grid.tsx +88 -0
- package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
- package/src/design_system/sections/testimonials-home.tsx +90 -0
- package/src/design_system/sections/values-section.aman.tsx +73 -0
- package/src/design_system/sections/values-section.tsx +128 -0
- package/src/design_system/utils/icon-mapping.tsx +28 -0
- package/src/index.ts +7 -0
- package/src/lib/component-registry.ts +53 -0
- package/src/lib/hooks/index.ts +8 -0
- package/src/lib/hooks/use-breakpoint.ts +37 -0
- package/src/lib/hooks/use-clipboard.ts +79 -0
- package/src/lib/hooks/use-resize-observer.ts +68 -0
- package/src/lib/server-api.ts +115 -0
- package/src/styles/style-overrides.aman.css +101 -0
- package/src/styles/theme.css +224 -0
- package/src/styles/typography.css +430 -0
- package/src/themes/index.ts +23 -0
- package/src/types/api/blog-post.ts +53 -0
- package/src/types/api/company-information.ts +44 -0
- package/src/types/api/contact.ts +63 -0
- package/src/types/api/faq.ts +37 -0
- package/src/types/api/job-posting.ts +34 -0
- package/src/types/api/location.ts +36 -0
- package/src/types/api/photos.ts +28 -0
- package/src/types/api/service.ts +37 -0
- package/src/types/api/social-post.ts +28 -0
- package/src/types/api/team-member.ts +29 -0
- package/src/types/api/testimonial.ts +29 -0
- package/src/types/api/website-photos.ts +22 -0
- package/src/types/config.ts +21 -0
- package/src/types/index.ts +21 -0
- package/src/utils/countries.tsx +1351 -0
- package/src/utils/cx.ts +25 -0
- package/src/utils/gradient-placeholder.ts +59 -0
- package/src/utils/is-react-component.ts +33 -0
- package/src/utils/markdown-toc.ts +54 -0
- 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
|
+
};
|