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,271 @@
1
+ "use client";
2
+
3
+ import { type ComponentType, type HTMLAttributes, type ReactNode, type Ref, createContext, useContext } from "react";
4
+ import { HelpCircle, InfoCircle } from "@untitledui/icons";
5
+ import type { InputProps as AriaInputProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
6
+ import { Group as AriaGroup, Input as AriaInput, TextField as AriaTextField } from "react-aria-components";
7
+ import { HintText } from './hint-text';
8
+ import { Label } from './label';
9
+ import { Tooltip, TooltipTrigger } from '../tooltip/tooltip';
10
+ import { cx, sortCx } from '../../../utils/cx';
11
+
12
+ export interface InputBaseProps extends TextFieldProps {
13
+ /** Tooltip message on hover. */
14
+ tooltip?: string;
15
+ /**
16
+ * Input size.
17
+ * @default "sm"
18
+ */
19
+ size?: "sm" | "md";
20
+ /** Placeholder text. */
21
+ placeholder?: string;
22
+ /** Class name for the icon. */
23
+ iconClassName?: string;
24
+ /** Class name for the input. */
25
+ inputClassName?: string;
26
+ /** Class name for the input wrapper. */
27
+ wrapperClassName?: string;
28
+ /** Class name for the tooltip. */
29
+ tooltipClassName?: string;
30
+ /** Keyboard shortcut to display. */
31
+ shortcut?: string | boolean;
32
+ ref?: Ref<HTMLInputElement>;
33
+ groupRef?: Ref<HTMLDivElement>;
34
+ /** Icon component to display on the left side of the input. */
35
+ icon?: ComponentType<HTMLAttributes<HTMLOrSVGElement>>;
36
+ }
37
+
38
+ export const InputBase = ({
39
+ ref,
40
+ tooltip,
41
+ shortcut,
42
+ groupRef,
43
+ size = "sm",
44
+ isInvalid,
45
+ isDisabled,
46
+ icon: Icon,
47
+ placeholder,
48
+ wrapperClassName,
49
+ tooltipClassName,
50
+ inputClassName,
51
+ iconClassName,
52
+ // Omit this prop to avoid invalid HTML attribute warning
53
+ isRequired: _isRequired,
54
+ ...inputProps
55
+ }: Omit<InputBaseProps, "label" | "hint">) => {
56
+ // Check if the input has a leading icon or tooltip
57
+ const hasTrailingIcon = tooltip || isInvalid;
58
+ const hasLeadingIcon = Icon;
59
+
60
+ // If the input is inside a `TextFieldContext`, use its context to simplify applying styles
61
+ const context = useContext(TextFieldContext);
62
+
63
+ const inputSize = context?.size || size;
64
+
65
+ const sizes = sortCx({
66
+ sm: {
67
+ root: cx("px-3 py-2", hasTrailingIcon && "pr-9", hasLeadingIcon && "pl-10"),
68
+ iconLeading: "left-3",
69
+ iconTrailing: "right-3",
70
+ shortcut: "pr-2.5",
71
+ },
72
+ md: {
73
+ root: cx("px-3.5 py-2.5", hasTrailingIcon && "pr-9.5", hasLeadingIcon && "pl-10.5"),
74
+ iconLeading: "left-3.5",
75
+ iconTrailing: "right-3.5",
76
+ shortcut: "pr-3",
77
+ },
78
+ });
79
+
80
+ return (
81
+ <AriaGroup
82
+ {...{ isDisabled, isInvalid }}
83
+ ref={groupRef}
84
+ className={({ isFocusWithin, isDisabled, isInvalid }) =>
85
+ cx(
86
+ "relative flex w-full flex-row place-content-center place-items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary transition-shadow duration-100 ease-linear ring-inset",
87
+
88
+ isFocusWithin && !isDisabled && "ring-2 ring-brand",
89
+
90
+ // Disabled state styles
91
+ isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
92
+ "group-disabled:cursor-not-allowed group-disabled:bg-disabled_subtle group-disabled:ring-disabled",
93
+
94
+ // Invalid state styles
95
+ isInvalid && "ring-error_subtle",
96
+ "group-invalid:ring-error_subtle",
97
+
98
+ // Invalid state with focus-within styles
99
+ isInvalid && isFocusWithin && "ring-2 ring-error",
100
+ isFocusWithin && "group-invalid:ring-2 group-invalid:ring-error",
101
+
102
+ context?.wrapperClassName,
103
+ wrapperClassName,
104
+ )
105
+ }
106
+ >
107
+ {/* Leading icon and Payment icon */}
108
+ {Icon && (
109
+ <Icon
110
+ className={cx(
111
+ "pointer-events-none absolute size-5 text-fg-quaternary",
112
+ isDisabled && "text-fg-disabled",
113
+ sizes[inputSize].iconLeading,
114
+ context?.iconClassName,
115
+ iconClassName,
116
+ )}
117
+ />
118
+ )}
119
+
120
+ {/* Input field */}
121
+ <AriaInput
122
+ {...(inputProps as AriaInputProps)}
123
+ ref={ref}
124
+ placeholder={placeholder}
125
+ className={cx(
126
+ "m-0 w-full bg-transparent text-md text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary",
127
+ isDisabled && "cursor-not-allowed text-disabled",
128
+ sizes[inputSize].root,
129
+ context?.inputClassName,
130
+ inputClassName,
131
+ )}
132
+ />
133
+
134
+ {/* Tooltip and help icon */}
135
+ {tooltip && !isInvalid && (
136
+ <Tooltip title={tooltip} placement="top">
137
+ <TooltipTrigger
138
+ className={cx(
139
+ "absolute cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover",
140
+ sizes[inputSize].iconTrailing,
141
+ context?.tooltipClassName,
142
+ tooltipClassName,
143
+ )}
144
+ >
145
+ <HelpCircle className="size-4" />
146
+ </TooltipTrigger>
147
+ </Tooltip>
148
+ )}
149
+
150
+ {/* Invalid icon */}
151
+ {isInvalid && (
152
+ <InfoCircle
153
+ className={cx(
154
+ "pointer-events-none absolute size-4 text-fg-error-secondary",
155
+ sizes[inputSize].iconTrailing,
156
+ context?.tooltipClassName,
157
+ tooltipClassName,
158
+ )}
159
+ />
160
+ )}
161
+
162
+ {/* Shortcut */}
163
+ {shortcut && (
164
+ <div
165
+ className={cx(
166
+ "pointer-events-none 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",
167
+ sizes[inputSize].shortcut,
168
+ )}
169
+ >
170
+ <span
171
+ className={cx(
172
+ "pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
173
+ isDisabled && "bg-transparent text-disabled",
174
+ )}
175
+ aria-hidden="true"
176
+ >
177
+ {typeof shortcut === "string" ? shortcut : "⌘K"}
178
+ </span>
179
+ </div>
180
+ )}
181
+ </AriaGroup>
182
+ );
183
+ };
184
+
185
+ InputBase.displayName = "InputBase";
186
+
187
+ interface BaseProps {
188
+ /** Label text for the input */
189
+ label?: string;
190
+ /** Helper text displayed below the input */
191
+ hint?: ReactNode;
192
+ }
193
+
194
+ interface TextFieldProps
195
+ extends BaseProps,
196
+ AriaTextFieldProps,
197
+ Pick<InputBaseProps, "size" | "wrapperClassName" | "inputClassName" | "iconClassName" | "tooltipClassName"> {
198
+ ref?: Ref<HTMLDivElement>;
199
+ }
200
+
201
+ const TextFieldContext = createContext<TextFieldProps>({});
202
+
203
+ export const TextField = ({ className, ...props }: TextFieldProps) => {
204
+ return (
205
+ <TextFieldContext.Provider value={props}>
206
+ <AriaTextField
207
+ {...props}
208
+ data-input-wrapper
209
+ className={(state) =>
210
+ cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
211
+ }
212
+ />
213
+ </TextFieldContext.Provider>
214
+ );
215
+ };
216
+
217
+ TextField.displayName = "TextField";
218
+
219
+ interface InputProps extends InputBaseProps, BaseProps {
220
+ /** Whether to hide required indicator from label */
221
+ hideRequiredIndicator?: boolean;
222
+ }
223
+
224
+ export const Input = ({
225
+ size = "sm",
226
+ placeholder,
227
+ icon: Icon,
228
+ label,
229
+ hint,
230
+ shortcut,
231
+ hideRequiredIndicator,
232
+ className,
233
+ ref,
234
+ groupRef,
235
+ tooltip,
236
+ iconClassName,
237
+ inputClassName,
238
+ wrapperClassName,
239
+ tooltipClassName,
240
+ ...props
241
+ }: InputProps) => {
242
+ return (
243
+ <TextField aria-label={!label ? placeholder : undefined} {...props} className={className}>
244
+ {({ isRequired, isInvalid }) => (
245
+ <>
246
+ {label && <Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired}>{label}</Label>}
247
+
248
+ <InputBase
249
+ {...{
250
+ ref,
251
+ groupRef,
252
+ size,
253
+ placeholder,
254
+ icon: Icon,
255
+ shortcut,
256
+ iconClassName,
257
+ inputClassName,
258
+ wrapperClassName,
259
+ tooltipClassName,
260
+ tooltip,
261
+ }}
262
+ />
263
+
264
+ {hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
265
+ </>
266
+ )}
267
+ </TextField>
268
+ );
269
+ };
270
+
271
+ Input.displayName = "Input";
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, Ref } from "react";
4
+ import { HelpCircle } from "@untitledui/icons";
5
+ import type { LabelProps as AriaLabelProps } from "react-aria-components";
6
+ import { Label as AriaLabel } from "react-aria-components";
7
+ import { Tooltip, TooltipTrigger } from '../tooltip/tooltip';
8
+ import { cx } from '../../../utils/cx';
9
+
10
+ interface LabelProps extends AriaLabelProps {
11
+ children: ReactNode;
12
+ isRequired?: boolean;
13
+ tooltip?: string;
14
+ tooltipDescription?: string;
15
+ ref?: Ref<HTMLLabelElement>;
16
+ }
17
+
18
+ export const Label = ({ isRequired, tooltip, tooltipDescription, className, ...props }: LabelProps) => {
19
+ return (
20
+ <AriaLabel
21
+ // Used for conditionally hiding/showing the label element via CSS:
22
+ // <Input label="Visible only on mobile" className="lg:**:data-label:hidden" />
23
+ // or
24
+ // <Input label="Visible only on mobile" className="lg:label:hidden" />
25
+ data-label="true"
26
+ {...props}
27
+ className={cx("flex cursor-default items-center gap-0.5 text-sm font-medium text-secondary", className)}
28
+ >
29
+ {props.children}
30
+
31
+ <span className={cx("hidden text-brand-tertiary", isRequired && "block", typeof isRequired === "undefined" && "group-required:block")}>*</span>
32
+
33
+ {tooltip && (
34
+ <Tooltip title={tooltip} description={tooltipDescription} placement="top">
35
+ <TooltipTrigger
36
+ // `TooltipTrigger` inherits the disabled state from the parent form field
37
+ // but we don't that. We want the tooltip be enabled even if the parent
38
+ // field is disabled.
39
+ isDisabled={false}
40
+ className="cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover"
41
+ >
42
+ <HelpCircle className="size-4" />
43
+ </TooltipTrigger>
44
+ </Tooltip>
45
+ )}
46
+ </AriaLabel>
47
+ );
48
+ };
49
+
50
+ Label.displayName = "Label";
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, Ref } from "react";
4
+ import { HelpCircle } from "@untitledui/icons";
5
+ import type { LabelProps as AriaLabelProps } from "react-aria-components";
6
+ import { Label as AriaLabel } from "react-aria-components";
7
+ import { Tooltip, TooltipTrigger } from '../tooltip/tooltip';
8
+ import { cx } from '../../../utils/cx';
9
+
10
+ interface LabelProps extends AriaLabelProps {
11
+ children: ReactNode;
12
+ isRequired?: boolean;
13
+ tooltip?: string;
14
+ tooltipDescription?: string;
15
+ ref?: Ref<HTMLLabelElement>;
16
+ }
17
+
18
+ export const Label = ({ isRequired, tooltip, tooltipDescription, className, ...props }: LabelProps) => {
19
+ return (
20
+ <AriaLabel
21
+ // Used for conditionally hiding/showing the label element via CSS:
22
+ // <Input label="Visible only on mobile" className="lg:**:data-label:hidden" />
23
+ // or
24
+ // <Input label="Visible only on mobile" className="lg:label:hidden" />
25
+ data-label="true"
26
+ {...props}
27
+ className={cx("flex cursor-default items-center gap-0.5 text-sm font-medium text-secondary", className)}
28
+ >
29
+ {props.children}
30
+
31
+ <span className={cx("hidden text-brand-tertiary", isRequired && "block", typeof isRequired === "undefined" && "group-required:block")}>*</span>
32
+
33
+ {tooltip && (
34
+ <Tooltip title={tooltip} description={tooltipDescription} placement="top">
35
+ <TooltipTrigger
36
+ // `TooltipTrigger` inherits the disabled state from the parent form field
37
+ // but we don't that. We want the tooltip be enabled even if the parent
38
+ // field is disabled.
39
+ isDisabled={false}
40
+ className="cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover"
41
+ >
42
+ <HelpCircle className="size-4" />
43
+ </TooltipTrigger>
44
+ </Tooltip>
45
+ )}
46
+ </AriaLabel>
47
+ );
48
+ };
49
+
50
+ Label.displayName = "Label";
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import { cx } from '../../../utils/cx';
4
+
5
+ const styles = {
6
+ sm: {
7
+ root: "gap-4",
8
+ label: "text-sm font-medium",
9
+ spinner: "size-8",
10
+ },
11
+ md: {
12
+ root: "gap-4",
13
+ label: "text-sm font-medium",
14
+ spinner: "size-12",
15
+ },
16
+ lg: {
17
+ root: "gap-4",
18
+ label: "text-lg font-medium",
19
+ spinner: "size-14",
20
+ },
21
+ xl: {
22
+ root: "gap-5",
23
+ label: "text-lg font-medium",
24
+ spinner: "size-16",
25
+ },
26
+ };
27
+
28
+ interface LoadingIndicatorProps {
29
+ /**
30
+ * The visual style of the loading indicator.
31
+ * @default 'line-simple'
32
+ */
33
+ type?: "line-simple" | "line-spinner" | "dot-circle";
34
+ /**
35
+ * The size of the loading indicator.
36
+ * @default 'sm'
37
+ */
38
+ size?: "sm" | "md" | "lg" | "xl";
39
+ /**
40
+ * Optional text label displayed below the indicator.
41
+ */
42
+ label?: string;
43
+ }
44
+
45
+ export const LoadingIndicator = ({ type = "line-simple", size = "sm", label }: LoadingIndicatorProps) => {
46
+ const renderSpinner = () => {
47
+ if (type === "line-spinner") {
48
+ return (
49
+ <svg className={cx("animate-spin", styles[size].spinner)} viewBox="0 0 32 32" fill="none">
50
+ <circle
51
+ className="stroke-fg-brand-primary"
52
+ cx="16"
53
+ cy="16"
54
+ r="14"
55
+ fill="none"
56
+ strokeWidth="4"
57
+ strokeDashoffset="40"
58
+ strokeDasharray="100"
59
+ strokeLinecap="round"
60
+ />
61
+ </svg>
62
+ );
63
+ }
64
+
65
+ if (type === "dot-circle") {
66
+ return (
67
+ <svg className={cx("animate-spin text-fg-brand-primary", styles[size].spinner)} viewBox="0 0 36 36" fill="none">
68
+ <path
69
+ d="M34 18C34 15.8989 33.5861 13.8183 32.7821 11.8771C31.978 9.93586 30.7994 8.17203 29.3137 6.68629C27.828 5.20055 26.0641 4.022 24.1229 3.21793C22.1817 2.41385 20.1011 2 18 2C15.8988 2 13.8183 2.41385 11.8771 3.21793C9.93585 4.022 8.17203 5.20055 6.68629 6.68629C5.20055 8.17203 4.022 9.93586 3.21793 11.8771C2.41385 13.8183 2 15.8989 2 18"
70
+ stroke="url(#paint0)"
71
+ strokeWidth="4"
72
+ strokeLinecap="round"
73
+ strokeLinejoin="round"
74
+ strokeDasharray="0.1 8"
75
+ />
76
+ <path
77
+ d="M3.21793 24.1229C4.022 26.0641 5.20055 27.828 6.68629 29.3137C8.17203 30.7994 9.93585 31.978 11.8771 32.7821C13.8183 33.5861 15.8988 34 18 34C20.1011 34 22.1817 33.5861 24.1229 32.7821C26.0641 31.978 27.828 30.7994 29.3137 29.3137C30.7994 27.828 31.978 26.0641 32.7821 24.1229"
78
+ stroke="url(#paint1)"
79
+ strokeWidth="4"
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ strokeDasharray="0.1 8"
83
+ />
84
+ <defs>
85
+ <linearGradient id="paint0" x1="34" y1="18" x2="2" y2="18" gradientUnits="userSpaceOnUse">
86
+ <stop stopColor="currentColor" />
87
+ <stop offset="1" stopColor="currentColor" stopOpacity="0.5" />
88
+ </linearGradient>
89
+ <linearGradient id="paint1" x1="33" y1="23.5" x2="3" y2="24" gradientUnits="userSpaceOnUse">
90
+ <stop stopOpacity="0" stopColor="currentColor" />
91
+ <stop offset="1" stopColor="currentColor" stopOpacity="0.48" />
92
+ </linearGradient>
93
+ </defs>
94
+ </svg>
95
+ );
96
+ }
97
+
98
+ // Default case: type === "line-simple"
99
+ return (
100
+ <svg className={cx("animate-spin", styles[size].spinner)} viewBox="0 0 32 32" fill="none">
101
+ <circle className="text-bg-tertiary" cx="16" cy="16" r="14" stroke="currentColor" strokeWidth="4" />
102
+ <circle
103
+ className="stroke-fg-brand-primary"
104
+ cx="16"
105
+ cy="16"
106
+ r="14"
107
+ fill="none"
108
+ strokeWidth="4"
109
+ strokeDashoffset="75"
110
+ strokeDasharray="100"
111
+ strokeLinecap="round"
112
+ />
113
+ </svg>
114
+ );
115
+ };
116
+
117
+ return (
118
+ <div className={cx("flex flex-col items-center justify-center", styles[size].root)}>
119
+ {renderSpinner()}
120
+ {label && <span className={cx("text-secondary", styles[size].label)}>{label}</span>}
121
+ </div>
122
+ );
123
+ };