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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cx } from '../../../utils/cx';
|
|
4
|
+
|
|
5
|
+
export interface ProgressBarProps {
|
|
6
|
+
/**
|
|
7
|
+
* The current value of the progress bar.
|
|
8
|
+
*/
|
|
9
|
+
value: number;
|
|
10
|
+
/**
|
|
11
|
+
* The minimum value of the progress bar.
|
|
12
|
+
* @default 0
|
|
13
|
+
*/
|
|
14
|
+
min?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The maximum value of the progress bar.
|
|
17
|
+
* @default 100
|
|
18
|
+
*/
|
|
19
|
+
max?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Optional additional CSS class names for the progress bar container.
|
|
22
|
+
*/
|
|
23
|
+
className?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Optional additional CSS class names for the progress bar indicator element.
|
|
26
|
+
*/
|
|
27
|
+
progressClassName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional function to format the displayed value.
|
|
30
|
+
* It receives the raw value and the calculated percentage.
|
|
31
|
+
*/
|
|
32
|
+
valueFormatter?: (value: number, valueInPercentage: number) => string | number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A basic progress bar component.
|
|
37
|
+
*/
|
|
38
|
+
export const ProgressBarBase = ({ value, min = 0, max = 100, className, progressClassName }: ProgressBarProps) => {
|
|
39
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
role="progressbar"
|
|
44
|
+
aria-valuenow={value}
|
|
45
|
+
aria-valuemin={min}
|
|
46
|
+
aria-valuemax={max}
|
|
47
|
+
className={cx("h-2 w-full overflow-hidden rounded-md bg-quaternary", className)}
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
// Use transform instead of width to avoid layout thrashing (and for smoother animation)
|
|
51
|
+
style={{ transform: `translateX(-${100 - percentage}%)` }}
|
|
52
|
+
className={cx("size-full rounded-md bg-fg-brand-primary transition duration-75 ease-linear", progressClassName)}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type ProgressBarLabelPosition = "right" | "bottom" | "top-floating" | "bottom-floating";
|
|
59
|
+
|
|
60
|
+
export interface ProgressIndicatorWithTextProps extends ProgressBarProps {
|
|
61
|
+
/**
|
|
62
|
+
* Specifies the layout of the text relative to the progress bar.
|
|
63
|
+
* - `right`: Text is displayed to the right of the progress bar.
|
|
64
|
+
* - `bottom`: Text is displayed below the progress bar, aligned to the right.
|
|
65
|
+
* - `top-floating`: Text is displayed in a floating tooltip above the progress indicator.
|
|
66
|
+
* - `bottom-floating`: Text is displayed in a floating tooltip below the progress indicator.
|
|
67
|
+
*/
|
|
68
|
+
labelPosition?: ProgressBarLabelPosition;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A progress bar component that displays the value text in various configurable layouts.
|
|
73
|
+
*/
|
|
74
|
+
export const ProgressBar = ({ value, min = 0, max = 100, valueFormatter, labelPosition, className, progressClassName }: ProgressIndicatorWithTextProps) => {
|
|
75
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
76
|
+
const formattedValue = valueFormatter ? valueFormatter(value, percentage) : `${percentage.toFixed(0)}%`; // Default to rounded percentage
|
|
77
|
+
|
|
78
|
+
const baseProgressBar = <ProgressBarBase min={min} max={max} value={value} className={className} progressClassName={progressClassName} />;
|
|
79
|
+
|
|
80
|
+
switch (labelPosition) {
|
|
81
|
+
case "right":
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex items-center gap-3">
|
|
84
|
+
{baseProgressBar}
|
|
85
|
+
<span className="shrink-0 text-sm font-medium text-secondary tabular-nums">{formattedValue}</span>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
case "bottom":
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex flex-col items-end gap-2">
|
|
91
|
+
{baseProgressBar}
|
|
92
|
+
<span className="text-sm font-medium text-secondary tabular-nums">{formattedValue}</span>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
case "top-floating":
|
|
96
|
+
return (
|
|
97
|
+
<div className="relative flex flex-col items-end gap-2">
|
|
98
|
+
{baseProgressBar}
|
|
99
|
+
<div
|
|
100
|
+
style={{ left: `${percentage}%` }}
|
|
101
|
+
className="absolute -top-2 -translate-x-1/2 -translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-secondary_alt"
|
|
102
|
+
>
|
|
103
|
+
<div className="text-xs font-semibold text-secondary tabular-nums">{formattedValue}</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
case "bottom-floating":
|
|
108
|
+
return (
|
|
109
|
+
<div className="relative flex flex-col items-end gap-2">
|
|
110
|
+
{baseProgressBar}
|
|
111
|
+
<div
|
|
112
|
+
style={{ left: `${percentage}%` }}
|
|
113
|
+
className="absolute -bottom-2 -translate-x-1/2 translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-secondary_alt"
|
|
114
|
+
>
|
|
115
|
+
<div className="text-xs font-semibold text-secondary">{formattedValue}</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
default:
|
|
120
|
+
// Fallback or default case, could render the basic progress bar or throw an error
|
|
121
|
+
return baseProgressBar;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export const CircleProgressBar = (props: { value: number; min?: 0; max?: 100 }) => {
|
|
4
|
+
const { value, min = 0, max = 100 } = props;
|
|
5
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
|
9
|
+
<span className="absolute text-sm font-medium text-primary">{percentage}%</span>
|
|
10
|
+
<svg className="size-16 -rotate-90" viewBox="0 0 60 60">
|
|
11
|
+
<circle className="stroke-bg-quaternary" cx="30" cy="30" r="26" fill="none" strokeWidth="6" />
|
|
12
|
+
<circle
|
|
13
|
+
className="stroke-fg-brand-primary"
|
|
14
|
+
style={{
|
|
15
|
+
strokeDashoffset: `calc(100 - ${percentage})`,
|
|
16
|
+
}}
|
|
17
|
+
cx="30"
|
|
18
|
+
cy="30"
|
|
19
|
+
r="26"
|
|
20
|
+
fill="none"
|
|
21
|
+
strokeWidth="6"
|
|
22
|
+
strokeDasharray="100"
|
|
23
|
+
pathLength="100"
|
|
24
|
+
strokeLinecap="round"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, type Ref, createContext, useContext } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Radio as AriaRadio,
|
|
6
|
+
RadioGroup as AriaRadioGroup,
|
|
7
|
+
type RadioGroupProps as AriaRadioGroupProps,
|
|
8
|
+
type RadioProps as AriaRadioProps,
|
|
9
|
+
} from "react-aria-components";
|
|
10
|
+
import { cx } from '../../../utils/cx';
|
|
11
|
+
|
|
12
|
+
export interface RadioGroupContextType {
|
|
13
|
+
size?: "sm" | "md";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const RadioGroupContext = createContext<RadioGroupContextType | null>(null);
|
|
17
|
+
|
|
18
|
+
export interface RadioButtonBaseProps {
|
|
19
|
+
size?: "sm" | "md";
|
|
20
|
+
className?: string;
|
|
21
|
+
isFocusVisible?: boolean;
|
|
22
|
+
isSelected?: boolean;
|
|
23
|
+
isDisabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const RadioButtonBase = ({ className, isFocusVisible, isSelected, isDisabled, size = "sm" }: RadioButtonBaseProps) => {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={cx(
|
|
30
|
+
"flex size-4 min-h-4 min-w-4 cursor-pointer appearance-none items-center justify-center rounded-full bg-primary ring-1 ring-primary ring-inset",
|
|
31
|
+
size === "md" && "size-5 min-h-5 min-w-5",
|
|
32
|
+
isSelected && !isDisabled && "bg-brand-solid ring-bg-brand-solid",
|
|
33
|
+
isDisabled && "cursor-not-allowed border-disabled bg-disabled_subtle",
|
|
34
|
+
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<div
|
|
39
|
+
className={cx(
|
|
40
|
+
"size-1.5 rounded-full bg-fg-white opacity-0 transition-inherit-all",
|
|
41
|
+
size === "md" && "size-2",
|
|
42
|
+
isDisabled && "bg-fg-disabled_subtle",
|
|
43
|
+
isSelected && "opacity-100",
|
|
44
|
+
)}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
RadioButtonBase.displayName = "RadioButtonBase";
|
|
50
|
+
|
|
51
|
+
interface RadioButtonProps extends AriaRadioProps {
|
|
52
|
+
size?: "sm" | "md";
|
|
53
|
+
label?: ReactNode;
|
|
54
|
+
hint?: ReactNode;
|
|
55
|
+
ref?: Ref<HTMLLabelElement>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const RadioButton = ({ label, hint, className, size = "sm", ...ariaRadioProps }: RadioButtonProps) => {
|
|
59
|
+
const context = useContext(RadioGroupContext);
|
|
60
|
+
|
|
61
|
+
size = context?.size ?? size;
|
|
62
|
+
|
|
63
|
+
const sizes = {
|
|
64
|
+
sm: {
|
|
65
|
+
root: "gap-2",
|
|
66
|
+
textWrapper: "",
|
|
67
|
+
label: "text-sm font-medium",
|
|
68
|
+
hint: "text-sm",
|
|
69
|
+
},
|
|
70
|
+
md: {
|
|
71
|
+
root: "gap-3",
|
|
72
|
+
textWrapper: "gap-0.5",
|
|
73
|
+
label: "text-md font-medium",
|
|
74
|
+
hint: "text-md",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<AriaRadio
|
|
80
|
+
{...ariaRadioProps}
|
|
81
|
+
className={(renderProps) =>
|
|
82
|
+
cx(
|
|
83
|
+
"flex items-start",
|
|
84
|
+
renderProps.isDisabled && "cursor-not-allowed",
|
|
85
|
+
sizes[size].root,
|
|
86
|
+
typeof className === "function" ? className(renderProps) : className,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
{({ isSelected, isDisabled, isFocusVisible }) => (
|
|
91
|
+
<>
|
|
92
|
+
<RadioButtonBase
|
|
93
|
+
size={size}
|
|
94
|
+
isSelected={isSelected}
|
|
95
|
+
isDisabled={isDisabled}
|
|
96
|
+
isFocusVisible={isFocusVisible}
|
|
97
|
+
className={label || hint ? "mt-0.5" : ""}
|
|
98
|
+
/>
|
|
99
|
+
{(label || hint) && (
|
|
100
|
+
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
|
101
|
+
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
|
102
|
+
{hint && (
|
|
103
|
+
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
|
104
|
+
{hint}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</AriaRadio>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
RadioButton.displayName = "RadioButton";
|
|
115
|
+
|
|
116
|
+
interface RadioGroupProps extends RadioGroupContextType, AriaRadioGroupProps {
|
|
117
|
+
children: ReactNode;
|
|
118
|
+
className?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const RadioGroup = ({ children, className, size = "sm", ...props }: RadioGroupProps) => {
|
|
122
|
+
return (
|
|
123
|
+
<RadioGroupContext.Provider value={{ size }}>
|
|
124
|
+
<AriaRadioGroup {...props} className={cx("flex flex-col gap-4", className)}>
|
|
125
|
+
{children}
|
|
126
|
+
</AriaRadioGroup>
|
|
127
|
+
</RadioGroupContext.Provider>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type HTMLAttributes } from "react";
|
|
4
|
+
import { RatingStars } from './rating-stars';
|
|
5
|
+
import { cx } from '../../../utils/cx';
|
|
6
|
+
|
|
7
|
+
export const Wreath = (props: HTMLAttributes<HTMLOrSVGElement>) => (
|
|
8
|
+
<svg width="36" height="81" viewBox="0 0 36 81" fill="none" {...props} className={cx("text-fg-primary", props.className)}>
|
|
9
|
+
<path
|
|
10
|
+
fillRule="evenodd"
|
|
11
|
+
clipRule="evenodd"
|
|
12
|
+
d="M34.188 79.123C21.8844 77.4193 12.9273 67.7396 8.84084 54.5087C7.16207 49.0327 6.91909 42.9593 7.50445 36.6094C8.58681 25.2702 13.7888 15.4245 21.3764 8.24482C21.4095 8.21163 21.4206 8.20057 21.4316 8.23376C21.4537 8.26695 21.52 8.30013 21.5531 8.32226C21.5973 8.34439 21.6083 8.32226 21.5752 8.37757C13.5237 15.7563 8.35488 27.4938 7.79161 39.3529C6.91909 56.2898 15.1362 71.2907 27.4509 76.4569C29.5162 77.3308 31.6809 77.9946 33.934 78.3375C34.0886 78.5367 34.177 78.8132 34.188 79.123Z"
|
|
13
|
+
fill="currentColor"
|
|
14
|
+
/>
|
|
15
|
+
<path
|
|
16
|
+
fillRule="evenodd"
|
|
17
|
+
clipRule="evenodd"
|
|
18
|
+
d="M3.51737 50.8359C4.52243 52.0306 6.12388 53.2033 7.84683 52.7497C7.93518 52.8825 8.38801 53.5462 8.45428 53.6458C8.53159 53.7675 8.67517 53.8892 8.78561 53.7896C8.85188 53.7453 8.9071 53.6458 8.84084 53.4909C8.58681 53.1812 8.34383 52.8603 8.05667 52.5285C7.54863 49.8402 6.80864 48.0481 5.26241 46.2338C3.4732 44.1541 1.6398 43.2248 0.502216 42.8376C0.336548 42.7934 0.181926 42.7491 0.0162576 42.6938C-0.0941877 43.7337 0.380727 45.2493 0.756241 46.1121C1.48518 47.8822 2.28039 49.4309 3.51737 50.8359Z"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
fillRule="evenodd"
|
|
23
|
+
clipRule="evenodd"
|
|
24
|
+
d="M18.317 12.9686C18.1735 13.0239 17.3341 13.2451 17.2347 13.0792C17.2015 13.0349 17.2457 12.869 17.323 12.8579C17.6323 12.8247 17.9747 12.8026 18.2729 12.7362C18.814 11.0326 20.3161 10.2914 21.6967 10.0038C23.3533 9.6719 25.1757 10.1808 27.1306 10.4573C27.23 10.4795 27.3404 10.4905 27.4288 10.4905C27.3956 10.5569 27.3515 10.6012 27.3073 10.6675C26.49 11.7849 25.2088 12.8137 23.9608 13.3226C23.1214 13.6766 22.1164 13.8757 21.1334 13.8425C20.0952 13.7983 19.2227 13.5659 18.317 12.9686Z"
|
|
25
|
+
fill="currentColor"
|
|
26
|
+
/>
|
|
27
|
+
<path
|
|
28
|
+
fillRule="evenodd"
|
|
29
|
+
clipRule="evenodd"
|
|
30
|
+
d="M21.5531 8.3112C21.6194 8.33332 21.6194 8.3112 21.7408 8.24482C21.9728 8.02357 22.1716 7.89082 22.4035 7.68063C23.7289 7.69169 24.9879 7.29344 26.1807 6.49693C26.7109 6.14293 27.241 5.71148 27.6828 5.19154C28.7431 4.05209 29.5935 2.38164 29.9801 0.854996C30.0022 0.766495 30.0132 0.711182 30.0242 0.622681C29.9248 0.666931 29.8365 0.711183 29.7371 0.755433C27.5945 1.61832 25.5181 2.01657 23.7951 3.68703C22.6244 4.80435 22.017 6.15399 22.2599 7.45938C21.9949 7.66957 21.7188 7.86869 21.4647 8.11207C21.3543 8.21163 21.4868 8.3112 21.5531 8.3112Z"
|
|
31
|
+
fill="currentColor"
|
|
32
|
+
/>
|
|
33
|
+
<path
|
|
34
|
+
fillRule="evenodd"
|
|
35
|
+
clipRule="evenodd"
|
|
36
|
+
d="M13.7557 20.1039C13.6121 20.1814 12.8058 20.6018 12.6843 20.4579C12.6291 20.4026 12.6512 20.2256 12.7285 20.1924C13.0267 20.0818 13.3801 20.0044 13.6783 19.8716C13.7888 18.6658 14.4625 17.5374 15.7989 16.763C17.8421 15.6789 20.1615 15.8559 22.3483 15.6678C22.4587 15.6568 22.5581 15.6568 22.6575 15.6457C22.6244 15.7121 22.5913 15.7674 22.5692 15.8338C21.8402 17.0949 20.6585 18.4556 19.4546 19.23C18.6484 19.7721 17.6544 20.1814 16.6604 20.3805C15.6111 20.5907 14.7165 20.469 13.7557 20.1039Z"
|
|
37
|
+
fill="currentColor"
|
|
38
|
+
/>
|
|
39
|
+
<path
|
|
40
|
+
fillRule="evenodd"
|
|
41
|
+
clipRule="evenodd"
|
|
42
|
+
d="M3.0535 38.7113C3.71618 40.2047 4.90898 41.842 6.65402 41.9526C6.70924 42.1075 7.02953 42.8266 7.07371 42.9372C7.10685 43.0921 7.21729 43.258 7.36087 43.1916C7.42714 43.1695 7.4934 43.081 7.48236 42.9261C7.31669 42.5279 7.10685 42.2181 6.90804 41.8088C7.06267 39.0653 6.90804 37.0629 5.85881 34.8062C4.666 32.2064 3.03141 30.713 2.02636 29.9829C1.90487 29.8722 1.76129 29.7837 1.61771 29.6952C1.38578 30.7572 1.46309 32.4056 1.60667 33.357C1.86069 35.2818 2.26934 36.9744 3.0535 38.7113Z"
|
|
43
|
+
fill="currentColor"
|
|
44
|
+
/>
|
|
45
|
+
<path
|
|
46
|
+
fillRule="evenodd"
|
|
47
|
+
clipRule="evenodd"
|
|
48
|
+
d="M5.00838 27.6929C5.29554 29.2859 6.01344 30.89 7.60385 31.5869C7.62594 31.7529 7.80265 32.5273 7.82474 32.6489C7.82474 32.8149 7.87996 33.0029 8.03458 32.9808C8.07876 32.9698 8.18921 32.9255 8.20025 32.7596C8.13399 32.3392 8.0125 31.8635 7.93519 31.4099C8.46532 29.474 8.75248 27.704 8.66412 25.8123C8.52054 23.1462 7.52654 20.8341 5.95821 18.9756C5.83672 18.8428 5.74837 18.699 5.63792 18.5773C5.19614 19.4955 4.90898 21.0664 4.83167 22.0067C4.666 23.9095 4.67705 25.8233 5.00838 27.6929Z"
|
|
49
|
+
fill="currentColor"
|
|
50
|
+
/>
|
|
51
|
+
<path
|
|
52
|
+
fillRule="evenodd"
|
|
53
|
+
clipRule="evenodd"
|
|
54
|
+
d="M9.01755 17.637C8.97337 19.2742 9.37097 21.1106 10.7405 22.1727C10.7295 22.3386 10.6963 23.1351 10.6963 23.2457C10.6742 23.4006 10.6853 23.5887 10.8399 23.6219C10.8951 23.6219 10.9945 23.6108 11.0277 23.4559C11.0608 23.0466 11.0718 22.416 11.0939 21.9625C12.2646 19.9712 12.8942 17.8029 12.9163 15.3249C12.9604 12.4597 12.2536 10.6343 11.6903 9.48383C11.613 9.32896 11.5357 9.17408 11.4584 9.01921C10.8399 9.77146 10.2545 11.2096 9.98947 12.0946C9.45933 13.8868 9.07277 15.7563 9.01755 17.637Z"
|
|
55
|
+
fill="currentColor"
|
|
56
|
+
/>
|
|
57
|
+
<path
|
|
58
|
+
fillRule="evenodd"
|
|
59
|
+
clipRule="evenodd"
|
|
60
|
+
d="M15.092 8.45501C14.7386 9.99271 14.838 12.2495 15.9867 13.1787C15.9425 13.3779 15.8431 14.0638 15.832 14.2186C15.7878 14.3846 15.7547 14.5394 15.8872 14.6058C15.9425 14.6279 15.9866 14.628 16.0529 14.4841C16.1302 14.0527 16.1523 13.6987 16.2186 13.2562C17.8532 11.5083 18.7809 9.78252 19.3 7.42619C19.8191 4.95923 19.7418 3.00114 19.5209 1.61832C19.4988 1.45238 19.4546 1.28644 19.4105 1.10944C17.9747 2.25995 16.9586 3.77553 16.0971 5.50129C15.6332 6.44162 15.2688 7.40406 15.092 8.45501Z"
|
|
61
|
+
fill="currentColor"
|
|
62
|
+
/>
|
|
63
|
+
<path
|
|
64
|
+
fillRule="evenodd"
|
|
65
|
+
clipRule="evenodd"
|
|
66
|
+
d="M10.0889 29.5182C9.95633 29.6399 9.22739 30.3369 9.05068 30.2484C9.0065 30.2041 8.97337 29.9718 9.05068 29.9275C9.32679 29.7063 9.68022 29.5404 9.95633 29.3302C9.86798 28.0026 10.1662 26.653 11.37 25.3919C13.2255 23.5887 15.6884 22.9802 17.9305 22.051C18.0299 22.0067 18.1293 21.9514 18.2508 21.9071C18.2287 21.9846 18.2176 22.0731 18.1845 22.1505C17.7427 23.6993 16.8592 25.5025 15.7878 26.7194C15.092 27.538 14.1533 28.3235 13.1703 28.8434C12.1432 29.3965 11.1712 29.5735 10.0889 29.5182Z"
|
|
67
|
+
fill="currentColor"
|
|
68
|
+
/>
|
|
69
|
+
<path
|
|
70
|
+
fillRule="evenodd"
|
|
71
|
+
clipRule="evenodd"
|
|
72
|
+
d="M8.54263 40.3264C8.44323 40.5034 7.85787 41.4548 7.67012 41.4105C7.5928 41.4105 7.51549 41.1782 7.57072 41.0897C7.81369 40.7689 8.12294 40.5145 8.33279 40.1826C7.95727 38.9657 8.13399 37.2067 9.03964 35.5252C10.4754 33.0693 12.5739 31.8746 14.6061 30.1599C14.6944 30.0714 14.7828 29.9939 14.8822 29.8944C14.8932 29.9718 14.9043 30.0603 14.8932 30.1599C14.8491 31.8414 14.4073 33.9543 13.6231 35.5474C13.1151 36.6204 12.364 37.6935 11.5026 38.5896C10.608 39.4856 9.65813 40.0056 8.54263 40.3264Z"
|
|
73
|
+
fill="currentColor"
|
|
74
|
+
/>
|
|
75
|
+
<path
|
|
76
|
+
fillRule="evenodd"
|
|
77
|
+
clipRule="evenodd"
|
|
78
|
+
d="M9.55873 50.9244C9.48142 51.1346 9.08382 52.263 8.88501 52.3072C8.81875 52.2961 8.68621 52.1302 8.74144 52.0085C8.90711 51.6324 9.09486 51.2009 9.24948 50.8138C8.56472 50.1611 8.33279 48.3468 8.88501 46.444C9.80171 43.6562 11.8449 41.5654 13.4906 39.2533C13.5679 39.1427 13.6452 39.0321 13.7225 38.9214C13.7446 38.9989 13.7557 39.0874 13.7777 39.1759C14.0649 40.8021 14.0428 42.9593 13.601 44.7183C13.3028 45.8909 12.7837 47.1852 12.11 48.2915C11.4142 49.4641 10.5859 50.2717 9.55873 50.9244Z"
|
|
79
|
+
fill="currentColor"
|
|
80
|
+
/>
|
|
81
|
+
<path
|
|
82
|
+
fillRule="evenodd"
|
|
83
|
+
clipRule="evenodd"
|
|
84
|
+
d="M7.0958 63.3256C8.34383 64.1442 10.5748 64.7637 12.11 63.757C12.2315 63.8455 12.8721 64.2327 12.9494 64.2991C13.0598 64.4097 13.2145 64.4871 13.3139 64.3323C13.3691 64.2659 13.4022 64.1553 13.2918 64.0336C12.9715 63.8123 12.6181 63.6685 12.2757 63.4473C11.105 61.0135 9.84589 59.3652 7.9131 58.1483C5.70419 56.7323 3.85975 56.7101 2.67799 56.7101C2.51232 56.7101 2.34665 56.7212 2.18098 56.7212C2.46814 59.1218 5.43912 62.2968 7.0958 63.3256Z"
|
|
85
|
+
fill="currentColor"
|
|
86
|
+
/>
|
|
87
|
+
<path
|
|
88
|
+
fillRule="evenodd"
|
|
89
|
+
clipRule="evenodd"
|
|
90
|
+
d="M13.2255 61.4449C13.2145 61.5998 13.1372 62.2857 13.0267 62.7061C12.9936 62.8277 12.9825 63.0048 12.8169 62.9384C12.7506 62.9273 12.6843 62.7835 12.6843 62.7171C12.7616 62.3078 12.8279 61.8432 12.861 61.4339C12.099 60.9471 11.5026 59.3541 11.3811 57.396C11.5136 54.4423 12.839 51.7651 13.8992 48.9774C13.9324 48.8557 13.9765 48.7229 14.0207 48.5902C14.0649 48.6565 14.087 48.7229 14.1312 48.7893C14.838 50.2496 15.3792 52.2962 15.4234 54.1436C15.4676 55.3494 15.2908 56.7544 14.9374 58.0598C14.5619 59.3983 14.0097 60.4493 13.2255 61.4449Z"
|
|
91
|
+
fill="currentColor"
|
|
92
|
+
/>
|
|
93
|
+
<path
|
|
94
|
+
fillRule="evenodd"
|
|
95
|
+
clipRule="evenodd"
|
|
96
|
+
d="M13.8551 73.4257C15.2908 73.7134 17.2347 73.647 18.4496 72.0982C18.7588 72.1425 19.2558 72.2199 19.5761 72.242C19.6645 72.2531 19.7307 72.1867 19.7639 72.0761C19.786 71.9987 19.7639 71.8991 19.6203 71.8217C19.2558 71.7442 18.8914 71.7442 18.5048 71.6778C16.8923 69.7861 15.3682 68.7241 13.1703 68.3259C10.6301 67.8612 8.47637 68.5139 7.32774 68.9896C7.17311 69.0449 7.00744 69.1113 6.86387 69.1888C7.27251 70.1069 8.46532 71.0473 9.21635 71.5008C10.6521 72.4522 12.1211 73.1381 13.8551 73.4257Z"
|
|
97
|
+
fill="currentColor"
|
|
98
|
+
/>
|
|
99
|
+
<path
|
|
100
|
+
fillRule="evenodd"
|
|
101
|
+
clipRule="evenodd"
|
|
102
|
+
d="M18.9908 69.4321C19.0128 69.5981 19.0349 70.2618 19.0349 70.7043C19.0239 70.826 19.057 70.992 18.8914 71.003C18.814 71.003 18.7257 70.9035 18.7257 70.826C18.6926 70.4167 18.7146 69.9742 18.6484 69.5538C17.798 69.2883 16.6714 67.994 16.1523 66.2129C15.5559 63.3588 16.2075 60.4161 16.5168 57.396C16.5389 57.2633 16.5278 57.1305 16.5389 56.9867C16.5941 57.042 16.6604 57.0973 16.7156 57.1526C17.7538 58.2921 18.792 60.051 19.289 61.7768C19.6313 62.9052 19.8081 64.2991 19.8081 65.6376C19.786 67.0426 19.4988 68.2263 18.9908 69.4321Z"
|
|
103
|
+
fill="currentColor"
|
|
104
|
+
/>
|
|
105
|
+
<path
|
|
106
|
+
fillRule="evenodd"
|
|
107
|
+
clipRule="evenodd"
|
|
108
|
+
d="M27.0312 75.24C27.1085 75.3617 27.2079 75.9148 27.3183 76.3352C27.3515 76.4348 27.4177 76.5896 27.2631 76.645C27.1858 76.6781 27.0864 76.6118 27.0643 76.5454C26.9207 76.1914 26.9207 75.8153 26.744 75.4613C25.9157 75.6604 24.4799 74.6094 23.5411 73.2377C22.2379 70.8703 22.0943 68.0936 21.5862 65.2726C21.5752 65.1398 21.531 65.0182 21.5089 64.8854C21.5752 64.8965 21.6414 64.9407 21.7077 64.9628C22.9557 65.5934 24.3805 66.7107 25.2972 68.0825C25.9267 68.9675 26.5121 70.1955 26.8434 71.4234C27.1858 72.7177 27.1968 73.9457 27.0312 75.24Z"
|
|
109
|
+
fill="currentColor"
|
|
110
|
+
/>
|
|
111
|
+
<path
|
|
112
|
+
fillRule="evenodd"
|
|
113
|
+
clipRule="evenodd"
|
|
114
|
+
d="M22.514 80.5058C23.9829 80.3067 25.8825 79.6319 26.7219 77.7512C27.0201 77.6627 27.5392 77.6074 27.8485 77.5189C27.9368 77.5078 27.981 77.4083 27.9921 77.2866C27.9921 77.2202 27.9479 77.1207 27.7933 77.0985C27.4288 77.1538 27.0643 77.2645 26.6557 77.3087C24.5241 76.0476 22.8011 75.4723 20.5591 75.7931C17.9857 76.1582 16.1081 77.4525 15.1031 78.2822C14.9595 78.4039 14.8159 78.5145 14.6834 78.6362C15.3019 79.3996 16.6935 79.9084 17.5218 80.1297C19.1454 80.5833 20.7358 80.7603 22.514 80.5058Z"
|
|
115
|
+
fill="currentColor"
|
|
116
|
+
/>
|
|
117
|
+
</svg>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
interface RatingBadgeProps extends HTMLAttributes<HTMLDivElement> {
|
|
121
|
+
title?: string;
|
|
122
|
+
subtitle?: string;
|
|
123
|
+
rating?: number;
|
|
124
|
+
theme?: "light" | "dark";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const RatingBadge = ({ title = "Best Design Tool", subtitle = "2,000+ reviews", rating, theme = "dark", className, ...props }: RatingBadgeProps) => {
|
|
128
|
+
return (
|
|
129
|
+
<div {...props} className={cx("flex items-center -space-x-0.5", className)}>
|
|
130
|
+
<Wreath className={cx("shrink-0", theme === "light" && "text-fg-white")} />
|
|
131
|
+
|
|
132
|
+
<div className="flex flex-col items-center gap-1">
|
|
133
|
+
<RatingStars rating={rating} className="gap-0.5" starClassName="size-4" />
|
|
134
|
+
|
|
135
|
+
<div className="text-center">
|
|
136
|
+
<p className={cx("text-sm font-semibold", theme === "light" ? "text-primary_on-brand" : "text-primary")}>{title}</p>
|
|
137
|
+
<p className={cx("text-xs font-medium", theme === "light" ? "text-secondary_on-brand" : "text-secondary")}>{subtitle}</p>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<Wreath className={cx("shrink-0 -scale-x-100", theme === "light" && "text-fg-white")} />
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, SVGProps } from "react";
|
|
4
|
+
import { useId } from "react";
|
|
5
|
+
import { cx } from '../../../utils/cx';
|
|
6
|
+
|
|
7
|
+
export const getStarProgress = (starPosition: number, rating: number, maxRating: number = 5) => {
|
|
8
|
+
// Ensure rating is between 0 and 5
|
|
9
|
+
const clampedRating = Math.min(Math.max(rating, 0), maxRating);
|
|
10
|
+
|
|
11
|
+
const diff = clampedRating - starPosition;
|
|
12
|
+
|
|
13
|
+
if (diff >= 1) return 100;
|
|
14
|
+
if (diff <= 0) return 0;
|
|
15
|
+
|
|
16
|
+
return Math.round(diff * 100);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface StarIconProps extends SVGProps<SVGSVGElement> {
|
|
20
|
+
/**
|
|
21
|
+
* The progress of the star icon. It should be a number between 0 and 100.
|
|
22
|
+
*
|
|
23
|
+
* @default 100
|
|
24
|
+
*/
|
|
25
|
+
progress?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const StarIcon = ({ progress = 100, ...props }: StarIconProps) => {
|
|
29
|
+
const id = useId();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" {...props} className={cx("size-5 text-warning-400", props.className)}>
|
|
33
|
+
<path
|
|
34
|
+
d="M9.53834 1.60996C9.70914 1.19932 10.2909 1.19932 10.4617 1.60996L12.5278 6.57744C12.5998 6.75056 12.7626 6.86885 12.9495 6.88383L18.3123 7.31376C18.7556 7.3493 18.9354 7.90256 18.5976 8.19189L14.5117 11.6919C14.3693 11.8139 14.3071 12.0053 14.3506 12.1876L15.5989 17.4208C15.7021 17.8534 15.2315 18.1954 14.8519 17.9635L10.2606 15.1592C10.1006 15.0615 9.89938 15.0615 9.73937 15.1592L5.14806 17.9635C4.76851 18.1954 4.29788 17.8534 4.40108 17.4208L5.64939 12.1876C5.69289 12.0053 5.6307 11.8139 5.48831 11.6919L1.40241 8.19189C1.06464 7.90256 1.24441 7.3493 1.68773 7.31376L7.05054 6.88383C7.23744 6.86885 7.40024 6.75056 7.47225 6.57744L9.53834 1.60996Z"
|
|
35
|
+
className="fill-bg-tertiary"
|
|
36
|
+
/>
|
|
37
|
+
<g clipPath={`url(#clip-${id})`}>
|
|
38
|
+
<path
|
|
39
|
+
d="M9.53834 1.60996C9.70914 1.19932 10.2909 1.19932 10.4617 1.60996L12.5278 6.57744C12.5998 6.75056 12.7626 6.86885 12.9495 6.88383L18.3123 7.31376C18.7556 7.3493 18.9354 7.90256 18.5976 8.19189L14.5117 11.6919C14.3693 11.8139 14.3071 12.0053 14.3506 12.1876L15.5989 17.4208C15.7021 17.8534 15.2315 18.1954 14.8519 17.9635L10.2606 15.1592C10.1006 15.0615 9.89938 15.0615 9.73937 15.1592L5.14806 17.9635C4.76851 18.1954 4.29788 17.8534 4.40108 17.4208L5.64939 12.1876C5.69289 12.0053 5.6307 11.8139 5.48831 11.6919L1.40241 8.19189C1.06464 7.90256 1.24441 7.3493 1.68773 7.31376L7.05054 6.88383C7.23744 6.86885 7.40024 6.75056 7.47225 6.57744L9.53834 1.60996Z"
|
|
40
|
+
fill="currentColor"
|
|
41
|
+
/>
|
|
42
|
+
</g>
|
|
43
|
+
<defs>
|
|
44
|
+
<clipPath id={`clip-${id}`}>
|
|
45
|
+
<rect width={`${progress}%`} height="20" fill="white" />
|
|
46
|
+
</clipPath>
|
|
47
|
+
</defs>
|
|
48
|
+
</svg>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
interface RatingStarsProps extends HTMLAttributes<HTMLDivElement> {
|
|
53
|
+
/**
|
|
54
|
+
* The rating to display.
|
|
55
|
+
*
|
|
56
|
+
* @default 5
|
|
57
|
+
*/
|
|
58
|
+
rating?: number;
|
|
59
|
+
/**
|
|
60
|
+
* The number of stars to display.
|
|
61
|
+
*/
|
|
62
|
+
stars?: number;
|
|
63
|
+
/**
|
|
64
|
+
* The class name of the star icon.
|
|
65
|
+
*/
|
|
66
|
+
starClassName?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const RatingStars = ({ rating = 5, stars = 5, starClassName, ...props }: RatingStarsProps) => {
|
|
70
|
+
return (
|
|
71
|
+
<div {...props} className={cx("flex", props.className)}>
|
|
72
|
+
{Array.from({ length: stars }).map((_, index) => (
|
|
73
|
+
<StarIcon key={index} progress={getStarProgress(index, rating, stars)} className={starClassName} />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FocusEventHandler, PointerEventHandler, RefAttributes, RefObject } from "react";
|
|
4
|
+
import { useCallback, useContext, useRef, useState } from "react";
|
|
5
|
+
import { SearchLg as SearchIcon } from "@untitledui/icons";
|
|
6
|
+
import type { ComboBoxProps as AriaComboBoxProps, GroupProps as AriaGroupProps, ListBoxProps as AriaListBoxProps } from "react-aria-components";
|
|
7
|
+
import { ComboBox as AriaComboBox, Group as AriaGroup, Input as AriaInput, ListBox as AriaListBox, ComboBoxStateContext } from "react-aria-components";
|
|
8
|
+
import { HintText } from '../input/hint-text';
|
|
9
|
+
import { Label } from '../input/label';
|
|
10
|
+
import { Popover } from './popover';
|
|
11
|
+
import { type CommonProps, SelectContext, type SelectItemType, sizes } from './select';
|
|
12
|
+
import { useResizeObserver } from '../../../lib/hooks/use-resize-observer';
|
|
13
|
+
import { cx } from '../../../utils/cx';
|
|
14
|
+
|
|
15
|
+
interface ComboBoxProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
|
|
16
|
+
shortcut?: boolean;
|
|
17
|
+
items?: SelectItemType[];
|
|
18
|
+
popoverClassName?: string;
|
|
19
|
+
shortcutClassName?: string;
|
|
20
|
+
children: AriaListBoxProps<SelectItemType>["children"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ComboBoxValueProps extends AriaGroupProps {
|
|
24
|
+
size: "sm" | "md";
|
|
25
|
+
shortcut: boolean;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
shortcutClassName?: string;
|
|
28
|
+
onFocus?: FocusEventHandler;
|
|
29
|
+
onPointerEnter?: PointerEventHandler;
|
|
30
|
+
ref?: RefObject<HTMLDivElement | null>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ComboBoxValue = ({ size, shortcut, placeholder, shortcutClassName, ...otherProps }: ComboBoxValueProps) => {
|
|
34
|
+
const state = useContext(ComboBoxStateContext);
|
|
35
|
+
|
|
36
|
+
const value = state?.selectedItem?.value || null;
|
|
37
|
+
const inputValue = state?.inputValue || null;
|
|
38
|
+
|
|
39
|
+
const first = inputValue?.split(value?.supportingText)?.[0] || "";
|
|
40
|
+
const last = inputValue?.split(first)[1];
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<AriaGroup
|
|
44
|
+
{...otherProps}
|
|
45
|
+
className={({ isFocusWithin, isDisabled }) =>
|
|
46
|
+
cx(
|
|
47
|
+
"relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition-shadow duration-100 ease-linear ring-inset",
|
|
48
|
+
isDisabled && "cursor-not-allowed bg-disabled_subtle",
|
|
49
|
+
isFocusWithin && "ring-2 ring-brand",
|
|
50
|
+
sizes[size].root,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
>
|
|
54
|
+
{({ isDisabled }) => (
|
|
55
|
+
<>
|
|
56
|
+
<SearchIcon className="pointer-events-none size-5 shrink-0 text-fg-quaternary" />
|
|
57
|
+
|
|
58
|
+
<div className="relative flex w-full items-center gap-2">
|
|
59
|
+
{inputValue && (
|
|
60
|
+
<span className="absolute top-1/2 z-0 inline-flex w-full -translate-y-1/2 gap-2 truncate" aria-hidden="true">
|
|
61
|
+
<p className={cx("text-md font-medium text-primary", isDisabled && "text-disabled")}>{first}</p>
|
|
62
|
+
{last && <p className={cx("-ml-0.75 text-md text-tertiary", isDisabled && "text-disabled")}>{last}</p>}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
<AriaInput
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
className="z-10 w-full appearance-none bg-transparent text-md text-transparent caret-alpha-black/90 placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{shortcut && (
|
|
73
|
+
<div
|
|
74
|
+
className={cx(
|
|
75
|
+
"absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
|
76
|
+
isDisabled && "to-bg-disabled_subtle",
|
|
77
|
+
sizes[size].shortcut,
|
|
78
|
+
shortcutClassName,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<span
|
|
82
|
+
className={cx(
|
|
83
|
+
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
|
84
|
+
isDisabled && "bg-transparent text-disabled",
|
|
85
|
+
)}
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
>
|
|
88
|
+
⌘K
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</AriaGroup>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const ComboBox = ({ placeholder = "Search", shortcut = true, size = "sm", children, items, shortcutClassName, ...otherProps }: ComboBoxProps) => {
|
|
99
|
+
const placeholderRef = useRef<HTMLDivElement>(null);
|
|
100
|
+
const [popoverWidth, setPopoverWidth] = useState("");
|
|
101
|
+
|
|
102
|
+
// Resize observer for popover width
|
|
103
|
+
const onResize = useCallback(() => {
|
|
104
|
+
if (!placeholderRef.current) return;
|
|
105
|
+
|
|
106
|
+
const divRect = placeholderRef.current?.getBoundingClientRect();
|
|
107
|
+
|
|
108
|
+
setPopoverWidth(divRect.width + "px");
|
|
109
|
+
}, [placeholderRef, setPopoverWidth]);
|
|
110
|
+
|
|
111
|
+
useResizeObserver({
|
|
112
|
+
ref: placeholderRef,
|
|
113
|
+
box: "border-box",
|
|
114
|
+
onResize,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<SelectContext.Provider value={{ size }}>
|
|
119
|
+
<AriaComboBox menuTrigger="focus" {...otherProps}>
|
|
120
|
+
{(state) => (
|
|
121
|
+
<div className="flex flex-col gap-1.5">
|
|
122
|
+
{otherProps.label && (
|
|
123
|
+
<Label isRequired={state.isRequired} tooltip={otherProps.tooltip}>
|
|
124
|
+
{otherProps.label}
|
|
125
|
+
</Label>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
<ComboBoxValue
|
|
129
|
+
ref={placeholderRef}
|
|
130
|
+
placeholder={placeholder}
|
|
131
|
+
shortcut={shortcut}
|
|
132
|
+
shortcutClassName={shortcutClassName}
|
|
133
|
+
size={size}
|
|
134
|
+
// This is a workaround to correctly calculating the trigger width
|
|
135
|
+
// while using ResizeObserver wasn't 100% reliable.
|
|
136
|
+
onFocus={onResize}
|
|
137
|
+
onPointerEnter={onResize}
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<Popover size={size} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={otherProps.popoverClassName}>
|
|
141
|
+
<AriaListBox items={items} className="size-full outline-hidden">
|
|
142
|
+
{children}
|
|
143
|
+
</AriaListBox>
|
|
144
|
+
</Popover>
|
|
145
|
+
|
|
146
|
+
{otherProps.hint && <HintText isInvalid={state.isInvalid}>{otherProps.hint}</HintText>}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</AriaComboBox>
|
|
150
|
+
</SelectContext.Provider>
|
|
151
|
+
);
|
|
152
|
+
};
|