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,308 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, ComponentPropsWithRef, HTMLAttributes, KeyboardEvent, ReactNode, Ref } from "react";
|
|
4
|
+
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useState } from "react";
|
|
5
|
+
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
|
6
|
+
import { cx } from '../../../utils/cx';
|
|
7
|
+
|
|
8
|
+
type CarouselApi = UseEmblaCarouselType[1];
|
|
9
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
10
|
+
type CarouselOptions = UseCarouselParameters[0];
|
|
11
|
+
type CarouselPlugin = UseCarouselParameters[1];
|
|
12
|
+
|
|
13
|
+
type CarouselProps = {
|
|
14
|
+
/** The options for the Embla carousel. */
|
|
15
|
+
opts?: CarouselOptions;
|
|
16
|
+
/** The plugins for the Embla carousel. */
|
|
17
|
+
plugins?: CarouselPlugin;
|
|
18
|
+
/** The orientation of the carousel. */
|
|
19
|
+
orientation?: "horizontal" | "vertical";
|
|
20
|
+
/** The function to set the API for the carousel. */
|
|
21
|
+
setApi?: (api: CarouselApi) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type CarouselContextProps = CarouselProps & {
|
|
25
|
+
/** The ref of the carousel. */
|
|
26
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
27
|
+
/** The API of the carousel. */
|
|
28
|
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
29
|
+
/** The function to scroll the carousel to the previous slide. */
|
|
30
|
+
scrollPrev: () => void;
|
|
31
|
+
/** The function to scroll the carousel to the next slide. */
|
|
32
|
+
scrollNext: () => void;
|
|
33
|
+
/** Whether the carousel can scroll to the previous slide. */
|
|
34
|
+
canScrollPrev: boolean;
|
|
35
|
+
/** Whether the carousel can scroll to the next slide. */
|
|
36
|
+
canScrollNext: boolean;
|
|
37
|
+
/** The index of the selected slide. */
|
|
38
|
+
selectedIndex: number;
|
|
39
|
+
/** The scroll snaps of the carousel. */
|
|
40
|
+
scrollSnaps: number[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const CarouselContext = createContext<CarouselContextProps | null>(null);
|
|
44
|
+
|
|
45
|
+
export const useCarousel = () => {
|
|
46
|
+
const context = useContext(CarouselContext);
|
|
47
|
+
|
|
48
|
+
if (!context) {
|
|
49
|
+
throw new Error("The `useCarousel` hook must be used within a <Carousel />");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return context;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const CarouselRoot = ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }: ComponentPropsWithRef<"div"> & CarouselProps) => {
|
|
56
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
57
|
+
{
|
|
58
|
+
...opts,
|
|
59
|
+
axis: orientation === "horizontal" ? "x" : "y",
|
|
60
|
+
},
|
|
61
|
+
plugins,
|
|
62
|
+
);
|
|
63
|
+
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
|
64
|
+
const [canScrollNext, setCanScrollNext] = useState(false);
|
|
65
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
66
|
+
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
|
|
67
|
+
|
|
68
|
+
const onInit = useCallback((api: CarouselApi) => {
|
|
69
|
+
if (!api) return;
|
|
70
|
+
|
|
71
|
+
setScrollSnaps(api.scrollSnapList());
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const onSelect = useCallback((api: CarouselApi) => {
|
|
75
|
+
if (!api) return;
|
|
76
|
+
|
|
77
|
+
setCanScrollPrev(api.canScrollPrev());
|
|
78
|
+
setCanScrollNext(api.canScrollNext());
|
|
79
|
+
setSelectedIndex(api.selectedScrollSnap());
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const scrollPrev = useCallback(() => {
|
|
83
|
+
api?.scrollPrev();
|
|
84
|
+
}, [api]);
|
|
85
|
+
|
|
86
|
+
const scrollNext = useCallback(() => {
|
|
87
|
+
api?.scrollNext();
|
|
88
|
+
}, [api]);
|
|
89
|
+
|
|
90
|
+
const handleKeyDown = useCallback(
|
|
91
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
92
|
+
if (event.key === "ArrowLeft") {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
scrollPrev();
|
|
95
|
+
} else if (event.key === "ArrowRight") {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
scrollNext();
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[scrollPrev, scrollNext],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!api || !setApi) return;
|
|
105
|
+
|
|
106
|
+
setApi(api);
|
|
107
|
+
}, [api, setApi]);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!api) return;
|
|
111
|
+
|
|
112
|
+
onInit(api);
|
|
113
|
+
onSelect(api);
|
|
114
|
+
|
|
115
|
+
api.on("reInit", onInit);
|
|
116
|
+
api.on("reInit", onSelect);
|
|
117
|
+
api.on("select", onSelect);
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
api?.off("select", onSelect);
|
|
121
|
+
};
|
|
122
|
+
}, [api, onInit, onSelect]);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<CarouselContext.Provider
|
|
126
|
+
value={{
|
|
127
|
+
carouselRef,
|
|
128
|
+
api: api,
|
|
129
|
+
opts,
|
|
130
|
+
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
131
|
+
scrollPrev,
|
|
132
|
+
scrollNext,
|
|
133
|
+
canScrollPrev,
|
|
134
|
+
canScrollNext,
|
|
135
|
+
selectedIndex,
|
|
136
|
+
scrollSnaps,
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<div onKeyDownCapture={handleKeyDown} className={cx("relative", className)} role="region" aria-roledescription="carousel" {...props}>
|
|
140
|
+
{children}
|
|
141
|
+
</div>
|
|
142
|
+
</CarouselContext.Provider>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
interface CarouselContentProps extends ComponentPropsWithRef<"div"> {
|
|
147
|
+
/** The class name of the content. */
|
|
148
|
+
className?: string;
|
|
149
|
+
/** Whether to hide the overflow. */
|
|
150
|
+
overflowHidden?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const CarouselContent = ({ className, overflowHidden = true, ...props }: CarouselContentProps) => {
|
|
154
|
+
const { carouselRef, orientation } = useCarousel();
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div ref={carouselRef} className={cx("h-full w-full", overflowHidden && "overflow-hidden")}>
|
|
158
|
+
<div className={cx("flex max-h-full", orientation === "horizontal" ? "" : "flex-col", className)} {...props} />
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const CarouselItem = ({ className, ...props }: ComponentPropsWithRef<"div">) => {
|
|
164
|
+
return <div role="group" aria-roledescription="slide" className={cx("min-w-0 shrink-0 grow-0 basis-full", className)} {...props} />;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
interface TriggerRenderProps {
|
|
168
|
+
isDisabled: boolean;
|
|
169
|
+
onClick: () => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface TriggerProps {
|
|
173
|
+
/** The ref of the trigger. */
|
|
174
|
+
ref?: Ref<HTMLButtonElement>;
|
|
175
|
+
/** If true, the child element will be cloned and passed down the prop of the trigger. */
|
|
176
|
+
asChild?: boolean;
|
|
177
|
+
/** The direction of the trigger. */
|
|
178
|
+
direction: "prev" | "next";
|
|
179
|
+
/** The children of the trigger. Can be a render prop or a valid element. */
|
|
180
|
+
children: ReactNode | ((props: TriggerRenderProps) => ReactNode);
|
|
181
|
+
/** The style of the trigger. */
|
|
182
|
+
style?: CSSProperties;
|
|
183
|
+
/** The class name of the trigger. */
|
|
184
|
+
className?: string | ((args: { isDisabled: boolean }) => string);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const Trigger = ({ className, children, asChild, direction, style, ...props }: TriggerProps) => {
|
|
188
|
+
const { scrollPrev, canScrollNext, scrollNext, canScrollPrev } = useCarousel();
|
|
189
|
+
|
|
190
|
+
const isDisabled = direction === "prev" ? !canScrollPrev : !canScrollNext;
|
|
191
|
+
|
|
192
|
+
const handleClick = () => {
|
|
193
|
+
if (isDisabled) return;
|
|
194
|
+
|
|
195
|
+
direction === "prev" ? scrollPrev() : scrollNext();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const computedClassName = typeof className === "function" ? className({ isDisabled }) : className;
|
|
199
|
+
|
|
200
|
+
const defaultAriaLabel = direction === "prev" ? "Previous slide" : "Next slide";
|
|
201
|
+
|
|
202
|
+
// If the children is a render prop, we need to pass the necessary props to the render prop.
|
|
203
|
+
if (typeof children === "function") {
|
|
204
|
+
return <>{children({ isDisabled, onClick: handleClick })}</>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// If the children is a valid element, we need to clone it and pass the necessary props to the cloned element.
|
|
208
|
+
if (asChild && isValidElement(children)) {
|
|
209
|
+
return cloneElement(children, {
|
|
210
|
+
onClick: handleClick,
|
|
211
|
+
disabled: isDisabled,
|
|
212
|
+
"aria-label": defaultAriaLabel,
|
|
213
|
+
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
|
214
|
+
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
|
215
|
+
} as HTMLAttributes<HTMLElement>);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<button aria-label={defaultAriaLabel} disabled={isDisabled} className={computedClassName} onClick={handleClick} {...props}>
|
|
220
|
+
{children}
|
|
221
|
+
</button>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const CarouselPrevTrigger = (props: Omit<TriggerProps, "direction">) => <Trigger {...props} direction="prev" />;
|
|
226
|
+
|
|
227
|
+
const CarouselNextTrigger = (props: Omit<TriggerProps, "direction">) => <Trigger {...props} direction="next" />;
|
|
228
|
+
|
|
229
|
+
interface CarouselIndicatorRenderProps {
|
|
230
|
+
isSelected: boolean;
|
|
231
|
+
onClick: () => void;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface CarouselIndicatorProps {
|
|
235
|
+
/** The index of the indicator. */
|
|
236
|
+
index: number;
|
|
237
|
+
/** If true, the child element will be cloned and passed down the prop of the indicator. */
|
|
238
|
+
asChild?: boolean;
|
|
239
|
+
/** If true, the indicator will be selected. */
|
|
240
|
+
isSelected?: boolean;
|
|
241
|
+
/** The children of the indicator. Can be a render prop or a valid element. */
|
|
242
|
+
children?: ReactNode | ((props: CarouselIndicatorRenderProps) => ReactNode);
|
|
243
|
+
/** The style of the indicator. */
|
|
244
|
+
style?: CSSProperties;
|
|
245
|
+
/** The class name of the indicator. */
|
|
246
|
+
className?: string | ((args: { isSelected: boolean }) => string);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const CarouselIndicator = ({ index, isSelected = false, children, asChild, className, style }: CarouselIndicatorProps) => {
|
|
250
|
+
const { api, selectedIndex } = useCarousel();
|
|
251
|
+
|
|
252
|
+
isSelected = isSelected || selectedIndex === index;
|
|
253
|
+
|
|
254
|
+
const handleClick = () => {
|
|
255
|
+
api?.scrollTo(index);
|
|
256
|
+
};
|
|
257
|
+
const computedClassName = typeof className === "function" ? className({ isSelected }) : className;
|
|
258
|
+
|
|
259
|
+
const defaultAriaLabel = "Go to slide" + (index + 1);
|
|
260
|
+
|
|
261
|
+
// If the children is a render prop, we need to pass the necessary props to the render prop.
|
|
262
|
+
if (typeof children === "function") {
|
|
263
|
+
return <>{children({ isSelected, onClick: handleClick })}</>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// If the children is a valid element, we need to clone it and pass the necessary props to the cloned element.
|
|
267
|
+
if (asChild && isValidElement(children)) {
|
|
268
|
+
return cloneElement(children, {
|
|
269
|
+
onClick: handleClick,
|
|
270
|
+
"aria-label": defaultAriaLabel,
|
|
271
|
+
"aria-current": isSelected ? "true" : undefined,
|
|
272
|
+
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
|
273
|
+
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
|
274
|
+
} as HTMLAttributes<HTMLElement>);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<button aria-label={defaultAriaLabel} aria-current={isSelected ? "true" : undefined} className={computedClassName} onClick={handleClick}>
|
|
279
|
+
{children}
|
|
280
|
+
</button>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
interface CarouselIndicatorGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
285
|
+
children: ReactNode | ((props: { index: number }) => ReactNode);
|
|
286
|
+
className?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const CarouselIndicatorGroup = ({ children, ...props }: CarouselIndicatorGroupProps) => {
|
|
290
|
+
const { scrollSnaps } = useCarousel();
|
|
291
|
+
|
|
292
|
+
// If the children is a render prop, we need to pass the index to the render prop.
|
|
293
|
+
if (typeof children === "function") {
|
|
294
|
+
return <nav {...props}>{scrollSnaps.map((index) => children({ index }))}</nav>;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return <nav {...props}>{children}</nav>;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const Carousel = {
|
|
301
|
+
Root: CarouselRoot,
|
|
302
|
+
Content: CarouselContent,
|
|
303
|
+
Item: CarouselItem,
|
|
304
|
+
PrevTrigger: CarouselPrevTrigger,
|
|
305
|
+
NextTrigger: CarouselNextTrigger,
|
|
306
|
+
IndicatorGroup: CarouselIndicatorGroup,
|
|
307
|
+
Indicator: CarouselIndicator,
|
|
308
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode, Ref } from "react";
|
|
4
|
+
import { Checkbox as AriaCheckbox, type CheckboxProps as AriaCheckboxProps } from "react-aria-components";
|
|
5
|
+
import { cx } from '../../../utils/cx';
|
|
6
|
+
|
|
7
|
+
export interface CheckboxBaseProps {
|
|
8
|
+
size?: "sm" | "md";
|
|
9
|
+
className?: string;
|
|
10
|
+
isFocusVisible?: boolean;
|
|
11
|
+
isSelected?: boolean;
|
|
12
|
+
isDisabled?: boolean;
|
|
13
|
+
isIndeterminate?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CheckboxBase = ({ className, isSelected, isDisabled, isIndeterminate, size = "sm", isFocusVisible = false }: CheckboxBaseProps) => {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cx(
|
|
20
|
+
"flex size-4 shrink-0 cursor-pointer appearance-none items-center justify-center rounded bg-primary ring-1 ring-primary ring-inset",
|
|
21
|
+
size === "md" && "size-5 rounded-md",
|
|
22
|
+
(isSelected || isIndeterminate) && "bg-brand-solid ring-bg-brand-solid",
|
|
23
|
+
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
|
24
|
+
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<svg
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
viewBox="0 0 14 14"
|
|
31
|
+
fill="none"
|
|
32
|
+
className={cx(
|
|
33
|
+
"pointer-events-none absolute h-3 w-2.5 text-fg-white opacity-0 transition-inherit-all",
|
|
34
|
+
size === "md" && "size-3.5",
|
|
35
|
+
isIndeterminate && "opacity-100",
|
|
36
|
+
isDisabled && "text-fg-disabled_subtle",
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<path d="M2.91675 7H11.0834" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
40
|
+
</svg>
|
|
41
|
+
|
|
42
|
+
<svg
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
viewBox="0 0 14 14"
|
|
45
|
+
fill="none"
|
|
46
|
+
className={cx(
|
|
47
|
+
"pointer-events-none absolute size-3 text-fg-white opacity-0 transition-inherit-all",
|
|
48
|
+
size === "md" && "size-3.5",
|
|
49
|
+
isSelected && !isIndeterminate && "opacity-100",
|
|
50
|
+
isDisabled && "text-fg-disabled_subtle",
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<path d="M11.6666 3.5L5.24992 9.91667L2.33325 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
CheckboxBase.displayName = "CheckboxBase";
|
|
59
|
+
|
|
60
|
+
interface CheckboxProps extends AriaCheckboxProps {
|
|
61
|
+
ref?: Ref<HTMLLabelElement>;
|
|
62
|
+
size?: "sm" | "md";
|
|
63
|
+
label?: ReactNode;
|
|
64
|
+
hint?: ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Checkbox = ({ label, hint, size = "sm", className, ...ariaCheckboxProps }: CheckboxProps) => {
|
|
68
|
+
const sizes = {
|
|
69
|
+
sm: {
|
|
70
|
+
root: "gap-2",
|
|
71
|
+
textWrapper: "",
|
|
72
|
+
label: "text-sm font-medium",
|
|
73
|
+
hint: "text-sm",
|
|
74
|
+
},
|
|
75
|
+
md: {
|
|
76
|
+
root: "gap-3",
|
|
77
|
+
textWrapper: "gap-0.5",
|
|
78
|
+
label: "text-md font-medium",
|
|
79
|
+
hint: "text-md",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<AriaCheckbox
|
|
85
|
+
{...ariaCheckboxProps}
|
|
86
|
+
className={(state) =>
|
|
87
|
+
cx(
|
|
88
|
+
"flex items-start",
|
|
89
|
+
state.isDisabled && "cursor-not-allowed",
|
|
90
|
+
sizes[size].root,
|
|
91
|
+
typeof className === "function" ? className(state) : className,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
{({ isSelected, isIndeterminate, isDisabled, isFocusVisible }) => (
|
|
96
|
+
<>
|
|
97
|
+
<CheckboxBase
|
|
98
|
+
size={size}
|
|
99
|
+
isSelected={isSelected}
|
|
100
|
+
isIndeterminate={isIndeterminate}
|
|
101
|
+
isDisabled={isDisabled}
|
|
102
|
+
isFocusVisible={isFocusVisible}
|
|
103
|
+
className={label || hint ? "mt-0.5" : ""}
|
|
104
|
+
/>
|
|
105
|
+
{(label || hint) && (
|
|
106
|
+
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
|
107
|
+
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
|
108
|
+
{hint && (
|
|
109
|
+
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
|
110
|
+
{hint}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
</AriaCheckbox>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
Checkbox.displayName = "Checkbox";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, PropsWithChildren } from "react";
|
|
4
|
+
import { Fragment, useContext, useState } from "react";
|
|
5
|
+
import { type CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
|
|
6
|
+
import { ChevronLeft, ChevronRight } from "@untitledui/icons";
|
|
7
|
+
import type { CalendarProps as AriaCalendarProps, DateValue } from "react-aria-components";
|
|
8
|
+
import {
|
|
9
|
+
Calendar as AriaCalendar,
|
|
10
|
+
CalendarContext as AriaCalendarContext,
|
|
11
|
+
CalendarGrid as AriaCalendarGrid,
|
|
12
|
+
CalendarGridBody as AriaCalendarGridBody,
|
|
13
|
+
CalendarGridHeader as AriaCalendarGridHeader,
|
|
14
|
+
CalendarHeaderCell as AriaCalendarHeaderCell,
|
|
15
|
+
CalendarStateContext as AriaCalendarStateContext,
|
|
16
|
+
Heading as AriaHeading,
|
|
17
|
+
useSlottedContext,
|
|
18
|
+
} from "react-aria-components";
|
|
19
|
+
import { Button } from '../buttons/button';
|
|
20
|
+
import { cx } from '../../../utils/cx';
|
|
21
|
+
import { CalendarCell } from "./cell";
|
|
22
|
+
import { DateInput } from "./date-input";
|
|
23
|
+
|
|
24
|
+
export const CalendarContextProvider = ({ children }: PropsWithChildren) => {
|
|
25
|
+
const [value, onChange] = useState<DateValue | null>(null);
|
|
26
|
+
const [focusedValue, onFocusChange] = useState<DateValue | undefined>();
|
|
27
|
+
|
|
28
|
+
return <AriaCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }}>{children}</AriaCalendarContext.Provider>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const PresetButton = ({ value, children, ...props }: HTMLAttributes<HTMLButtonElement> & { value: CalendarDate }) => {
|
|
32
|
+
const context = useContext(AriaCalendarStateContext);
|
|
33
|
+
|
|
34
|
+
if (!context) {
|
|
35
|
+
throw new Error("Preset must be used within a Calendar component");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleClick = () => {
|
|
39
|
+
context.setValue(value);
|
|
40
|
+
context.setFocusedDate(value);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Button
|
|
45
|
+
{...props}
|
|
46
|
+
// It's important to give `null` explicitly to the `slot` prop
|
|
47
|
+
// otherwise the button will throw an error due to not using one of
|
|
48
|
+
// the required slots inside the Calendar component.
|
|
49
|
+
// Passing `null` will tell the button to not use a slot context.
|
|
50
|
+
slot={null}
|
|
51
|
+
size="md"
|
|
52
|
+
color="secondary"
|
|
53
|
+
onClick={handleClick}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</Button>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
interface CalendarProps extends AriaCalendarProps<DateValue> {
|
|
61
|
+
/** The dates to highlight. */
|
|
62
|
+
highlightedDates?: DateValue[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const Calendar = ({ highlightedDates, className, ...props }: CalendarProps) => {
|
|
66
|
+
const context = useSlottedContext(AriaCalendarContext)!;
|
|
67
|
+
|
|
68
|
+
const ContextWrapper = context ? Fragment : CalendarContextProvider;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<ContextWrapper>
|
|
72
|
+
<AriaCalendar {...props} className={(state) => cx("flex flex-col gap-3", typeof className === "function" ? className(state) : className)}>
|
|
73
|
+
<header className="flex items-center justify-between">
|
|
74
|
+
<Button slot="previous" iconLeading={ChevronLeft} size="sm" color="tertiary" className="size-8" />
|
|
75
|
+
<AriaHeading className="text-sm font-semibold text-fg-secondary" />
|
|
76
|
+
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8" />
|
|
77
|
+
</header>
|
|
78
|
+
|
|
79
|
+
<div className="flex gap-3">
|
|
80
|
+
<DateInput className="flex-1" />
|
|
81
|
+
<PresetButton value={today(getLocalTimeZone())}>Today</PresetButton>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<AriaCalendarGrid weekdayStyle="short" className="w-max">
|
|
85
|
+
<AriaCalendarGridHeader className="border-b-4 border-transparent">
|
|
86
|
+
{(day) => (
|
|
87
|
+
<AriaCalendarHeaderCell className="p-0">
|
|
88
|
+
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
|
|
89
|
+
</AriaCalendarHeaderCell>
|
|
90
|
+
)}
|
|
91
|
+
</AriaCalendarGridHeader>
|
|
92
|
+
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
|
|
93
|
+
{(date) => (
|
|
94
|
+
<CalendarCell date={date} isHighlighted={highlightedDates?.some((highlightedDate) => date.compare(highlightedDate) === 0)} />
|
|
95
|
+
)}
|
|
96
|
+
</AriaCalendarGridBody>
|
|
97
|
+
</AriaCalendarGrid>
|
|
98
|
+
</AriaCalendar>
|
|
99
|
+
</ContextWrapper>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getDayOfWeek, getLocalTimeZone, isToday } from "@internationalized/date";
|
|
4
|
+
import type { CalendarCellProps as AriaCalendarCellProps } from "react-aria-components";
|
|
5
|
+
import { CalendarCell as AriaCalendarCell, RangeCalendarContext, useLocale, useSlottedContext } from "react-aria-components";
|
|
6
|
+
import { cx } from '../../../utils/cx';
|
|
7
|
+
|
|
8
|
+
interface CalendarCellProps extends AriaCalendarCellProps {
|
|
9
|
+
/** Whether the calendar is a range calendar. */
|
|
10
|
+
isRangeCalendar?: boolean;
|
|
11
|
+
/** Whether the cell is highlighted. */
|
|
12
|
+
isHighlighted?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const CalendarCell = ({ date, isHighlighted, ...props }: CalendarCellProps) => {
|
|
16
|
+
const { locale } = useLocale();
|
|
17
|
+
const dayOfWeek = getDayOfWeek(date, locale);
|
|
18
|
+
const rangeCalendarContext = useSlottedContext(RangeCalendarContext);
|
|
19
|
+
|
|
20
|
+
const isRangeCalendar = !!rangeCalendarContext;
|
|
21
|
+
|
|
22
|
+
const start = rangeCalendarContext?.value?.start;
|
|
23
|
+
const end = rangeCalendarContext?.value?.end;
|
|
24
|
+
|
|
25
|
+
const isAfterStart = start ? date.compare(start) > 0 : true;
|
|
26
|
+
const isBeforeEnd = end ? date.compare(end) < 0 : true;
|
|
27
|
+
|
|
28
|
+
const isAfterOrOnStart = start && date.compare(start) >= 0;
|
|
29
|
+
const isBeforeOrOnEnd = end && date.compare(end) <= 0;
|
|
30
|
+
const isInRange = isAfterOrOnStart && isBeforeOrOnEnd;
|
|
31
|
+
|
|
32
|
+
const lastDayOfMonth = new Date(date.year, date.month, 0).getDate();
|
|
33
|
+
const isLastDayOfMonth = date.day === lastDayOfMonth;
|
|
34
|
+
const isFirstDayOfMonth = date.day === 1;
|
|
35
|
+
|
|
36
|
+
const isTodayDate = isToday(date, getLocalTimeZone());
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<AriaCalendarCell
|
|
40
|
+
{...props}
|
|
41
|
+
date={date}
|
|
42
|
+
className={({ isDisabled, isFocusVisible, isSelectionStart, isSelectionEnd, isSelected, isOutsideMonth }) => {
|
|
43
|
+
const isRoundedLeft = isSelectionStart || dayOfWeek === 0;
|
|
44
|
+
const isRoundedRight = isSelectionEnd || dayOfWeek === 6;
|
|
45
|
+
|
|
46
|
+
return cx(
|
|
47
|
+
"relative size-10 focus:outline-none",
|
|
48
|
+
isRoundedLeft && "rounded-l-full",
|
|
49
|
+
isRoundedRight && "rounded-r-full",
|
|
50
|
+
isInRange && isDisabled && "bg-active",
|
|
51
|
+
isSelected && isRangeCalendar && "bg-active",
|
|
52
|
+
isDisabled ? "pointer-events-none" : "cursor-pointer",
|
|
53
|
+
isFocusVisible ? "z-10" : "z-0",
|
|
54
|
+
isRangeCalendar && isOutsideMonth && "hidden",
|
|
55
|
+
|
|
56
|
+
// Show gradient on last day of month if it's within the selected range.
|
|
57
|
+
isLastDayOfMonth &&
|
|
58
|
+
isSelected &&
|
|
59
|
+
isBeforeEnd &&
|
|
60
|
+
isRangeCalendar &&
|
|
61
|
+
"after:absolute after:inset-0 after:translate-x-full after:bg-gradient-to-l after:from-transparent after:to-bg-active in-[[role=gridcell]:last-child]:after:hidden",
|
|
62
|
+
|
|
63
|
+
// Show gradient on first day of month if it's within the selected range.
|
|
64
|
+
isFirstDayOfMonth &&
|
|
65
|
+
isSelected &&
|
|
66
|
+
isAfterStart &&
|
|
67
|
+
isRangeCalendar &&
|
|
68
|
+
"after:absolute after:inset-0 after:-translate-x-full after:bg-gradient-to-r after:from-transparent after:to-bg-active in-[[role=gridcell]:first-child]:after:hidden",
|
|
69
|
+
);
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{({ isDisabled, isFocusVisible, isSelectionStart, isSelectionEnd, isSelected, formattedDate }) => {
|
|
73
|
+
const markedAsSelected = isSelectionStart || isSelectionEnd || (isSelected && !isDisabled && !isRangeCalendar);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
className={cx(
|
|
78
|
+
"relative flex size-full items-center justify-center rounded-full text-sm",
|
|
79
|
+
// Disabled state.
|
|
80
|
+
isDisabled ? "text-disabled" : "text-secondary hover:text-secondary_hover",
|
|
81
|
+
// Focus ring, visible while the cell has keyboard focus.
|
|
82
|
+
isFocusVisible ? "outline-2 outline-offset-2 outline-focus-ring" : "",
|
|
83
|
+
// Hover state for cells in the middle of the range.
|
|
84
|
+
isSelected && !isDisabled && isRangeCalendar ? "font-medium" : "",
|
|
85
|
+
markedAsSelected && "bg-brand-solid font-medium text-white hover:bg-brand-solid_hover hover:text-white",
|
|
86
|
+
// Hover state for non-selected cells.
|
|
87
|
+
!isSelected && !isDisabled ? "hover:bg-primary_hover hover:font-medium!" : "",
|
|
88
|
+
!isSelected && isTodayDate ? "bg-active font-medium hover:bg-secondary_hover" : "",
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
{formattedDate}
|
|
92
|
+
|
|
93
|
+
{(isHighlighted || isTodayDate) && (
|
|
94
|
+
<div
|
|
95
|
+
className={cx(
|
|
96
|
+
"absolute bottom-1 left-1/2 size-1.25 -translate-x-1/2 rounded-full",
|
|
97
|
+
isDisabled ? "bg-fg-disabled" : markedAsSelected ? "bg-fg-white" : "bg-fg-brand-primary",
|
|
98
|
+
)}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}}
|
|
104
|
+
</AriaCalendarCell>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { DateInputProps as AriaDateInputProps } from "react-aria-components";
|
|
4
|
+
import { DateInput as AriaDateInput, DateSegment as AriaDateSegment } from "react-aria-components";
|
|
5
|
+
import { cx } from '../../../utils/cx';
|
|
6
|
+
|
|
7
|
+
interface DateInputProps extends Omit<AriaDateInputProps, "children"> {}
|
|
8
|
+
|
|
9
|
+
export const DateInput = (props: DateInputProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<AriaDateInput
|
|
12
|
+
{...props}
|
|
13
|
+
className={cx(
|
|
14
|
+
"flex rounded-lg bg-primary px-2.5 py-2 text-md shadow-xs ring-1 ring-primary ring-inset focus-within:ring-2 focus-within:ring-brand",
|
|
15
|
+
typeof props.className === "string" && props.className,
|
|
16
|
+
)}
|
|
17
|
+
>
|
|
18
|
+
{(segment) => (
|
|
19
|
+
<AriaDateSegment
|
|
20
|
+
segment={segment}
|
|
21
|
+
className={cx(
|
|
22
|
+
"rounded px-0.5 text-primary tabular-nums caret-transparent focus:bg-brand-solid focus:font-medium focus:text-white focus:outline-hidden",
|
|
23
|
+
// The placeholder segment.
|
|
24
|
+
segment.isPlaceholder && "text-placeholder uppercase",
|
|
25
|
+
// The separator "/" segment.
|
|
26
|
+
segment.type === "literal" && "text-fg-quaternary",
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
)}
|
|
30
|
+
</AriaDateInput>
|
|
31
|
+
);
|
|
32
|
+
};
|