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.
Files changed (121) hide show
  1. package/dist/commands/add.js +339 -0
  2. package/dist/commands/init.js +436 -0
  3. package/dist/helper/download-tar-api.js +129 -0
  4. package/dist/helper/download-tar.js +81 -0
  5. package/dist/helper/find-css-file.js +19 -0
  6. package/dist/helper/formatText.js +37 -0
  7. package/dist/helper/get-components-api.js +47 -0
  8. package/dist/helper/get-components-list.js +62 -0
  9. package/dist/helper/get-components.js +19 -0
  10. package/dist/helper/get-config.js +163 -0
  11. package/dist/helper/get-package-info.js +99 -0
  12. package/dist/helper/get-pkg-manager.js +16 -0
  13. package/dist/helper/get-project.js +176 -0
  14. package/dist/helper/install-template.js +29 -0
  15. package/dist/helper/match-color-css.js +82 -0
  16. package/dist/helper/update-color-css.js +134 -0
  17. package/dist/index.js +25 -0
  18. package/dist/package.json +50 -0
  19. package/dist/res/components.json +520 -0
  20. package/dist/res/config.json +3 -0
  21. package/package.json +61 -0
  22. package/templates/default/.prettierrc +10 -0
  23. package/templates/default/README.md +36 -0
  24. package/templates/default/eslint.config.mjs +58 -0
  25. package/templates/default/next.config.ts +6 -0
  26. package/templates/default/package.json +57 -0
  27. package/templates/default/postcss.config.js +5 -0
  28. package/templates/default/public/favicon.ico +0 -0
  29. package/templates/default/public/marketing/smiling-girl.png +0 -0
  30. package/templates/default/public/marketing/spirals.webp +0 -0
  31. package/templates/default/src/app/home-screen.tsx +109 -0
  32. package/templates/default/src/app/layout.tsx +42 -0
  33. package/templates/default/src/app/not-found.tsx +40 -0
  34. package/templates/default/src/app/page.tsx +3 -0
  35. package/templates/default/src/components/foundations/dot-icon.tsx +27 -0
  36. package/templates/default/src/components/foundations/featured-icon/featured-icons.tsx +153 -0
  37. package/templates/default/src/components/foundations/logo/UntitledLogo.tsx +63 -0
  38. package/templates/default/src/components/foundations/logo/UntitledLogoMinimal.tsx +164 -0
  39. package/templates/default/src/components/foundations/payment-icons/amex-icon.tsx +19 -0
  40. package/templates/default/src/components/foundations/payment-icons/apple-pay-icon.tsx +27 -0
  41. package/templates/default/src/components/foundations/payment-icons/discover-icon.tsx +34 -0
  42. package/templates/default/src/components/foundations/payment-icons/index.tsx +10 -0
  43. package/templates/default/src/components/foundations/payment-icons/mastercard-icon.tsx +39 -0
  44. package/templates/default/src/components/foundations/payment-icons/paypal-icon.tsx +45 -0
  45. package/templates/default/src/components/foundations/payment-icons/stripe-icon.tsx +27 -0
  46. package/templates/default/src/components/foundations/payment-icons/union-pay-icon.tsx +37 -0
  47. package/templates/default/src/components/foundations/payment-icons/visa-icon.tsx +27 -0
  48. package/templates/default/src/components/marketing/header-navigation/base-components/nav-menu-item.tsx +41 -0
  49. package/templates/default/src/components/marketing/header-navigation/components/header.tsx +245 -0
  50. package/templates/default/src/components/marketing/header-navigation/dropdown-header-navigation.tsx +53 -0
  51. package/templates/default/src/components/shared/avatar/avatar-label-group.tsx +32 -0
  52. package/templates/default/src/components/shared/avatar/avatar-profile-photo.tsx +84 -0
  53. package/templates/default/src/components/shared/avatar/avatar.tsx +131 -0
  54. package/templates/default/src/components/shared/avatar/base-components/avatar-add-button.tsx +33 -0
  55. package/templates/default/src/components/shared/avatar/base-components/avatar-company-icon.tsx +26 -0
  56. package/templates/default/src/components/shared/avatar/base-components/avatar-online-indicator.tsx +31 -0
  57. package/templates/default/src/components/shared/avatar/base-components/index.ts +4 -0
  58. package/templates/default/src/components/shared/avatar/base-components/verified-tick.tsx +34 -0
  59. package/templates/default/src/components/shared/avatar/utils.ts +12 -0
  60. package/templates/default/src/components/shared/badges/badge-groups.tsx +176 -0
  61. package/templates/default/src/components/shared/badges/badge-types.ts +264 -0
  62. package/templates/default/src/components/shared/badges/badges.tsx +479 -0
  63. package/templates/default/src/components/shared/button-group/button-group.tsx +97 -0
  64. package/templates/default/src/components/shared/buttons/app-store-buttons-outline.tsx +454 -0
  65. package/templates/default/src/components/shared/buttons/app-store-buttons.tsx +806 -0
  66. package/templates/default/src/components/shared/buttons/button-utility.tsx +87 -0
  67. package/templates/default/src/components/shared/buttons/button.tsx +284 -0
  68. package/templates/default/src/components/shared/buttons/close-button.tsx +39 -0
  69. package/templates/default/src/components/shared/buttons/social-button.tsx +135 -0
  70. package/templates/default/src/components/shared/buttons/social-logos.tsx +115 -0
  71. package/templates/default/src/components/shared/checkbox/checkbox.tsx +120 -0
  72. package/templates/default/src/components/shared/dropdown/dropdown.tsx +138 -0
  73. package/templates/default/src/components/shared/input-dropdown/combobox.tsx +161 -0
  74. package/templates/default/src/components/shared/input-dropdown/dropdown-item.tsx +98 -0
  75. package/templates/default/src/components/shared/input-dropdown/input-dropdown.tsx +172 -0
  76. package/templates/default/src/components/shared/input-dropdown/multi-select.tsx +373 -0
  77. package/templates/default/src/components/shared/input-dropdown/popover.tsx +36 -0
  78. package/templates/default/src/components/shared/input-dropdown/select.tsx +63 -0
  79. package/templates/default/src/components/shared/inputs/file-upload-trigger.tsx +74 -0
  80. package/templates/default/src/components/shared/inputs/form/form.tsx +10 -0
  81. package/templates/default/src/components/shared/inputs/hint-text.tsx +34 -0
  82. package/templates/default/src/components/shared/inputs/input/index.tsx +189 -0
  83. package/templates/default/src/components/shared/inputs/input/input-payment.tsx +134 -0
  84. package/templates/default/src/components/shared/inputs/input/input-with-button.tsx +69 -0
  85. package/templates/default/src/components/shared/inputs/input/input-with-dropdown.tsx +178 -0
  86. package/templates/default/src/components/shared/inputs/input/input-with-prefix.tsx +74 -0
  87. package/templates/default/src/components/shared/inputs/label.tsx +46 -0
  88. package/templates/default/src/components/shared/inputs/textarea/textarea.tsx +82 -0
  89. package/templates/default/src/components/shared/progress-indicators/progress-circles.tsx +176 -0
  90. package/templates/default/src/components/shared/progress-indicators/progress-indicators.tsx +86 -0
  91. package/templates/default/src/components/shared/progress-indicators/simple-circle.tsx +29 -0
  92. package/templates/default/src/components/shared/radio-buttons/radio-buttons.tsx +125 -0
  93. package/templates/default/src/components/shared/radio-groups/radio-group-avatar.tsx +62 -0
  94. package/templates/default/src/components/shared/radio-groups/radio-group-checkbox.tsx +72 -0
  95. package/templates/default/src/components/shared/radio-groups/radio-group-icon-card.tsx +95 -0
  96. package/templates/default/src/components/shared/radio-groups/radio-group-icon-simple.tsx +70 -0
  97. package/templates/default/src/components/shared/radio-groups/radio-group-payment-icon.tsx +71 -0
  98. package/templates/default/src/components/shared/radio-groups/radio-group-radio-button.tsx +76 -0
  99. package/templates/default/src/components/shared/radio-groups/radio-groups.tsx +8 -0
  100. package/templates/default/src/components/shared/slider/slider.tsx +76 -0
  101. package/templates/default/src/components/shared/tags/base-components/tag-checkbox.tsx +47 -0
  102. package/templates/default/src/components/shared/tags/base-components/tag-close-x.tsx +34 -0
  103. package/templates/default/src/components/shared/tags/tags.tsx +162 -0
  104. package/templates/default/src/components/shared/toggle/toggle.tsx +140 -0
  105. package/templates/default/src/components/shared/tooltips/tooltips.tsx +140 -0
  106. package/templates/default/src/components/utils/index.ts +48 -0
  107. package/templates/default/src/components/utils/isDeepEqual.ts +31 -0
  108. package/templates/default/src/components/utils/isReactComponent.ts +22 -0
  109. package/templates/default/src/components/utils/mergeRefs.ts +19 -0
  110. package/templates/default/src/components/utils/useBreakpoint.ts +36 -0
  111. package/templates/default/src/components/utils/uuid.ts +9 -0
  112. package/templates/default/src/fonts/GeistMonoVF.woff +0 -0
  113. package/templates/default/src/fonts/GeistVF.woff +0 -0
  114. package/templates/default/src/hooks/use-resize-observer.tsx +55 -0
  115. package/templates/default/src/providers/theme.tsx +11 -0
  116. package/templates/default/src/styles/colors.css +805 -0
  117. package/templates/default/src/styles/globals.css +86 -0
  118. package/templates/default/src/styles/text-styles.css +177 -0
  119. package/templates/default/src/styles/theme.css +1310 -0
  120. package/templates/default/src/styles/typography.css +428 -0
  121. 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
+ };