untitledui 0.1.1
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/dist/commands/add.js +339 -0
- package/dist/commands/init.js +436 -0
- package/dist/helper/download-tar-api.js +129 -0
- package/dist/helper/download-tar.js +81 -0
- package/dist/helper/find-css-file.js +19 -0
- package/dist/helper/formatText.js +37 -0
- package/dist/helper/get-components-api.js +47 -0
- package/dist/helper/get-components-list.js +62 -0
- package/dist/helper/get-components.js +19 -0
- package/dist/helper/get-config.js +163 -0
- package/dist/helper/get-package-info.js +99 -0
- package/dist/helper/get-pkg-manager.js +16 -0
- package/dist/helper/get-project.js +176 -0
- package/dist/helper/install-template.js +29 -0
- package/dist/helper/match-color-css.js +82 -0
- package/dist/helper/update-color-css.js +134 -0
- package/dist/index.js +25 -0
- package/dist/package.json +50 -0
- package/dist/res/components.json +520 -0
- package/dist/res/config.json +3 -0
- package/package.json +61 -0
- package/templates/default/.prettierrc +10 -0
- package/templates/default/README.md +36 -0
- package/templates/default/eslint.config.mjs +58 -0
- package/templates/default/next.config.ts +6 -0
- package/templates/default/package.json +57 -0
- package/templates/default/postcss.config.js +5 -0
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/public/marketing/smiling-girl.png +0 -0
- package/templates/default/public/marketing/spirals.webp +0 -0
- package/templates/default/src/app/home-screen.tsx +109 -0
- package/templates/default/src/app/layout.tsx +42 -0
- package/templates/default/src/app/not-found.tsx +40 -0
- package/templates/default/src/app/page.tsx +3 -0
- package/templates/default/src/components/foundations/dot-icon.tsx +27 -0
- package/templates/default/src/components/foundations/featured-icon/featured-icons.tsx +153 -0
- package/templates/default/src/components/foundations/logo/UntitledLogo.tsx +63 -0
- package/templates/default/src/components/foundations/logo/UntitledLogoMinimal.tsx +164 -0
- package/templates/default/src/components/foundations/payment-icons/amex-icon.tsx +19 -0
- package/templates/default/src/components/foundations/payment-icons/apple-pay-icon.tsx +27 -0
- package/templates/default/src/components/foundations/payment-icons/discover-icon.tsx +34 -0
- package/templates/default/src/components/foundations/payment-icons/index.tsx +10 -0
- package/templates/default/src/components/foundations/payment-icons/mastercard-icon.tsx +39 -0
- package/templates/default/src/components/foundations/payment-icons/paypal-icon.tsx +45 -0
- package/templates/default/src/components/foundations/payment-icons/stripe-icon.tsx +27 -0
- package/templates/default/src/components/foundations/payment-icons/union-pay-icon.tsx +37 -0
- package/templates/default/src/components/foundations/payment-icons/visa-icon.tsx +27 -0
- package/templates/default/src/components/marketing/header-navigation/base-components/nav-menu-item.tsx +41 -0
- package/templates/default/src/components/marketing/header-navigation/components/header.tsx +245 -0
- package/templates/default/src/components/marketing/header-navigation/dropdown-header-navigation.tsx +53 -0
- package/templates/default/src/components/shared/avatar/avatar-label-group.tsx +32 -0
- package/templates/default/src/components/shared/avatar/avatar-profile-photo.tsx +84 -0
- package/templates/default/src/components/shared/avatar/avatar.tsx +131 -0
- package/templates/default/src/components/shared/avatar/base-components/avatar-add-button.tsx +33 -0
- package/templates/default/src/components/shared/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/templates/default/src/components/shared/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/templates/default/src/components/shared/avatar/base-components/index.ts +4 -0
- package/templates/default/src/components/shared/avatar/base-components/verified-tick.tsx +34 -0
- package/templates/default/src/components/shared/avatar/utils.ts +12 -0
- package/templates/default/src/components/shared/badges/badge-groups.tsx +176 -0
- package/templates/default/src/components/shared/badges/badge-types.ts +264 -0
- package/templates/default/src/components/shared/badges/badges.tsx +479 -0
- package/templates/default/src/components/shared/button-group/button-group.tsx +97 -0
- package/templates/default/src/components/shared/buttons/app-store-buttons-outline.tsx +454 -0
- package/templates/default/src/components/shared/buttons/app-store-buttons.tsx +806 -0
- package/templates/default/src/components/shared/buttons/button-utility.tsx +87 -0
- package/templates/default/src/components/shared/buttons/button.tsx +284 -0
- package/templates/default/src/components/shared/buttons/close-button.tsx +39 -0
- package/templates/default/src/components/shared/buttons/social-button.tsx +135 -0
- package/templates/default/src/components/shared/buttons/social-logos.tsx +115 -0
- package/templates/default/src/components/shared/checkbox/checkbox.tsx +120 -0
- package/templates/default/src/components/shared/dropdown/dropdown.tsx +138 -0
- package/templates/default/src/components/shared/input-dropdown/combobox.tsx +161 -0
- package/templates/default/src/components/shared/input-dropdown/dropdown-item.tsx +98 -0
- package/templates/default/src/components/shared/input-dropdown/input-dropdown.tsx +172 -0
- package/templates/default/src/components/shared/input-dropdown/multi-select.tsx +373 -0
- package/templates/default/src/components/shared/input-dropdown/popover.tsx +36 -0
- package/templates/default/src/components/shared/input-dropdown/select.tsx +63 -0
- package/templates/default/src/components/shared/inputs/file-upload-trigger.tsx +74 -0
- package/templates/default/src/components/shared/inputs/form/form.tsx +10 -0
- package/templates/default/src/components/shared/inputs/hint-text.tsx +34 -0
- package/templates/default/src/components/shared/inputs/input/index.tsx +189 -0
- package/templates/default/src/components/shared/inputs/input/input-payment.tsx +134 -0
- package/templates/default/src/components/shared/inputs/input/input-with-button.tsx +69 -0
- package/templates/default/src/components/shared/inputs/input/input-with-dropdown.tsx +178 -0
- package/templates/default/src/components/shared/inputs/input/input-with-prefix.tsx +74 -0
- package/templates/default/src/components/shared/inputs/label.tsx +46 -0
- package/templates/default/src/components/shared/inputs/textarea/textarea.tsx +82 -0
- package/templates/default/src/components/shared/progress-indicators/progress-circles.tsx +176 -0
- package/templates/default/src/components/shared/progress-indicators/progress-indicators.tsx +86 -0
- package/templates/default/src/components/shared/progress-indicators/simple-circle.tsx +29 -0
- package/templates/default/src/components/shared/radio-buttons/radio-buttons.tsx +125 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-avatar.tsx +62 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-checkbox.tsx +72 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-icon-card.tsx +95 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-icon-simple.tsx +70 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-payment-icon.tsx +71 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-radio-button.tsx +76 -0
- package/templates/default/src/components/shared/radio-groups/radio-groups.tsx +8 -0
- package/templates/default/src/components/shared/slider/slider.tsx +76 -0
- package/templates/default/src/components/shared/tags/base-components/tag-checkbox.tsx +47 -0
- package/templates/default/src/components/shared/tags/base-components/tag-close-x.tsx +34 -0
- package/templates/default/src/components/shared/tags/tags.tsx +162 -0
- package/templates/default/src/components/shared/toggle/toggle.tsx +140 -0
- package/templates/default/src/components/shared/tooltips/tooltips.tsx +140 -0
- package/templates/default/src/components/utils/index.ts +48 -0
- package/templates/default/src/components/utils/isDeepEqual.ts +31 -0
- package/templates/default/src/components/utils/isReactComponent.ts +22 -0
- package/templates/default/src/components/utils/mergeRefs.ts +19 -0
- package/templates/default/src/components/utils/useBreakpoint.ts +36 -0
- package/templates/default/src/components/utils/uuid.ts +9 -0
- package/templates/default/src/fonts/GeistMonoVF.woff +0 -0
- package/templates/default/src/fonts/GeistVF.woff +0 -0
- package/templates/default/src/hooks/use-resize-observer.tsx +55 -0
- package/templates/default/src/providers/theme.tsx +11 -0
- package/templates/default/src/styles/colors.css +805 -0
- package/templates/default/src/styles/globals.css +86 -0
- package/templates/default/src/styles/text-styles.css +177 -0
- package/templates/default/src/styles/theme.css +1310 -0
- package/templates/default/src/styles/typography.css +428 -0
- package/templates/default/tsconfig.json +27 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { DetailedReactHTMLElement, ReactNode } from "react";
|
|
4
|
+
import React, { cloneElement, useRef } from "react";
|
|
5
|
+
import { filterDOMProps } from "@react-aria/utils";
|
|
6
|
+
|
|
7
|
+
interface FileTriggerProps {
|
|
8
|
+
/**
|
|
9
|
+
* Specifies what mime type of files are allowed.
|
|
10
|
+
*/
|
|
11
|
+
acceptedFileTypes?: Array<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Whether multiple files can be selected.
|
|
14
|
+
*/
|
|
15
|
+
allowsMultiple?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Specifies the use of a media capture mechanism to capture the media on the spot.
|
|
18
|
+
*/
|
|
19
|
+
defaultCamera?: "user" | "environment";
|
|
20
|
+
/**
|
|
21
|
+
* Handler when a user selects a file.
|
|
22
|
+
*/
|
|
23
|
+
onSelect?: (files: FileList | null) => void;
|
|
24
|
+
/**
|
|
25
|
+
* The children of the component.
|
|
26
|
+
*/
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
/**
|
|
29
|
+
* Enables the selection of directories instead of individual files.
|
|
30
|
+
*/
|
|
31
|
+
acceptDirectory?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// TODO: Make it possible to pass down a ref to the input element.
|
|
35
|
+
/**
|
|
36
|
+
* A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress.
|
|
37
|
+
*/
|
|
38
|
+
export const FileTrigger = (props: FileTriggerProps) => {
|
|
39
|
+
const { children, onSelect, acceptedFileTypes, allowsMultiple, defaultCamera, acceptDirectory, ...rest } = props;
|
|
40
|
+
|
|
41
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
42
|
+
const domProps = filterDOMProps(rest);
|
|
43
|
+
|
|
44
|
+
// Make sure that only one child is passed to the component.
|
|
45
|
+
const clonableElement = React.Children.only(children);
|
|
46
|
+
|
|
47
|
+
// Clone the child element and add an `onClick` handler to open the file dialog.
|
|
48
|
+
const mainElement = cloneElement(clonableElement as DetailedReactHTMLElement<any, any>, {
|
|
49
|
+
onClick: () => {
|
|
50
|
+
if (inputRef.current?.value) {
|
|
51
|
+
inputRef.current.value = "";
|
|
52
|
+
}
|
|
53
|
+
inputRef.current?.click();
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
{mainElement}
|
|
60
|
+
<input
|
|
61
|
+
{...domProps}
|
|
62
|
+
type="file"
|
|
63
|
+
ref={inputRef}
|
|
64
|
+
style={{ display: "none" }}
|
|
65
|
+
accept={acceptedFileTypes?.toString()}
|
|
66
|
+
onChange={(e) => onSelect?.(e.target.files)}
|
|
67
|
+
capture={defaultCamera}
|
|
68
|
+
multiple={allowsMultiple}
|
|
69
|
+
// @ts-expect-error
|
|
70
|
+
webkitdirectory={acceptDirectory ? "" : undefined}
|
|
71
|
+
/>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -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";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode, Ref } from "react";
|
|
4
|
+
import type { TextProps as AriaTextProps } from "react-aria-components";
|
|
5
|
+
import { Text as AriaText } from "react-aria-components";
|
|
6
|
+
import { cx } from "@/components/utils";
|
|
7
|
+
|
|
8
|
+
interface HintTextProps extends AriaTextProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
isInvalid?: boolean;
|
|
11
|
+
ref?: Ref<HTMLElement>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const HintText = ({ isInvalid, className, ...props }: HintTextProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<AriaText
|
|
17
|
+
{...props}
|
|
18
|
+
slot={isInvalid ? "errorMessage" : "description"}
|
|
19
|
+
className={cx(
|
|
20
|
+
"tt-sm text-tertiary",
|
|
21
|
+
|
|
22
|
+
// Invalid state
|
|
23
|
+
isInvalid && "text-error-primary",
|
|
24
|
+
"group-invalid:text-error-primary",
|
|
25
|
+
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
HintText.displayName = "HintText";
|
|
33
|
+
|
|
34
|
+
export default HintText;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentType, HTMLAttributes, ReactNode, Ref } from "react";
|
|
4
|
+
import { HelpCircle, InfoCircle } from "@untitledui/icons";
|
|
5
|
+
import type { TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
|
6
|
+
import { Input as AriaInput, TextField as AriaTextField, Group } from "react-aria-components";
|
|
7
|
+
import HintText from "@/components/shared/inputs/hint-text";
|
|
8
|
+
import Label from "@/components/shared/inputs/label";
|
|
9
|
+
import { Tooltip, TooltipTrigger } from "@/components/shared/tooltips/tooltips";
|
|
10
|
+
import { cx, sortCx } from "@/components/utils";
|
|
11
|
+
|
|
12
|
+
export interface InputBaseProps extends TextFieldProps {
|
|
13
|
+
label?: string;
|
|
14
|
+
hint?: ReactNode;
|
|
15
|
+
tooltip?: string;
|
|
16
|
+
size?: "sm" | "md";
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
iconClassName?: string;
|
|
19
|
+
inputClassName?: string;
|
|
20
|
+
wrapperClassName?: string;
|
|
21
|
+
tooltipClassName?: string;
|
|
22
|
+
shortcut?: string | boolean;
|
|
23
|
+
ref?: Ref<HTMLInputElement>;
|
|
24
|
+
groupRef?: Ref<HTMLDivElement>;
|
|
25
|
+
icon?: ComponentType<HTMLAttributes<HTMLOrSVGElement>>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const InputBase = ({ size = "sm", placeholder, icon: Icon, isDisabled, isInvalid, tooltip, shortcut, ref, groupRef, ...props }: InputBaseProps) => {
|
|
29
|
+
// Check if the input has a leading icon or tooltip
|
|
30
|
+
const hasTrailingIcon = tooltip || isInvalid;
|
|
31
|
+
const hasLeadingIcon = Icon;
|
|
32
|
+
|
|
33
|
+
const sizes = sortCx({
|
|
34
|
+
sm: {
|
|
35
|
+
root: cx("px-3 py-2", hasTrailingIcon && "pr-9", hasLeadingIcon && "pl-10"),
|
|
36
|
+
iconLeading: "left-3",
|
|
37
|
+
iconTrailing: "right-3",
|
|
38
|
+
shortcut: "pr-2.5",
|
|
39
|
+
},
|
|
40
|
+
md: {
|
|
41
|
+
root: cx("px-3.5 py-2.5", hasTrailingIcon && "pr-9.5", hasLeadingIcon && "pl-10.5"),
|
|
42
|
+
iconLeading: "left-3.5",
|
|
43
|
+
iconTrailing: "right-3.5",
|
|
44
|
+
shortcut: "pr-3",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Group
|
|
50
|
+
{...{ isDisabled, isInvalid }}
|
|
51
|
+
ref={groupRef}
|
|
52
|
+
className={({ isFocusWithin, isDisabled, isInvalid }) =>
|
|
53
|
+
cx(
|
|
54
|
+
"relative flex w-full flex-row place-content-center place-items-center rounded-lg bg-primary shadow-xs ring-1 ring-border-primary transition-shadow duration-100 ease-linear ring-inset",
|
|
55
|
+
|
|
56
|
+
isFocusWithin && !isDisabled && "ring-2 ring-border-brand",
|
|
57
|
+
|
|
58
|
+
// Disabled state styles
|
|
59
|
+
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-border-disabled",
|
|
60
|
+
"group-disabled:cursor-not-allowed group-disabled:bg-disabled_subtle group-disabled:ring-border-disabled",
|
|
61
|
+
|
|
62
|
+
// Invalid state styles
|
|
63
|
+
isInvalid && "ring-border-error_subtle",
|
|
64
|
+
"group-invalid:ring-border-error_subtle",
|
|
65
|
+
|
|
66
|
+
// Invalid state with focus-within styles
|
|
67
|
+
isInvalid && isFocusWithin && "ring-2 ring-border-error",
|
|
68
|
+
isFocusWithin && "group-invalid:ring-2 group-invalid:ring-border-error",
|
|
69
|
+
|
|
70
|
+
props.wrapperClassName,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{/* Leading icon and Payment icon */}
|
|
75
|
+
{Icon && (
|
|
76
|
+
<Icon
|
|
77
|
+
className={cx(
|
|
78
|
+
"pointer-events-none absolute size-5 text-fg-quaternary",
|
|
79
|
+
isDisabled && "text-fg-disabled",
|
|
80
|
+
sizes[size].iconLeading,
|
|
81
|
+
props.iconClassName,
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Input field */}
|
|
87
|
+
<AriaInput
|
|
88
|
+
ref={ref}
|
|
89
|
+
placeholder={placeholder}
|
|
90
|
+
className={cx(
|
|
91
|
+
"m-0 w-full bg-transparent tt-md text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary",
|
|
92
|
+
isDisabled && "cursor-not-allowed text-disabled",
|
|
93
|
+
sizes[size].root,
|
|
94
|
+
props.inputClassName,
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
{/* Tooltip and help icon */}
|
|
99
|
+
{tooltip && !isInvalid && (
|
|
100
|
+
<Tooltip title={tooltip} placement="top">
|
|
101
|
+
<TooltipTrigger
|
|
102
|
+
className={cx(
|
|
103
|
+
"absolute cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover",
|
|
104
|
+
sizes[size].iconTrailing,
|
|
105
|
+
props.tooltipClassName,
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
<HelpCircle className="size-4" />
|
|
109
|
+
</TooltipTrigger>
|
|
110
|
+
</Tooltip>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* Invalid icon */}
|
|
114
|
+
{isInvalid && (
|
|
115
|
+
<InfoCircle className={cx("pointer-events-none absolute size-4 text-fg-error-secondary", sizes[size].iconTrailing, props.tooltipClassName)} />
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Shortcut */}
|
|
119
|
+
{shortcut && (
|
|
120
|
+
<div
|
|
121
|
+
className={cx(
|
|
122
|
+
"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",
|
|
123
|
+
sizes[size].shortcut,
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<span
|
|
127
|
+
className={cx(
|
|
128
|
+
"pointer-events-none rounded px-1 py-px tt-xs-md text-quaternary ring-1 ring-border-secondary select-none ring-inset",
|
|
129
|
+
isDisabled && "bg-transparent text-disabled",
|
|
130
|
+
)}
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
>
|
|
133
|
+
{shortcut || "⌘K"}
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</Group>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
InputBase.displayName = "InputBase";
|
|
142
|
+
|
|
143
|
+
interface TextFieldProps extends AriaTextFieldProps {
|
|
144
|
+
ref?: Ref<HTMLDivElement>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const TextField = ({ className, ...props }: TextFieldProps) => {
|
|
148
|
+
return (
|
|
149
|
+
<AriaTextField
|
|
150
|
+
{...props}
|
|
151
|
+
className={(state) =>
|
|
152
|
+
cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
|
153
|
+
}
|
|
154
|
+
>
|
|
155
|
+
{props.children}
|
|
156
|
+
</AriaTextField>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
TextField.displayName = "TextField";
|
|
161
|
+
|
|
162
|
+
interface InputProps extends InputBaseProps {
|
|
163
|
+
hideRequiredIndicator?: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const Input = ({
|
|
167
|
+
size = "sm",
|
|
168
|
+
placeholder = "olivia@untitledui.com",
|
|
169
|
+
icon: Icon,
|
|
170
|
+
label,
|
|
171
|
+
hint,
|
|
172
|
+
hideRequiredIndicator,
|
|
173
|
+
className,
|
|
174
|
+
ref,
|
|
175
|
+
groupRef,
|
|
176
|
+
...props
|
|
177
|
+
}: InputProps) => {
|
|
178
|
+
return (
|
|
179
|
+
<TextField aria-label={!label ? placeholder : undefined} {...props} className={className}>
|
|
180
|
+
{label && <Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : undefined}>{label}</Label>}
|
|
181
|
+
|
|
182
|
+
<InputBase {...props} {...{ ref, groupRef, size, placeholder, icon: Icon }} />
|
|
183
|
+
|
|
184
|
+
{hint && <HintText>{hint}</HintText>}
|
|
185
|
+
</TextField>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { TextField } from "react-aria-components";
|
|
5
|
+
import { AmexIcon, DiscoverIcon, MastercardIcon, UnionPayIcon, VisaIcon } from "@/components/foundations/payment-icons";
|
|
6
|
+
import HintText from "@/components/shared/inputs/hint-text";
|
|
7
|
+
import type { InputBaseProps } from "@/components/shared/inputs/input";
|
|
8
|
+
import { InputBase } from "@/components/shared/inputs/input";
|
|
9
|
+
import Label from "@/components/shared/inputs/label";
|
|
10
|
+
import { cx } from "@/components/utils";
|
|
11
|
+
|
|
12
|
+
const cardTypes = [
|
|
13
|
+
{
|
|
14
|
+
name: "Visa",
|
|
15
|
+
pattern: /^4[0-9]{3,}$/, // Visa card numbers start with 4 and are 13 or 16 digits long
|
|
16
|
+
card: "visa",
|
|
17
|
+
icon: VisaIcon,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "MasterCard",
|
|
21
|
+
pattern: /^5[1-5][0-9]{2,}$/, // MasterCard numbers start with 51-55 and are 16 digits long
|
|
22
|
+
card: "mastercard",
|
|
23
|
+
icon: MastercardIcon,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "American Express",
|
|
27
|
+
pattern: /^3[47][0-9]{2,}$/, // American Express numbers start with 34 or 37 and are 15 digits long
|
|
28
|
+
card: "amex",
|
|
29
|
+
icon: AmexIcon,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "Discover",
|
|
33
|
+
pattern: /^6(?:011|5[0-9]{2}|4[4-9][0-9])[0-9]{12}$/, // Discover card numbers start with 6011 or 65 and are 16 digits long
|
|
34
|
+
card: "discover",
|
|
35
|
+
icon: DiscoverIcon,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "UnionPay",
|
|
39
|
+
pattern: /^(62|88)[0-9]{14,17}$/, // UnionPay card numbers start with 62 or 88 and are between 15-19 digits long
|
|
40
|
+
card: "unionpay",
|
|
41
|
+
icon: UnionPayIcon,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "Unknown",
|
|
45
|
+
pattern: /.*/, // Fallback pattern for unknown cards
|
|
46
|
+
card: "unknown",
|
|
47
|
+
icon: MastercardIcon,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect the card type based on the card number.
|
|
53
|
+
* @param number The card number to detect the type for.
|
|
54
|
+
* @returns The matching card type object.
|
|
55
|
+
*/
|
|
56
|
+
const detectCardType = (number: string) => {
|
|
57
|
+
// Remove all spaces
|
|
58
|
+
const sanitizedNumber = number.replace(/\D/g, "");
|
|
59
|
+
|
|
60
|
+
// Find the matching card type
|
|
61
|
+
const card = cardTypes.find((cardType) => cardType.pattern.test(sanitizedNumber));
|
|
62
|
+
|
|
63
|
+
return card || cardTypes[cardTypes.length - 1];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format the card number in groups of 4 digits (i.e. 1234 5678 9012 3456).
|
|
68
|
+
*/
|
|
69
|
+
export const formatCardNumber = (number: string) => {
|
|
70
|
+
// Remove non-numeric characters
|
|
71
|
+
const cleaned = number.replace(/\D/g, "");
|
|
72
|
+
|
|
73
|
+
// Format the card number in groups of 4 digits
|
|
74
|
+
const match = cleaned.match(/\d{1,4}/g);
|
|
75
|
+
|
|
76
|
+
if (match) {
|
|
77
|
+
return match.join(" ");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return cleaned;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
interface PaymentInputProps extends Omit<InputBaseProps, "icon"> {}
|
|
84
|
+
|
|
85
|
+
export const PaymentInput = ({ onChange, className, maxLength = 19, label, hint, ...props }: PaymentInputProps) => {
|
|
86
|
+
const [cardNumber, setCardNumber] = useState(formatCardNumber(props.value || props.defaultValue || ""));
|
|
87
|
+
|
|
88
|
+
const handleCardNumberChange = (value: string) => {
|
|
89
|
+
// Remove all non-numeric characters
|
|
90
|
+
value = value.replace(/\D/g, "");
|
|
91
|
+
|
|
92
|
+
// Return if the value is empty
|
|
93
|
+
if (!value) {
|
|
94
|
+
setCardNumber("");
|
|
95
|
+
onChange?.("");
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Format the card number in groups of 4 digits
|
|
100
|
+
const formatted = formatCardNumber(value);
|
|
101
|
+
|
|
102
|
+
setCardNumber(formatted);
|
|
103
|
+
onChange?.(value);
|
|
104
|
+
return value;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const card = detectCardType(cardNumber);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<TextField
|
|
111
|
+
aria-label={!label ? props?.placeholder : undefined}
|
|
112
|
+
{...props}
|
|
113
|
+
value={cardNumber}
|
|
114
|
+
inputMode="numeric"
|
|
115
|
+
maxLength={maxLength}
|
|
116
|
+
onChange={handleCardNumberChange}
|
|
117
|
+
className={(state) =>
|
|
118
|
+
cx("flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
{({ isDisabled, isInvalid, isRequired }) => (
|
|
122
|
+
<>
|
|
123
|
+
{label && <Label {...{ isRequired }}>{label}</Label>}
|
|
124
|
+
|
|
125
|
+
<InputBase {...props} {...{ isDisabled, isInvalid }} icon={card.icon} inputClassName="pl-13" iconClassName="left-2.5 size-max" />
|
|
126
|
+
|
|
127
|
+
{hint && <HintText {...{ isInvalid }}>{hint}</HintText>}
|
|
128
|
+
</>
|
|
129
|
+
)}
|
|
130
|
+
</TextField>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
PaymentInput.displayName = "PaymentInput";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Copy01 } from "@untitledui/icons";
|
|
6
|
+
import { TextField } from "react-aria-components";
|
|
7
|
+
import type { CommonProps } from "@/components/shared/buttons/button";
|
|
8
|
+
import Button from "@/components/shared/buttons/button";
|
|
9
|
+
import HintText from "@/components/shared/inputs/hint-text";
|
|
10
|
+
import type { InputBaseProps } from "@/components/shared/inputs/input";
|
|
11
|
+
import { InputBase } from "@/components/shared/inputs/input";
|
|
12
|
+
import Label from "@/components/shared/inputs/label";
|
|
13
|
+
import { cx } from "@/components/utils";
|
|
14
|
+
|
|
15
|
+
interface InputWithButtonProps extends Omit<InputBaseProps, "icon"> {
|
|
16
|
+
buttonText: string;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
buttonColor?: CommonProps["color"];
|
|
19
|
+
iconLeading?: CommonProps["iconLeading"];
|
|
20
|
+
iconTrailing?: CommonProps["iconTrailing"];
|
|
21
|
+
hint?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const InputWithButton = ({
|
|
25
|
+
size = "sm",
|
|
26
|
+
buttonColor = "secondary",
|
|
27
|
+
iconLeading = Copy01,
|
|
28
|
+
onClick,
|
|
29
|
+
className,
|
|
30
|
+
buttonText,
|
|
31
|
+
label,
|
|
32
|
+
hint,
|
|
33
|
+
...props
|
|
34
|
+
}: InputWithButtonProps) => {
|
|
35
|
+
return (
|
|
36
|
+
<TextField
|
|
37
|
+
aria-label={!label ? props?.placeholder : undefined}
|
|
38
|
+
{...props}
|
|
39
|
+
className={(state) =>
|
|
40
|
+
cx("flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
{({ isDisabled, isInvalid, isRequired }) => (
|
|
44
|
+
<>
|
|
45
|
+
{label && <Label {...{ isRequired }}>{label}</Label>}
|
|
46
|
+
|
|
47
|
+
<div className="flex h-max w-full flex-row justify-center">
|
|
48
|
+
<InputBase {...props} {...{ isDisabled, isInvalid }} wrapperClassName="rounded-r-none z-10" />
|
|
49
|
+
|
|
50
|
+
{/* TODO: Take this button out of here and move it to be a prop so the user can fully control the button. */}
|
|
51
|
+
<Button
|
|
52
|
+
onClick={onClick}
|
|
53
|
+
color={buttonColor}
|
|
54
|
+
iconLeading={iconLeading}
|
|
55
|
+
size={size === "sm" ? "md" : "lg"}
|
|
56
|
+
className="-ml-px rounded-l-none shadow-xs! ring-1 ring-border-primary ring-inset focus:z-10"
|
|
57
|
+
>
|
|
58
|
+
{buttonText}
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{hint && <HintText {...{ isInvalid }}>{hint}</HintText>}
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
</TextField>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
InputWithButton.displayName = "InputWithButton";
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { ChevronDown } from "@untitledui/icons";
|
|
5
|
+
import type { Key } from "react-aria-components";
|
|
6
|
+
import { TextField } from "react-aria-components";
|
|
7
|
+
import HintText from "@/components/shared/inputs/hint-text";
|
|
8
|
+
import type { InputBaseProps } from "@/components/shared/inputs/input";
|
|
9
|
+
import { InputBase } from "@/components/shared/inputs/input";
|
|
10
|
+
import Label from "@/components/shared/inputs/label";
|
|
11
|
+
import { cx, sortCx } from "@/components/utils";
|
|
12
|
+
|
|
13
|
+
interface SelectorComponentProps {
|
|
14
|
+
size: "sm" | "md";
|
|
15
|
+
options: Option[];
|
|
16
|
+
selectedKey?: Key;
|
|
17
|
+
className?: string;
|
|
18
|
+
isInvalid?: boolean;
|
|
19
|
+
selectName?: string;
|
|
20
|
+
isDisabled?: boolean;
|
|
21
|
+
iconClassName?: string;
|
|
22
|
+
onSelectionChange?: (value: Key) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SelectorComponent = (props: SelectorComponentProps) => {
|
|
26
|
+
return (
|
|
27
|
+
<div className="relative inline-flex h-full items-center">
|
|
28
|
+
<select
|
|
29
|
+
name={props.selectName}
|
|
30
|
+
value={props.selectedKey}
|
|
31
|
+
disabled={props.isDisabled}
|
|
32
|
+
autoComplete="input-dropdown"
|
|
33
|
+
onChange={(e) => props.onSelectionChange?.(e.target.value)}
|
|
34
|
+
className={cx(
|
|
35
|
+
"flex h-full w-min appearance-none items-center gap-1 bg-inherit px-3 py-2 tt-md text-tertiary outline-hidden ring-inset focus:ring-2 focus:ring-border-brand disabled:cursor-not-allowed",
|
|
36
|
+
props.isInvalid && "focus:ring-2 focus:ring-border-error",
|
|
37
|
+
props.isDisabled && "text-disabled",
|
|
38
|
+
props.className,
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{props.options.map((option) => (
|
|
42
|
+
<option key={option.value} value={option.value}>
|
|
43
|
+
{option.label}
|
|
44
|
+
</option>
|
|
45
|
+
))}
|
|
46
|
+
</select>
|
|
47
|
+
<ChevronDown className={cx("pointer-events-none absolute right-0 size-4 stroke-[2.625px] text-fg-quaternary", props.iconClassName)} />
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type Option = {
|
|
53
|
+
label: string;
|
|
54
|
+
value: string | number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface InputWithDropdownProps extends Omit<InputBaseProps, "icon"> {
|
|
58
|
+
selectedKey?: Key;
|
|
59
|
+
selectName?: string;
|
|
60
|
+
leadingText?: string;
|
|
61
|
+
leadingOptions?: Option[];
|
|
62
|
+
trailingOptions?: Option[];
|
|
63
|
+
onSelectionChange?: (value: Key) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const InputWithDropdown = ({
|
|
67
|
+
size = "sm",
|
|
68
|
+
leadingOptions,
|
|
69
|
+
trailingOptions,
|
|
70
|
+
leadingText,
|
|
71
|
+
className,
|
|
72
|
+
selectedKey,
|
|
73
|
+
label,
|
|
74
|
+
hint,
|
|
75
|
+
onChange,
|
|
76
|
+
onSelectionChange,
|
|
77
|
+
...props
|
|
78
|
+
}: InputWithDropdownProps) => {
|
|
79
|
+
const hasLeadingDropdown = !!leadingOptions?.length;
|
|
80
|
+
const hasTrailingDropdown = !!trailingOptions?.length;
|
|
81
|
+
|
|
82
|
+
const paddings = sortCx({
|
|
83
|
+
sm: {
|
|
84
|
+
input: cx(hasLeadingDropdown && "px-2.5 pl-2.5", hasTrailingDropdown && (leadingText ? "pr-6! pl-0" : "pr-6! pl-3")),
|
|
85
|
+
leadingText: "pl-3",
|
|
86
|
+
dropdownLeading: "rounded-l-lg py-2 pr-4.5 pl-3",
|
|
87
|
+
dropdownTrailing: "rounded-r-lg py-2 pr-9 pl-3",
|
|
88
|
+
dropdownTrailingIcon: "right-3",
|
|
89
|
+
},
|
|
90
|
+
md: {
|
|
91
|
+
input: cx(hasLeadingDropdown && "px-3 pl-3", hasTrailingDropdown && (leadingText ? "pr-6! pl-0!" : "pr-6! pl-3!")),
|
|
92
|
+
leadingText: "pl-3.5",
|
|
93
|
+
dropdownLeading: "rounded-l-lg py-2.5 pr-4.5 pl-3.5",
|
|
94
|
+
dropdownTrailing: "rounded-r-lg py-2.5 pr-9.5 pl-3.5",
|
|
95
|
+
dropdownTrailingIcon: "right-3.5",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<TextField
|
|
101
|
+
aria-label={!label ? props?.placeholder : undefined}
|
|
102
|
+
{...props}
|
|
103
|
+
className={(state) =>
|
|
104
|
+
cx("flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
{({ isDisabled, isInvalid, isRequired }) => (
|
|
108
|
+
<>
|
|
109
|
+
{label && <Label {...{ isRequired }}>{label}</Label>}
|
|
110
|
+
|
|
111
|
+
<div
|
|
112
|
+
className={cx(
|
|
113
|
+
"relative flex h-max w-full flex-row justify-start rounded-lg bg-primary shadow-xs ring-1 ring-border-primary transition-all duration-100 ease-linear ring-inset",
|
|
114
|
+
|
|
115
|
+
// Only apply focus ring when child input is focused
|
|
116
|
+
"has-[input:focus]:ring-2 has-[input:focus]:ring-border-brand",
|
|
117
|
+
|
|
118
|
+
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-border-disabled",
|
|
119
|
+
isInvalid && "ring-border-error_subtle has-[input:focus]:ring-border-error",
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{/* leading dropdown with padding style */}
|
|
123
|
+
{hasLeadingDropdown && (
|
|
124
|
+
<SelectorComponent
|
|
125
|
+
{...{
|
|
126
|
+
size,
|
|
127
|
+
isInvalid,
|
|
128
|
+
isDisabled,
|
|
129
|
+
selectedKey,
|
|
130
|
+
onSelectionChange,
|
|
131
|
+
options: leadingOptions,
|
|
132
|
+
className: paddings[size].dropdownLeading,
|
|
133
|
+
name: props.selectName,
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* leading text if trailing dropdown is applied */}
|
|
139
|
+
{leadingText && (
|
|
140
|
+
<span className={cx("my-auto grow pr-2", paddings[size].leadingText)}>
|
|
141
|
+
<p className={cx("tt-md text-tertiary", isDisabled && "text-disabled")}>{leadingText}</p>
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<InputBase
|
|
146
|
+
{...props}
|
|
147
|
+
{...{ isDisabled, isInvalid }}
|
|
148
|
+
wrapperClassName={cx("bg-transparent shadow-none! ring-0 focus-within:ring-0")}
|
|
149
|
+
inputClassName={cx(paddings[size].input)}
|
|
150
|
+
tooltipClassName={cx(hasTrailingDropdown && "right-0")}
|
|
151
|
+
/>
|
|
152
|
+
|
|
153
|
+
{/* trailing dropdown with padding style */}
|
|
154
|
+
{hasTrailingDropdown && (
|
|
155
|
+
<SelectorComponent
|
|
156
|
+
{...{
|
|
157
|
+
size,
|
|
158
|
+
isInvalid,
|
|
159
|
+
isDisabled,
|
|
160
|
+
selectedKey,
|
|
161
|
+
onSelectionChange,
|
|
162
|
+
options: trailingOptions,
|
|
163
|
+
iconClassName: paddings[size].dropdownTrailingIcon,
|
|
164
|
+
className: paddings[size].dropdownTrailing,
|
|
165
|
+
name: props.selectName,
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{hint && <HintText {...{ isInvalid }}>{hint}</HintText>}
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
</TextField>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
InputWithDropdown.displayName = "InputWithDropdown";
|