keystone-design-bootstrap 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +179 -0
  2. package/package.json +59 -0
  3. package/src/contexts/ThemeContext.tsx +34 -0
  4. package/src/contexts/index.ts +1 -0
  5. package/src/design_system/elements/IconComponent.tsx +98 -0
  6. package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
  7. package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
  8. package/src/design_system/elements/avatar/avatar.tsx +131 -0
  9. package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
  10. package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
  11. package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
  12. package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
  13. package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
  14. package/src/design_system/elements/avatar/utils.ts +12 -0
  15. package/src/design_system/elements/badges/avatar.tsx +132 -0
  16. package/src/design_system/elements/badges/badge-groups.tsx +176 -0
  17. package/src/design_system/elements/badges/badge-types.ts +266 -0
  18. package/src/design_system/elements/badges/badges.tsx +430 -0
  19. package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
  20. package/src/design_system/elements/button-group/button-group.tsx +106 -0
  21. package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
  22. package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
  23. package/src/design_system/elements/buttons/button-utility.tsx +116 -0
  24. package/src/design_system/elements/buttons/button.aman.tsx +174 -0
  25. package/src/design_system/elements/buttons/button.tsx +271 -0
  26. package/src/design_system/elements/buttons/close-button.tsx +42 -0
  27. package/src/design_system/elements/buttons/round-button.tsx +29 -0
  28. package/src/design_system/elements/buttons/social-button.tsx +148 -0
  29. package/src/design_system/elements/buttons/social-logos.tsx +115 -0
  30. package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
  31. package/src/design_system/elements/carousel/carousel.tsx +308 -0
  32. package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
  33. package/src/design_system/elements/date-picker/calendar.tsx +101 -0
  34. package/src/design_system/elements/date-picker/cell.tsx +106 -0
  35. package/src/design_system/elements/date-picker/date-input.tsx +32 -0
  36. package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
  37. package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
  38. package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
  39. package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
  40. package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
  41. package/src/design_system/elements/form/form.tsx +10 -0
  42. package/src/design_system/elements/form/hook-form.tsx +75 -0
  43. package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
  44. package/src/design_system/elements/index.tsx +158 -0
  45. package/src/design_system/elements/input/hint-text.tsx +33 -0
  46. package/src/design_system/elements/input/input-group.tsx +133 -0
  47. package/src/design_system/elements/input/input.aman.tsx +172 -0
  48. package/src/design_system/elements/input/input.tsx +271 -0
  49. package/src/design_system/elements/input/label.tsx +50 -0
  50. package/src/design_system/elements/label/label.tsx +50 -0
  51. package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
  52. package/src/design_system/elements/map/GoogleMap.tsx +286 -0
  53. package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
  54. package/src/design_system/elements/modals/modal.tsx +41 -0
  55. package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
  56. package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
  57. package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
  58. package/src/design_system/elements/pagination/pagination.tsx +330 -0
  59. package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
  60. package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
  61. package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
  62. package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
  63. package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
  64. package/src/design_system/elements/rating/rating-badge.tsx +144 -0
  65. package/src/design_system/elements/rating/rating-stars.tsx +77 -0
  66. package/src/design_system/elements/select/combobox.tsx +152 -0
  67. package/src/design_system/elements/select/multi-select.tsx +363 -0
  68. package/src/design_system/elements/select/popover.tsx +34 -0
  69. package/src/design_system/elements/select/select-item.tsx +97 -0
  70. package/src/design_system/elements/select/select-native.tsx +69 -0
  71. package/src/design_system/elements/select/select.aman.tsx +75 -0
  72. package/src/design_system/elements/select/select.tsx +146 -0
  73. package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
  74. package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
  75. package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
  76. package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
  77. package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
  78. package/src/design_system/elements/tabs/tabs.tsx +225 -0
  79. package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
  80. package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
  81. package/src/design_system/elements/tags/tags.tsx +176 -0
  82. package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
  83. package/src/design_system/elements/textarea/textarea.tsx +111 -0
  84. package/src/design_system/elements/toggle/toggle.tsx +140 -0
  85. package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
  86. package/src/design_system/hooks/use-breakpoint.ts +37 -0
  87. package/src/design_system/hooks/use-resize-observer.ts +68 -0
  88. package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
  89. package/src/design_system/logo/keystone-logo.tsx +22 -0
  90. package/src/design_system/sections/about-home.aman.tsx +85 -0
  91. package/src/design_system/sections/about-home.tsx +115 -0
  92. package/src/design_system/sections/blog-cards.tsx +848 -0
  93. package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
  94. package/src/design_system/sections/blog-gallery.tsx +204 -0
  95. package/src/design_system/sections/blog-home.aman.tsx +84 -0
  96. package/src/design_system/sections/blog-home.tsx +153 -0
  97. package/src/design_system/sections/blog-post.aman.tsx +74 -0
  98. package/src/design_system/sections/blog-post.tsx +301 -0
  99. package/src/design_system/sections/blog-section.aman.tsx +101 -0
  100. package/src/design_system/sections/blog-section.tsx +179 -0
  101. package/src/design_system/sections/contact-home.tsx +25 -0
  102. package/src/design_system/sections/contact-section.aman.tsx +173 -0
  103. package/src/design_system/sections/contact-section.tsx +143 -0
  104. package/src/design_system/sections/faq-grid.aman.tsx +79 -0
  105. package/src/design_system/sections/faq-grid.tsx +102 -0
  106. package/src/design_system/sections/faq-home.aman.tsx +92 -0
  107. package/src/design_system/sections/faq-home.tsx +134 -0
  108. package/src/design_system/sections/feature-tab.tsx +43 -0
  109. package/src/design_system/sections/feature-text.tsx +284 -0
  110. package/src/design_system/sections/footer-home.aman.tsx +62 -0
  111. package/src/design_system/sections/footer-home.tsx +259 -0
  112. package/src/design_system/sections/generic-header-component.tsx +103 -0
  113. package/src/design_system/sections/header-navigation.aman.tsx +360 -0
  114. package/src/design_system/sections/header-navigation.tsx +334 -0
  115. package/src/design_system/sections/hero-faq.aman.tsx +38 -0
  116. package/src/design_system/sections/hero-faq.tsx +55 -0
  117. package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
  118. package/src/design_system/sections/hero-generic-text.tsx +51 -0
  119. package/src/design_system/sections/hero-home.aman.tsx +84 -0
  120. package/src/design_system/sections/hero-home.tsx +246 -0
  121. package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
  122. package/src/design_system/sections/hero-location-detail.tsx +72 -0
  123. package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
  124. package/src/design_system/sections/hero-service-detail.tsx +51 -0
  125. package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
  126. package/src/design_system/sections/hero-social-media.tsx +35 -0
  127. package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
  128. package/src/design_system/sections/hero-testimonials.tsx +55 -0
  129. package/src/design_system/sections/home-hero-component.tsx +228 -0
  130. package/src/design_system/sections/index.tsx +131 -0
  131. package/src/design_system/sections/job-gallery.aman.tsx +91 -0
  132. package/src/design_system/sections/job-gallery.tsx +183 -0
  133. package/src/design_system/sections/location-details-section.aman.tsx +179 -0
  134. package/src/design_system/sections/location-details-section.tsx +196 -0
  135. package/src/design_system/sections/location-grid.aman.tsx +76 -0
  136. package/src/design_system/sections/location-grid.tsx +123 -0
  137. package/src/design_system/sections/services-grid.aman.tsx +85 -0
  138. package/src/design_system/sections/services-grid.tsx +104 -0
  139. package/src/design_system/sections/services-home.aman.tsx +78 -0
  140. package/src/design_system/sections/services-home.tsx +131 -0
  141. package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
  142. package/src/design_system/sections/social-media-grid.tsx +189 -0
  143. package/src/design_system/sections/statistics-section.aman.tsx +79 -0
  144. package/src/design_system/sections/statistics-section.tsx +97 -0
  145. package/src/design_system/sections/team-grid.aman.tsx +85 -0
  146. package/src/design_system/sections/team-grid.tsx +88 -0
  147. package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
  148. package/src/design_system/sections/testimonials-home.tsx +90 -0
  149. package/src/design_system/sections/values-section.aman.tsx +73 -0
  150. package/src/design_system/sections/values-section.tsx +128 -0
  151. package/src/design_system/utils/icon-mapping.tsx +28 -0
  152. package/src/index.ts +7 -0
  153. package/src/lib/component-registry.ts +53 -0
  154. package/src/lib/hooks/index.ts +8 -0
  155. package/src/lib/hooks/use-breakpoint.ts +37 -0
  156. package/src/lib/hooks/use-clipboard.ts +79 -0
  157. package/src/lib/hooks/use-resize-observer.ts +68 -0
  158. package/src/lib/server-api.ts +115 -0
  159. package/src/styles/style-overrides.aman.css +101 -0
  160. package/src/styles/theme.css +224 -0
  161. package/src/styles/typography.css +430 -0
  162. package/src/themes/index.ts +23 -0
  163. package/src/types/api/blog-post.ts +53 -0
  164. package/src/types/api/company-information.ts +44 -0
  165. package/src/types/api/contact.ts +63 -0
  166. package/src/types/api/faq.ts +37 -0
  167. package/src/types/api/job-posting.ts +34 -0
  168. package/src/types/api/location.ts +36 -0
  169. package/src/types/api/photos.ts +28 -0
  170. package/src/types/api/service.ts +37 -0
  171. package/src/types/api/social-post.ts +28 -0
  172. package/src/types/api/team-member.ts +29 -0
  173. package/src/types/api/testimonial.ts +29 -0
  174. package/src/types/api/website-photos.ts +22 -0
  175. package/src/types/config.ts +21 -0
  176. package/src/types/index.ts +21 -0
  177. package/src/utils/countries.tsx +1351 -0
  178. package/src/utils/cx.ts +25 -0
  179. package/src/utils/gradient-placeholder.ts +59 -0
  180. package/src/utils/is-react-component.ts +33 -0
  181. package/src/utils/markdown-toc.ts +54 -0
  182. package/src/utils/photo-helpers.ts +94 -0
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { getLocalTimeZone, today } from "@internationalized/date";
4
+ import { useControlledState } from "@react-stately/utils";
5
+ import { Calendar as CalendarIcon } from "@untitledui/icons";
6
+ import { useDateFormatter } from "react-aria";
7
+ import type { DatePickerProps as AriaDatePickerProps, DateValue } from "react-aria-components";
8
+ import { DatePicker as AriaDatePicker, Dialog as AriaDialog, Group as AriaGroup, Popover as AriaPopover } from "react-aria-components";
9
+ import { Button } from '../buttons/button';
10
+ import { cx } from '../../../utils/cx';
11
+ import { Calendar } from "./calendar";
12
+
13
+ const highlightedDates = [today(getLocalTimeZone())];
14
+
15
+ interface DatePickerProps extends AriaDatePickerProps<DateValue> {
16
+ /** The function to call when the apply button is clicked. */
17
+ onApply?: () => void;
18
+ /** The function to call when the cancel button is clicked. */
19
+ onCancel?: () => void;
20
+ }
21
+
22
+ export const DatePicker = ({ value: valueProp, defaultValue, onChange, onApply, onCancel, ...props }: DatePickerProps) => {
23
+ const formatter = useDateFormatter({
24
+ month: "short",
25
+ day: "numeric",
26
+ year: "numeric",
27
+ });
28
+ const [value, setValue] = useControlledState(valueProp, defaultValue || null, onChange);
29
+
30
+ const formattedDate = value ? formatter.format(value.toDate(getLocalTimeZone())) : "Select date";
31
+
32
+ return (
33
+ <AriaDatePicker shouldCloseOnSelect={false} {...props} value={value} onChange={setValue}>
34
+ <AriaGroup>
35
+ <Button size="md" color="secondary" iconLeading={CalendarIcon}>
36
+ {formattedDate}
37
+ </Button>
38
+ </AriaGroup>
39
+ <AriaPopover
40
+ offset={8}
41
+ placement="bottom right"
42
+ className={({ isEntering, isExiting }) =>
43
+ cx(
44
+ "origin-(--trigger-anchor-point) will-change-transform",
45
+ isEntering &&
46
+ "duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
47
+ isExiting &&
48
+ "duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
49
+ )
50
+ }
51
+ >
52
+ <AriaDialog className="rounded-2xl bg-primary shadow-xl ring ring-secondary_alt">
53
+ {({ close }) => (
54
+ <>
55
+ <div className="flex px-6 py-5">
56
+ <Calendar highlightedDates={highlightedDates} />
57
+ </div>
58
+ <div className="grid grid-cols-2 gap-3 border-t border-secondary p-4">
59
+ <Button
60
+ size="md"
61
+ color="secondary"
62
+ onClick={() => {
63
+ onCancel?.();
64
+ close();
65
+ }}
66
+ >
67
+ Cancel
68
+ </Button>
69
+ <Button
70
+ size="md"
71
+ color="primary"
72
+ onClick={() => {
73
+ onApply?.();
74
+ close();
75
+ }}
76
+ >
77
+ Apply
78
+ </Button>
79
+ </div>
80
+ </>
81
+ )}
82
+ </AriaDialog>
83
+ </AriaPopover>
84
+ </AriaDatePicker>
85
+ );
86
+ };
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { endOfMonth, endOfWeek, getLocalTimeZone, startOfMonth, startOfWeek, today } from "@internationalized/date";
5
+ import { useControlledState } from "@react-stately/utils";
6
+ import { Calendar as CalendarIcon } from "@untitledui/icons";
7
+ import { useDateFormatter } from "react-aria";
8
+ import type { DateRangePickerProps as AriaDateRangePickerProps, DateValue } from "react-aria-components";
9
+ import { DateRangePicker as AriaDateRangePicker, Dialog as AriaDialog, Group as AriaGroup, Popover as AriaPopover, useLocale } from "react-aria-components";
10
+ import { Button } from '../buttons/button';
11
+ import { cx } from '../../../utils/cx';
12
+ import { DateInput } from "./date-input";
13
+ import { RangeCalendar } from "./range-calendar";
14
+ import { RangePresetButton } from "./range-preset";
15
+
16
+ const now = today(getLocalTimeZone());
17
+
18
+ const highlightedDates = [today(getLocalTimeZone())];
19
+
20
+ interface DateRangePickerProps extends AriaDateRangePickerProps<DateValue> {
21
+ /** The function to call when the apply button is clicked. */
22
+ onApply?: () => void;
23
+ /** The function to call when the cancel button is clicked. */
24
+ onCancel?: () => void;
25
+ }
26
+
27
+ export const DateRangePicker = ({ value: valueProp, defaultValue, onChange, onApply, onCancel, ...props }: DateRangePickerProps) => {
28
+ const { locale } = useLocale();
29
+ const formatter = useDateFormatter({
30
+ month: "short",
31
+ day: "numeric",
32
+ year: "numeric",
33
+ });
34
+ const [value, setValue] = useControlledState(valueProp, defaultValue || null, onChange);
35
+ const [focusedValue, setFocusedValue] = useState<DateValue | null>(null);
36
+
37
+ const formattedStartDate = value?.start ? formatter.format(value.start.toDate(getLocalTimeZone())) : "Select date";
38
+ const formattedEndDate = value?.end ? formatter.format(value.end.toDate(getLocalTimeZone())) : "Select date";
39
+
40
+ const presets = useMemo(
41
+ () => ({
42
+ today: { label: "Today", value: { start: now, end: now } },
43
+ yesterday: { label: "Yesterday", value: { start: now.subtract({ days: 1 }), end: now.subtract({ days: 1 }) } },
44
+ thisWeek: { label: "This week", value: { start: startOfWeek(now, locale), end: endOfWeek(now, locale) } },
45
+ lastWeek: {
46
+ label: "Last week",
47
+ value: {
48
+ start: startOfWeek(now, locale).subtract({ weeks: 1 }),
49
+ end: endOfWeek(now, locale).subtract({ weeks: 1 }),
50
+ },
51
+ },
52
+ thisMonth: { label: "This month", value: { start: startOfMonth(now), end: endOfMonth(now) } },
53
+ lastMonth: {
54
+ label: "Last month",
55
+ value: {
56
+ start: startOfMonth(now).subtract({ months: 1 }),
57
+ end: endOfMonth(now).subtract({ months: 1 }),
58
+ },
59
+ },
60
+ thisYear: { label: "This year", value: { start: startOfMonth(now.set({ month: 1 })), end: endOfMonth(now.set({ month: 12 })) } },
61
+ lastYear: {
62
+ label: "Last year",
63
+ value: {
64
+ start: startOfMonth(now.set({ month: 1 }).subtract({ years: 1 })),
65
+ end: endOfMonth(now.set({ month: 12 }).subtract({ years: 1 })),
66
+ },
67
+ },
68
+ allTime: {
69
+ label: "All time",
70
+ value: {
71
+ start: now.set({ year: 2000, month: 1, day: 1 }),
72
+ end: now,
73
+ },
74
+ },
75
+ }),
76
+ [locale],
77
+ );
78
+
79
+ return (
80
+ <AriaDateRangePicker aria-label="Date range picker" shouldCloseOnSelect={false} {...props} value={value} onChange={setValue}>
81
+ <AriaGroup>
82
+ <Button size="md" color="secondary" iconLeading={CalendarIcon}>
83
+ {!value ? <span className="text-placeholder">Select dates</span> : `${formattedStartDate} – ${formattedEndDate}`}
84
+ </Button>
85
+ </AriaGroup>
86
+ <AriaPopover
87
+ placement="bottom right"
88
+ offset={8}
89
+ className={({ isEntering, isExiting }) =>
90
+ cx(
91
+ "origin-(--trigger-anchor-point) will-change-transform",
92
+ isEntering &&
93
+ "duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
94
+ isExiting &&
95
+ "duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
96
+ )
97
+ }
98
+ >
99
+ <AriaDialog className="flex rounded-2xl bg-primary shadow-xl ring ring-secondary_alt focus:outline-hidden">
100
+ {({ close }) => (
101
+ <>
102
+ <div className="hidden w-38 flex-col gap-0.5 border-r border-solid border-secondary p-3 lg:flex">
103
+ {Object.values(presets).map((preset) => (
104
+ <RangePresetButton
105
+ key={preset.label}
106
+ value={preset.value}
107
+ onClick={() => {
108
+ setValue(preset.value);
109
+ setFocusedValue(preset.value.start);
110
+ }}
111
+ >
112
+ {preset.label}
113
+ </RangePresetButton>
114
+ ))}
115
+ </div>
116
+ <div className="flex flex-col">
117
+ <RangeCalendar
118
+ focusedValue={focusedValue}
119
+ onFocusChange={setFocusedValue}
120
+ highlightedDates={highlightedDates}
121
+ presets={{
122
+ lastWeek: presets.lastWeek,
123
+ lastMonth: presets.lastMonth,
124
+ lastYear: presets.lastYear,
125
+ }}
126
+ />
127
+ <div className="flex justify-between gap-3 border-t border-secondary p-4">
128
+ <div className="hidden items-center gap-3 md:flex">
129
+ <DateInput slot="start" className="w-36" />
130
+ <div className="text-md text-quaternary">–</div>
131
+ <DateInput slot="end" className="w-36" />
132
+ </div>
133
+ <div className="grid w-full grid-cols-2 gap-3 md:flex md:w-auto">
134
+ <Button
135
+ size="md"
136
+ color="secondary"
137
+ onClick={() => {
138
+ onCancel?.();
139
+ close();
140
+ }}
141
+ >
142
+ Cancel
143
+ </Button>
144
+ <Button
145
+ size="md"
146
+ color="primary"
147
+ onClick={() => {
148
+ onApply?.();
149
+ close();
150
+ }}
151
+ >
152
+ Apply
153
+ </Button>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </>
158
+ )}
159
+ </AriaDialog>
160
+ </AriaPopover>
161
+ </AriaDateRangePicker>
162
+ );
163
+ };
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ import type { HTMLAttributes, PropsWithChildren } from "react";
4
+ import { Fragment, useContext, useState } from "react";
5
+ import type { CalendarDate } from "@internationalized/date";
6
+ import { ChevronLeft, ChevronRight } from "@untitledui/icons";
7
+ import { useDateFormatter } from "react-aria";
8
+ import type { RangeCalendarProps as AriaRangeCalendarProps, DateValue } from "react-aria-components";
9
+ import {
10
+ CalendarGrid as AriaCalendarGrid,
11
+ CalendarGridBody as AriaCalendarGridBody,
12
+ CalendarGridHeader as AriaCalendarGridHeader,
13
+ CalendarHeaderCell as AriaCalendarHeaderCell,
14
+ RangeCalendar as AriaRangeCalendar,
15
+ RangeCalendarContext,
16
+ RangeCalendarStateContext,
17
+ useSlottedContext,
18
+ } from "react-aria-components";
19
+ import { Button } from '../buttons/button';
20
+ import { useBreakpoint } from '../../../lib/hooks/use-breakpoint';
21
+ import { CalendarCell } from "./cell";
22
+ import { DateInput } from "./date-input";
23
+
24
+ export const RangeCalendarContextProvider = ({ children }: PropsWithChildren) => {
25
+ const [value, onChange] = useState<{ start: DateValue; end: DateValue } | null>(null);
26
+ const [focusedValue, onFocusChange] = useState<DateValue | undefined>();
27
+
28
+ return <RangeCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }}>{children}</RangeCalendarContext.Provider>;
29
+ };
30
+
31
+ const RangeCalendarTitle = ({ part }: { part: "start" | "end" }) => {
32
+ const context = useContext(RangeCalendarStateContext);
33
+
34
+ if (!context) {
35
+ throw new Error("<RangeCalendarTitle /> must be used within a <RangeCalendar /> component.");
36
+ }
37
+
38
+ const formatter = useDateFormatter({
39
+ month: "long",
40
+ year: "numeric",
41
+ calendar: context.visibleRange.start.calendar.identifier,
42
+ timeZone: context.timeZone,
43
+ });
44
+
45
+ return part === "start"
46
+ ? formatter.format(context.visibleRange.start.toDate(context.timeZone))
47
+ : formatter.format(context.visibleRange.end.toDate(context.timeZone));
48
+ };
49
+
50
+ const MobilePresetButton = ({ value, children, ...props }: HTMLAttributes<HTMLButtonElement> & { value: { start: DateValue; end: DateValue } }) => {
51
+ const context = useContext(RangeCalendarStateContext);
52
+
53
+ return (
54
+ <Button
55
+ {...props}
56
+ slot={null}
57
+ size="sm"
58
+ color="link-color"
59
+ onClick={() => {
60
+ context?.setValue(value);
61
+ context?.setFocusedDate(value.start as CalendarDate);
62
+ }}
63
+ >
64
+ {children}
65
+ </Button>
66
+ );
67
+ };
68
+
69
+ interface RangeCalendarProps extends AriaRangeCalendarProps<DateValue> {
70
+ /** The dates to highlight. */
71
+ highlightedDates?: DateValue[];
72
+ /** The date presets to display. */
73
+ presets?: Record<string, { label: string; value: { start: DateValue; end: DateValue } }>;
74
+ }
75
+
76
+ export const RangeCalendar = ({ presets, ...props }: RangeCalendarProps) => {
77
+ const isDesktop = useBreakpoint("md");
78
+ const context = useSlottedContext(RangeCalendarContext);
79
+
80
+ const ContextWrapper = context ? Fragment : RangeCalendarContextProvider;
81
+
82
+ return (
83
+ <ContextWrapper>
84
+ <AriaRangeCalendar
85
+ className="flex items-start"
86
+ visibleDuration={{
87
+ months: isDesktop ? 2 : 1,
88
+ }}
89
+ {...props}
90
+ >
91
+ <div className="flex flex-col gap-3 px-6 py-5">
92
+ <header className="relative flex items-center justify-between md:justify-start">
93
+ <Button slot="previous" iconLeading={ChevronLeft} size="sm" color="tertiary" className="size-8" />
94
+
95
+ <h2 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm font-semibold text-fg-secondary">
96
+ <RangeCalendarTitle part="start" />
97
+ </h2>
98
+
99
+ <Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8 md:hidden" />
100
+ </header>
101
+
102
+ {!isDesktop && (
103
+ <div className="flex items-center gap-2 md:hidden">
104
+ <DateInput slot="start" className="flex-1" />
105
+ <div className="text-md text-quaternary">–</div>
106
+ <DateInput slot="end" className="flex-1" />
107
+ </div>
108
+ )}
109
+
110
+ {!isDesktop && presets && (
111
+ <div className="mt-1 flex justify-between gap-3 px-2 md:hidden">
112
+ {Object.values(presets).map((preset) => (
113
+ <MobilePresetButton key={preset.label} value={preset.value}>
114
+ {preset.label}
115
+ </MobilePresetButton>
116
+ ))}
117
+ </div>
118
+ )}
119
+
120
+ <AriaCalendarGrid weekdayStyle="short" className="w-max">
121
+ <AriaCalendarGridHeader>
122
+ {(day) => (
123
+ <AriaCalendarHeaderCell className="border-b-4 border-transparent p-0">
124
+ <div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
125
+ </AriaCalendarHeaderCell>
126
+ )}
127
+ </AriaCalendarGridHeader>
128
+ <AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
129
+ {(date) => <CalendarCell date={date} />}
130
+ </AriaCalendarGridBody>
131
+ </AriaCalendarGrid>
132
+ </div>
133
+
134
+ {isDesktop && (
135
+ <div className="flex flex-col gap-3 border-l border-secondary px-6 py-5">
136
+ <header className="relative flex items-center justify-end">
137
+ <h2 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm font-semibold text-fg-secondary">
138
+ <RangeCalendarTitle part="end" />
139
+ </h2>
140
+
141
+ <Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8" />
142
+ </header>
143
+
144
+ <AriaCalendarGrid weekdayStyle="short" offset={{ months: 1 }} className="w-max">
145
+ <AriaCalendarGridHeader>
146
+ {(day) => (
147
+ <AriaCalendarHeaderCell className="border-b-4 border-transparent p-0">
148
+ <div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
149
+ </AriaCalendarHeaderCell>
150
+ )}
151
+ </AriaCalendarGridHeader>
152
+ <AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
153
+ {(date) => <CalendarCell date={date} />}
154
+ </AriaCalendarGridBody>
155
+ </AriaCalendarGrid>
156
+ </div>
157
+ )}
158
+ </AriaRangeCalendar>
159
+ </ContextWrapper>
160
+ );
161
+ };
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { type HTMLAttributes } from "react";
4
+ import { type DateValue, RangeCalendarContext, useSlottedContext } from "react-aria-components";
5
+ import { cx } from '../../../utils/cx';
6
+
7
+ interface RangePresetButtonProps extends HTMLAttributes<HTMLButtonElement> {
8
+ value: { start: DateValue; end: DateValue };
9
+ }
10
+
11
+ export const RangePresetButton = ({ value, className, children, ...props }: RangePresetButtonProps) => {
12
+ const context = useSlottedContext(RangeCalendarContext);
13
+
14
+ const isSelected = context?.value?.start?.compare(value.start) === 0 && context?.value?.end?.compare(value.end) === 0;
15
+
16
+ return (
17
+ <button
18
+ {...props}
19
+ className={cx(
20
+ "cursor-pointer rounded-md px-3 py-2 text-left text-sm font-medium outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2",
21
+ isSelected ? "bg-active text-secondary_hover hover:bg-secondary_hover" : "text-secondary hover:bg-primary_hover hover:text-secondary_hover",
22
+ className,
23
+ )}
24
+ >
25
+ {children}
26
+ </button>
27
+ );
28
+ };
@@ -0,0 +1,154 @@
1
+ import type { FC, ReactNode, Ref } from "react";
2
+ import { isValidElement } from "react";
3
+ import { cx, sortCx } from '../../../utils/cx';
4
+ import { isReactComponent } from '../../../utils/is-react-component';
5
+
6
+ const iconsSizes = {
7
+ sm: "*:data-icon:size-4",
8
+ md: "*:data-icon:size-5",
9
+ lg: "*:data-icon:size-6",
10
+ xl: "*:data-icon:size-7",
11
+ };
12
+
13
+ const styles = sortCx({
14
+ light: {
15
+ base: "rounded-full",
16
+ sizes: {
17
+ sm: "size-8",
18
+ md: "size-10",
19
+ lg: "size-12",
20
+ xl: "size-14",
21
+ },
22
+ colors: {
23
+ brand: "bg-brand-secondary text-featured-icon-light-fg-brand",
24
+ gray: "bg-tertiary text-featured-icon-light-fg-gray",
25
+ error: "bg-error-secondary text-featured-icon-light-fg-error",
26
+ warning: "bg-warning-secondary text-featured-icon-light-fg-warning",
27
+ success: "bg-success-secondary text-featured-icon-light-fg-success",
28
+ },
29
+ },
30
+
31
+ gradient: {
32
+ base: "rounded-full text-fg-white before:absolute before:inset-0 before:size-full before:rounded-full before:border before:mask-b-from-0% after:absolute after:block after:rounded-full",
33
+ sizes: {
34
+ sm: "size-8 after:size-6 *:data-icon:size-4",
35
+ md: "size-10 after:size-7 *:data-icon:size-4",
36
+ lg: "size-12 after:size-8 *:data-icon:size-5",
37
+ xl: "size-14 after:size-10 *:data-icon:size-5",
38
+ },
39
+ colors: {
40
+ brand: "before:border-utility-brand-200 before:bg-utility-brand-50 after:bg-brand-solid",
41
+ gray: "before:border-utility-gray-200 before:bg-utility-gray-50 after:bg-secondary-solid",
42
+ error: "before:border-utility-error-200 before:bg-utility-error-50 after:bg-error-solid",
43
+ warning: "before:border-utility-warning-200 before:bg-utility-warning-50 after:bg-warning-solid",
44
+ success: "before:border-utility-success-200 before:bg-utility-success-50 after:bg-success-solid",
45
+ },
46
+ },
47
+
48
+ dark: {
49
+ base: "text-fg-white shadow-xs-skeumorphic before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
50
+ sizes: {
51
+ sm: "size-8 rounded-md before:rounded-[5px]",
52
+ md: "size-10 rounded-lg before:rounded-[7px]",
53
+ lg: "size-12 rounded-[10px] before:rounded-[9px]",
54
+ xl: "size-14 rounded-xl before:rounded-[11px]",
55
+ },
56
+ colors: {
57
+ brand: "bg-brand-solid before:border-utility-brand-200/12",
58
+ gray: "bg-secondary-solid before:border-utility-gray-200/12",
59
+ error: "bg-error-solid before:border-utility-error-200/12",
60
+ warning: "bg-warning-solid before:border-utility-warning-200/12",
61
+ success: "bg-success-solid before:border-utility-success-200/12",
62
+ },
63
+ },
64
+
65
+ modern: {
66
+ base: "bg-primary shadow-xs-skeumorphic ring-1 ring-inset",
67
+ sizes: {
68
+ sm: "size-8 rounded-md",
69
+ md: "size-10 rounded-lg",
70
+ lg: "size-12 rounded-[10px]",
71
+ xl: "size-14 rounded-xl",
72
+ },
73
+ colors: {
74
+ brand: "",
75
+ gray: "text-fg-secondary ring-primary",
76
+ error: "",
77
+ warning: "",
78
+ success: "",
79
+ },
80
+ },
81
+ "modern-neue": {
82
+ base: [
83
+ "bg-primary_alt ring-1 ring-inset before:absolute before:inset-1",
84
+ // Shadow
85
+ "before:shadow-[0px_1px_2px_0px_rgba(0,0,0,0.1),0px_3px_3px_0px_rgba(0,0,0,0.09),1px_8px_5px_0px_rgba(0,0,0,0.05),2px_21px_6px_0px_rgba(0,0,0,0),0px_0px_0px_1px_rgba(0,0,0,0.08),1px_13px_5px_0px_rgba(0,0,0,0.01),0px_-2px_2px_0px_rgba(0,0,0,0.13)_inset] before:ring-1 before:ring-secondary_alt",
86
+ ].join(" "),
87
+ sizes: {
88
+ sm: "size-8 rounded-[8px] before:rounded-[4px]",
89
+ md: "size-10 rounded-[10px] before:rounded-[6px]",
90
+ lg: "size-12 rounded-[12px] before:rounded-[8px]",
91
+ xl: "size-14 rounded-[14px] before:rounded-[10px]",
92
+ },
93
+ colors: {
94
+ brand: "",
95
+ gray: "text-fg-secondary ring-primary",
96
+ error: "",
97
+ warning: "",
98
+ success: "",
99
+ },
100
+ },
101
+
102
+ outline: {
103
+ base: "before:absolute before:rounded-full before:border-2 after:absolute after:rounded-full after:border-2",
104
+ sizes: {
105
+ sm: "size-4 before:size-6 after:size-8.5",
106
+ md: "size-5 before:size-7 after:size-9.5",
107
+ lg: "size-6 before:size-8 after:size-10.5",
108
+ xl: "size-7 before:size-9 after:size-11.5",
109
+ },
110
+ colors: {
111
+ brand: "text-fg-brand-primary before:border-fg-brand-primary/30 after:border-fg-brand-primary/10",
112
+ gray: "text-fg-tertiary before:border-fg-tertiary/30 after:border-fg-tertiary/10",
113
+ error: "text-fg-error-primary before:border-fg-error-primary/30 after:border-fg-error-primary/10",
114
+ warning: "text-fg-warning-primary before:border-fg-warning-primary/30 after:border-fg-warning-primary/10",
115
+ success: "text-fg-success-primary before:border-fg-success-primary/30 after:border-fg-success-primary/10",
116
+ },
117
+ },
118
+ });
119
+
120
+ interface FeaturedIconProps {
121
+ ref?: Ref<HTMLDivElement>;
122
+ children?: ReactNode;
123
+ className?: string;
124
+ icon?: FC<{ className?: string }> | ReactNode;
125
+ size?: "sm" | "md" | "lg" | "xl";
126
+ color: "brand" | "gray" | "success" | "warning" | "error";
127
+ theme?: "light" | "gradient" | "dark" | "outline" | "modern" | "modern-neue";
128
+ }
129
+
130
+ export const FeaturedIcon = (props: FeaturedIconProps) => {
131
+ const { size = "sm", theme: variant = "light", color = "brand", icon: Icon, ...otherProps } = props;
132
+
133
+ return (
134
+ <div
135
+ {...otherProps}
136
+ data-featured-icon
137
+ className={cx(
138
+ "relative flex shrink-0 items-center justify-center",
139
+
140
+ iconsSizes[size],
141
+ styles[variant].base,
142
+ styles[variant].sizes[size],
143
+ styles[variant].colors[color],
144
+
145
+ props.className,
146
+ )}
147
+ >
148
+ {isReactComponent(Icon) && <Icon data-icon className="z-1" />}
149
+ {isValidElement(Icon) && <div className="z-1">{Icon}</div>}
150
+
151
+ {props.children}
152
+ </div>
153
+ );
154
+ };
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ import type { ComponentPropsWithRef } from "react";
4
+ import { Form as AriaForm } from "react-aria-components";
5
+
6
+ export const Form = (props: ComponentPropsWithRef<typeof AriaForm>) => {
7
+ return <AriaForm {...props} />;
8
+ };
9
+
10
+ Form.displayName = "Form";