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 { HTMLAttributes } from "react";
|
|
4
|
+
import HintText from "@/components/shared/inputs/hint-text";
|
|
5
|
+
import type { InputBaseProps } from "@/components/shared/inputs/input";
|
|
6
|
+
import { InputBase, TextField } from "@/components/shared/inputs/input";
|
|
7
|
+
import Label from "@/components/shared/inputs/label";
|
|
8
|
+
import { cx } from "@/components/utils";
|
|
9
|
+
|
|
10
|
+
interface InputPrefixProps extends HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
position?: "leading" | "trailing";
|
|
12
|
+
size?: "sm" | "md";
|
|
13
|
+
isDisabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const InputPrefix = ({ position = "leading", size = "sm", isDisabled, children, ...props }: InputPrefixProps) => {
|
|
17
|
+
const styles = {
|
|
18
|
+
sm: "px-3 py-2",
|
|
19
|
+
md: "py-2.5 pl-3.5 pr-3",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
{...props}
|
|
25
|
+
className={cx(
|
|
26
|
+
"flex tt-md text-tertiary shadow-xs ring-1 ring-border-primary ring-inset",
|
|
27
|
+
styles[size],
|
|
28
|
+
position === "leading" && "-mr-px rounded-l-lg",
|
|
29
|
+
position === "trailing" && "-ml-px rounded-r-lg",
|
|
30
|
+
|
|
31
|
+
// Disabled state
|
|
32
|
+
isDisabled && "border-disabled bg-disabled_subtle text-tertiary",
|
|
33
|
+
"group-disabled:border-disabled group-disabled:bg-disabled_subtle group-disabled:text-tertiary",
|
|
34
|
+
|
|
35
|
+
props.className,
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface InputWithPrefixProps extends Omit<InputBaseProps, "icon"> {
|
|
44
|
+
leadingText?: string;
|
|
45
|
+
trailingText?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const InputWithPrefix = ({ size = "sm", placeholder, leadingText, trailingText, className, label, hint, ...props }: InputWithPrefixProps) => {
|
|
49
|
+
return (
|
|
50
|
+
<TextField aria-label={!label ? placeholder : undefined} {...props} className={className}>
|
|
51
|
+
{label && <Label>{label}</Label>}
|
|
52
|
+
|
|
53
|
+
<div className="flex w-full">
|
|
54
|
+
{leadingText && (
|
|
55
|
+
<InputPrefix position="leading" size={size}>
|
|
56
|
+
{leadingText}
|
|
57
|
+
</InputPrefix>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<InputBase {...props} {...{ size, placeholder }} wrapperClassName={cx(trailingText && "rounded-r-none", leadingText && "rounded-l-none")} />
|
|
61
|
+
|
|
62
|
+
{trailingText && (
|
|
63
|
+
<InputPrefix position="trailing" size={size}>
|
|
64
|
+
{trailingText}
|
|
65
|
+
</InputPrefix>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{hint && <HintText>{hint}</HintText>}
|
|
70
|
+
</TextField>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
InputWithPrefix.displayName = "InputWithPrefix";
|
|
@@ -0,0 +1,46 @@
|
|
|
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 "@/components/shared/tooltips/tooltips";
|
|
8
|
+
import { cx } from "@/components/utils";
|
|
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
|
+
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 w-full cursor-default items-center gap-0.5 tt-sm-md 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 className="cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover">
|
|
36
|
+
<HelpCircle className="size-4" />
|
|
37
|
+
</TooltipTrigger>
|
|
38
|
+
</Tooltip>
|
|
39
|
+
)}
|
|
40
|
+
</AriaLabel>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
Label.displayName = "Label";
|
|
45
|
+
|
|
46
|
+
export default Label;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode, Ref } from "react";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import type { TextAreaProps as AriaTextAreaProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
|
6
|
+
import { TextArea as AriaTextArea } from "react-aria-components";
|
|
7
|
+
import HintText from "@/components/shared/inputs/hint-text";
|
|
8
|
+
import Label from "@/components/shared/inputs/label";
|
|
9
|
+
import { cx } from "@/components/utils";
|
|
10
|
+
import { TextField } from "../input";
|
|
11
|
+
|
|
12
|
+
interface TextAreaBaseProps extends AriaTextAreaProps {
|
|
13
|
+
ref?: Ref<HTMLTextAreaElement>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TextAreaBase = ({ className, ...props }: TextAreaBaseProps) => {
|
|
17
|
+
return (
|
|
18
|
+
<AriaTextArea
|
|
19
|
+
{...props}
|
|
20
|
+
className={(state) =>
|
|
21
|
+
cx(
|
|
22
|
+
"w-full scroll-py-3 rounded-lg bg-primary px-3.5 py-3 tt-md text-primary shadow-xs ring-1 ring-border-primary transition duration-100 ease-linear ring-inset placeholder:text-placeholder autofill:rounded-lg autofill:text-primary focus:outline-hidden",
|
|
23
|
+
|
|
24
|
+
// Resize handle
|
|
25
|
+
"[&::-webkit-resizer]:bg-[url(/shared/textarea-resize-handle-light-mode.svg)] [&::-webkit-resizer]:bg-contain dark:[&::-webkit-resizer]:bg-[url(/shared/textarea-resize-handle-dark-mode.svg)]",
|
|
26
|
+
|
|
27
|
+
state.isFocused && !state.isDisabled && "ring-2 ring-border-brand",
|
|
28
|
+
state.isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled ring-border-disabled",
|
|
29
|
+
state.isInvalid && "ring-border-error_subtle",
|
|
30
|
+
state.isInvalid && state.isFocused && "ring-2 ring-border-error",
|
|
31
|
+
|
|
32
|
+
typeof className === "function" ? className(state) : className,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
TextAreaBase.displayName = "TextAreaBase";
|
|
40
|
+
|
|
41
|
+
interface TextFieldProps extends AriaTextAreaProps {
|
|
42
|
+
label?: string;
|
|
43
|
+
hint?: ReactNode;
|
|
44
|
+
tooltip?: string;
|
|
45
|
+
ref?: Ref<HTMLDivElement>;
|
|
46
|
+
isInvalid?: AriaTextFieldProps["isInvalid"];
|
|
47
|
+
isDisabled?: AriaTextFieldProps["isDisabled"];
|
|
48
|
+
isRequired?: AriaTextFieldProps["isRequired"];
|
|
49
|
+
isReadOnly?: AriaTextFieldProps["isReadOnly"];
|
|
50
|
+
wrapperClassName?: AriaTextFieldProps["className"];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const TextArea = ({
|
|
54
|
+
label,
|
|
55
|
+
hint,
|
|
56
|
+
wrapperClassName,
|
|
57
|
+
isDisabled,
|
|
58
|
+
isInvalid,
|
|
59
|
+
isRequired,
|
|
60
|
+
isReadOnly,
|
|
61
|
+
tooltip,
|
|
62
|
+
value,
|
|
63
|
+
defaultValue,
|
|
64
|
+
ref,
|
|
65
|
+
...textAreaProps
|
|
66
|
+
}: TextFieldProps) => {
|
|
67
|
+
return (
|
|
68
|
+
<TextField
|
|
69
|
+
{...{ ref, isDisabled, isInvalid, isReadOnly, isRequired, className: wrapperClassName }}
|
|
70
|
+
value={value as string}
|
|
71
|
+
defaultValue={defaultValue as string}
|
|
72
|
+
>
|
|
73
|
+
{label && <Label tooltip={tooltip}>{label}</Label>}
|
|
74
|
+
|
|
75
|
+
<TextAreaBase {...textAreaProps} />
|
|
76
|
+
|
|
77
|
+
{hint && <HintText>{hint}</HintText>}
|
|
78
|
+
</TextField>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
TextArea.displayName = "TextArea";
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cx as clx, sortCx } from "@/components/utils";
|
|
4
|
+
|
|
5
|
+
interface ProgressBarProps {
|
|
6
|
+
value: number;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
size: "xxs" | "xs" | "sm" | "md" | "lg";
|
|
10
|
+
label?: string;
|
|
11
|
+
valueFormatter?: (value: number, valueInPercentage: number) => string | number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sizes = sortCx({
|
|
15
|
+
xxs: {
|
|
16
|
+
strokeWidth: 6,
|
|
17
|
+
radius: 29,
|
|
18
|
+
valueClass: "tt-sm-semi text-primary",
|
|
19
|
+
labelClass: "tt-xs-md text-tertiary",
|
|
20
|
+
halfCircleTextPosition: "absolute bottom-0.5 text-center",
|
|
21
|
+
},
|
|
22
|
+
xs: {
|
|
23
|
+
strokeWidth: 16,
|
|
24
|
+
radius: 72,
|
|
25
|
+
valueClass: "td-xs-semi text-primary",
|
|
26
|
+
labelClass: "tt-xs-md text-tertiary",
|
|
27
|
+
halfCircleTextPosition: "absolute bottom-0.5 text-center",
|
|
28
|
+
},
|
|
29
|
+
sm: {
|
|
30
|
+
strokeWidth: 20,
|
|
31
|
+
radius: 90,
|
|
32
|
+
valueClass: "td-sm-semi text-primary",
|
|
33
|
+
labelClass: "tt-xs-md text-tertiary",
|
|
34
|
+
halfCircleTextPosition: "absolute bottom-1 text-center",
|
|
35
|
+
},
|
|
36
|
+
md: {
|
|
37
|
+
strokeWidth: 24,
|
|
38
|
+
radius: 108,
|
|
39
|
+
valueClass: "td-md-semi text-primary",
|
|
40
|
+
labelClass: "tt-sm-md text-tertiary",
|
|
41
|
+
halfCircleTextPosition: "absolute bottom-1 text-center",
|
|
42
|
+
},
|
|
43
|
+
lg: {
|
|
44
|
+
strokeWidth: 28,
|
|
45
|
+
radius: 126,
|
|
46
|
+
valueClass: "td-lg-semi text-primary",
|
|
47
|
+
labelClass: "tt-sm-md text-tertiary",
|
|
48
|
+
halfCircleTextPosition: "absolute bottom-0 text-center",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const ProgressBarCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {
|
|
53
|
+
const percentage = Math.round(((value - min) * 100) / (max - min));
|
|
54
|
+
|
|
55
|
+
const sizeConfig = sizes[size];
|
|
56
|
+
|
|
57
|
+
const { strokeWidth, radius, valueClass, labelClass } = sizeConfig;
|
|
58
|
+
|
|
59
|
+
const diameter = 2 * (radius + strokeWidth / 2);
|
|
60
|
+
const width = diameter;
|
|
61
|
+
const height = diameter;
|
|
62
|
+
const viewBox = `0 0 ${width} ${height}`;
|
|
63
|
+
const cx = diameter / 2;
|
|
64
|
+
const cy = diameter / 2;
|
|
65
|
+
|
|
66
|
+
const textPosition = label ? "absolute text-center" : "absolute text-primary";
|
|
67
|
+
const strokeDashoffset = 100 - percentage;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex flex-col items-center gap-0.5">
|
|
71
|
+
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
|
72
|
+
<svg className="-rotate-90" width={width} height={height} viewBox={viewBox}>
|
|
73
|
+
{/* Background circle */}
|
|
74
|
+
<circle
|
|
75
|
+
className="stroke-bg-quaternary"
|
|
76
|
+
cx={cx}
|
|
77
|
+
cy={cy}
|
|
78
|
+
r={radius}
|
|
79
|
+
fill="none"
|
|
80
|
+
strokeWidth={strokeWidth}
|
|
81
|
+
pathLength="100"
|
|
82
|
+
strokeDasharray="100"
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{/* Foreground circle */}
|
|
87
|
+
<circle
|
|
88
|
+
className="stroke-fg-brand-primary"
|
|
89
|
+
cx={cx}
|
|
90
|
+
cy={cy}
|
|
91
|
+
r={radius}
|
|
92
|
+
fill="none"
|
|
93
|
+
strokeWidth={strokeWidth}
|
|
94
|
+
pathLength="100"
|
|
95
|
+
strokeDasharray="100"
|
|
96
|
+
strokeLinecap="round"
|
|
97
|
+
strokeDashoffset={strokeDashoffset}
|
|
98
|
+
/>
|
|
99
|
+
</svg>
|
|
100
|
+
{label && size !== "xxs" ? (
|
|
101
|
+
<div className="absolute text-center">
|
|
102
|
+
<div className={labelClass}>{label}</div>
|
|
103
|
+
<div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<span className={clx(textPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{label && size === "xxs" && <div className={labelClass}>{label}</div>}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const ProgressBarHalfCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {
|
|
116
|
+
const percentage = Math.round(((value - min) * 100) / (max - min));
|
|
117
|
+
|
|
118
|
+
const sizeConfig = sizes[size];
|
|
119
|
+
|
|
120
|
+
const { strokeWidth, radius, valueClass, labelClass, halfCircleTextPosition } = sizeConfig;
|
|
121
|
+
|
|
122
|
+
const width = 2 * (radius + strokeWidth / 2);
|
|
123
|
+
const height = radius + strokeWidth;
|
|
124
|
+
const viewBox = `0 0 ${width} ${height}`;
|
|
125
|
+
const cx = "50%";
|
|
126
|
+
const cy = radius + strokeWidth / 2;
|
|
127
|
+
|
|
128
|
+
const strokeDashoffset = -50 - (100 - percentage) / 2;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="flex flex-col items-center gap-0.5">
|
|
132
|
+
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
|
133
|
+
<svg width={width} height={height} viewBox={viewBox}>
|
|
134
|
+
{/* Background half-circle */}
|
|
135
|
+
<circle
|
|
136
|
+
className="stroke-bg-quaternary"
|
|
137
|
+
cx={cx}
|
|
138
|
+
cy={cy}
|
|
139
|
+
r={radius}
|
|
140
|
+
fill="none"
|
|
141
|
+
strokeWidth={strokeWidth}
|
|
142
|
+
pathLength="100"
|
|
143
|
+
strokeDasharray="100"
|
|
144
|
+
strokeDashoffset="-50"
|
|
145
|
+
strokeLinecap="round"
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
{/* Foreground half-circle */}
|
|
149
|
+
<circle
|
|
150
|
+
className="origin-center -scale-x-100 stroke-fg-brand-primary"
|
|
151
|
+
cx={cx}
|
|
152
|
+
cy={cy}
|
|
153
|
+
r={radius}
|
|
154
|
+
fill="none"
|
|
155
|
+
strokeWidth={strokeWidth}
|
|
156
|
+
pathLength="100"
|
|
157
|
+
strokeDasharray="100"
|
|
158
|
+
strokeDashoffset={strokeDashoffset}
|
|
159
|
+
strokeLinecap="round"
|
|
160
|
+
/>
|
|
161
|
+
</svg>
|
|
162
|
+
|
|
163
|
+
{label && size !== "xxs" ? (
|
|
164
|
+
<div className={halfCircleTextPosition}>
|
|
165
|
+
<div className={labelClass}>{label}</div>
|
|
166
|
+
<div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
|
167
|
+
</div>
|
|
168
|
+
) : (
|
|
169
|
+
<span className={clx(halfCircleTextPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{label && size === "xxs" && <div className={labelClass}>{label}</div>}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cx } from "@/components/utils";
|
|
4
|
+
|
|
5
|
+
export interface ProgressBarProps {
|
|
6
|
+
value: number;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
className?: string;
|
|
10
|
+
progressClassName?: string;
|
|
11
|
+
valueFormatter?: (value: number, valueInPercentage: number) => string | number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ProgressBar = ({ value, min = 0, max = 100, className, progressClassName }: ProgressBarProps) => {
|
|
15
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
role="progressbar"
|
|
20
|
+
aria-valuenow={value}
|
|
21
|
+
aria-valuemin={min}
|
|
22
|
+
aria-valuemax={max}
|
|
23
|
+
className={cx("h-2 w-full overflow-hidden rounded-md bg-quaternary", className)}
|
|
24
|
+
>
|
|
25
|
+
<div
|
|
26
|
+
// Use transform instead of width to avoid layout thrashing (and for smoother animation)
|
|
27
|
+
style={{ transform: `translateX(-${100 - percentage}%)` }}
|
|
28
|
+
className={cx("size-full rounded-md bg-fg-brand-primary transition duration-75 ease-linear", progressClassName)}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const ProgressBarTextRight = ({ value, min = 0, max = 100, valueFormatter }: ProgressBarProps) => {
|
|
35
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex items-center gap-3">
|
|
39
|
+
<ProgressBar min={min} max={max} value={value} />
|
|
40
|
+
<span className="tt-sm-md text-secondary tabular-nums">{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const ProgressBarTextBottom = ({ value, min = 0, max = 100, valueFormatter }: ProgressBarProps) => {
|
|
46
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col items-end gap-2">
|
|
50
|
+
<ProgressBar min={min} max={max} value={value} />
|
|
51
|
+
<span className="tt-sm-md text-secondary tabular-nums">{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const ProgressBarTextTopFloating = ({ value, min = 0, max = 100, valueFormatter }: ProgressBarProps) => {
|
|
57
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="relative flex flex-col items-end gap-2">
|
|
61
|
+
<ProgressBar min={min} max={max} value={value} />
|
|
62
|
+
<div
|
|
63
|
+
style={{ left: `${percentage}%` }}
|
|
64
|
+
className="absolute -top-2 -translate-x-1/2 -translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-border-secondary_alt"
|
|
65
|
+
>
|
|
66
|
+
<div className="tt-xs-semi text-secondary tabular-nums">{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const ProgressBarTextBottomFloating = ({ value, min = 0, max = 100, valueFormatter }: ProgressBarProps) => {
|
|
73
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="relative flex flex-col items-end gap-2">
|
|
77
|
+
<ProgressBar min={min} max={max} value={value} />
|
|
78
|
+
<div
|
|
79
|
+
style={{ left: `${percentage}%` }}
|
|
80
|
+
className="absolute -bottom-2 -translate-x-1/2 translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-border-secondary_alt"
|
|
81
|
+
>
|
|
82
|
+
<div className="tt-xs-semi text-secondary">{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
const CircleProgressBar = (props: { value: number; min?: 0; max?: 100 }) => {
|
|
4
|
+
const { value, min = 0, max = 100 } = props;
|
|
5
|
+
const percentage = ((value - min) * 100) / (max - min);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
|
9
|
+
<span className="absolute tt-sm-md text-primary">{percentage}%</span>
|
|
10
|
+
<svg className="size-16 -rotate-90" viewBox="0 0 60 60">
|
|
11
|
+
<circle className="stroke-bg-quaternary" cx="30" cy="30" r="26" fill="none" strokeWidth="6" />
|
|
12
|
+
<circle
|
|
13
|
+
className="stroke-fg-brand-primary"
|
|
14
|
+
style={{
|
|
15
|
+
strokeDashoffset: `calc(100 - ${percentage})`,
|
|
16
|
+
}}
|
|
17
|
+
cx="30"
|
|
18
|
+
cy="30"
|
|
19
|
+
r="26"
|
|
20
|
+
fill="none"
|
|
21
|
+
strokeWidth="6"
|
|
22
|
+
strokeDasharray="100"
|
|
23
|
+
pathLength="100"
|
|
24
|
+
strokeLinecap="round"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, type Ref, createContext, useContext } from "react";
|
|
4
|
+
import type { RadioGroupProps as AriaRadioGroupProps } from "react-aria-components";
|
|
5
|
+
import { type RadioProps as AriaRadioProps, Radio, RadioGroup } from "react-aria-components";
|
|
6
|
+
import { cx } from "@/components/utils";
|
|
7
|
+
|
|
8
|
+
export interface RadioButtonGroupContextType {
|
|
9
|
+
size?: "sm" | "md";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const RadioButtonGroupContext = createContext<RadioButtonGroupContextType | null>(null);
|
|
13
|
+
|
|
14
|
+
export interface RadioButtonBaseProps {
|
|
15
|
+
size?: "sm" | "md";
|
|
16
|
+
className?: string;
|
|
17
|
+
isFocused?: boolean;
|
|
18
|
+
isSelected?: boolean;
|
|
19
|
+
isDisabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const RadioButtonBase = ({ className, isFocused, isSelected, isDisabled, size = "sm" }: RadioButtonBaseProps) => {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={cx(
|
|
26
|
+
"flex size-4 min-h-4 min-w-4 cursor-pointer appearance-none items-center justify-center rounded-full bg-primary ring-1 ring-border-primary ring-inset",
|
|
27
|
+
size === "md" && "size-5 min-h-5 min-w-5",
|
|
28
|
+
isSelected && !isDisabled && "bg-brand-solid ring-bg-brand-solid",
|
|
29
|
+
isDisabled && "cursor-not-allowed border-disabled bg-disabled_subtle",
|
|
30
|
+
isFocused && "outline-2 outline-offset-2 outline-focus-ring",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
className={cx(
|
|
36
|
+
"size-1.5 rounded-full bg-fg-white opacity-0 transition-inherit-all",
|
|
37
|
+
size === "md" && "size-2",
|
|
38
|
+
isDisabled && "bg-fg-disabled_subtle",
|
|
39
|
+
isSelected && "opacity-100",
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
RadioButtonBase.displayName = "RadioButtonBase";
|
|
46
|
+
|
|
47
|
+
interface RadioButtonProps extends AriaRadioProps {
|
|
48
|
+
size?: "sm" | "md";
|
|
49
|
+
label?: ReactNode;
|
|
50
|
+
hint?: ReactNode;
|
|
51
|
+
ref?: Ref<HTMLLabelElement>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const RadioButton = ({ label, hint, className, size = "sm", ...ariaRadioProps }: RadioButtonProps) => {
|
|
55
|
+
const context = useContext(RadioButtonGroupContext);
|
|
56
|
+
|
|
57
|
+
size = context?.size ?? size;
|
|
58
|
+
|
|
59
|
+
const sizes = {
|
|
60
|
+
sm: {
|
|
61
|
+
root: "gap-2",
|
|
62
|
+
textWrapper: "",
|
|
63
|
+
label: "tt-sm-md",
|
|
64
|
+
hint: "tt-sm",
|
|
65
|
+
},
|
|
66
|
+
md: {
|
|
67
|
+
root: "gap-3",
|
|
68
|
+
textWrapper: "gap-0.5",
|
|
69
|
+
label: "tt-md-md",
|
|
70
|
+
hint: "tt-md",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Radio
|
|
76
|
+
{...ariaRadioProps}
|
|
77
|
+
className={(renderProps) =>
|
|
78
|
+
cx(
|
|
79
|
+
"flex items-start",
|
|
80
|
+
renderProps.isDisabled && "cursor-not-allowed",
|
|
81
|
+
sizes[size].root,
|
|
82
|
+
typeof className === "function" ? className(renderProps) : className,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
{({ isSelected, isDisabled, isFocused }) => (
|
|
87
|
+
<>
|
|
88
|
+
<RadioButtonBase
|
|
89
|
+
size={size}
|
|
90
|
+
isSelected={isSelected}
|
|
91
|
+
isDisabled={isDisabled}
|
|
92
|
+
isFocused={isFocused}
|
|
93
|
+
className={label || hint ? "mt-0.5" : ""}
|
|
94
|
+
/>
|
|
95
|
+
{(label || hint) && (
|
|
96
|
+
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
|
97
|
+
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
|
98
|
+
{hint && (
|
|
99
|
+
<span className={cx("pointer-events-none text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
|
100
|
+
{hint}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
</Radio>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
RadioButton.displayName = "Checkbox";
|
|
111
|
+
|
|
112
|
+
interface RadioButtonGroupProps extends RadioButtonGroupContextType, AriaRadioGroupProps {
|
|
113
|
+
children: ReactNode;
|
|
114
|
+
className?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const RadioButtonGroup = ({ children, className, size = "sm", ...props }: RadioButtonGroupProps) => {
|
|
118
|
+
return (
|
|
119
|
+
<RadioButtonGroupContext.Provider value={{ size }}>
|
|
120
|
+
<RadioGroup {...props} className={cx("flex flex-col gap-4", className)}>
|
|
121
|
+
{children}
|
|
122
|
+
</RadioGroup>
|
|
123
|
+
</RadioButtonGroupContext.Provider>
|
|
124
|
+
);
|
|
125
|
+
};
|