@studiocubics/components 0.0.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 (140) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +71 -0
  3. package/eslint.config.js +21 -0
  4. package/package.json +66 -0
  5. package/rollup.config.js +34 -0
  6. package/src/Cards/Card/Card.module.css +27 -0
  7. package/src/Cards/Card/Card.tsx +105 -0
  8. package/src/Cards/CollectionItemCard/CollectionItemCard.module.css +84 -0
  9. package/src/Cards/CollectionItemCard/CollectionItemCard.tsx +170 -0
  10. package/src/Cards/CollectionItemCard/CollectionItemCardActions.tsx +85 -0
  11. package/src/Cards/CollectionItemCard/_index.ts +2 -0
  12. package/src/Cards/GlassCard/GlassCard.module.css +71 -0
  13. package/src/Cards/GlassCard/GlassCard.tsx +80 -0
  14. package/src/Cards/_index.ts +3 -0
  15. package/src/Display/Accordion/Accordion.module.css +69 -0
  16. package/src/Display/Accordion/Accordion.tsx +61 -0
  17. package/src/Display/Accordion/AccordionItem.tsx +135 -0
  18. package/src/Display/Accordion/_index.ts +2 -0
  19. package/src/Display/Chip/Chip.module.css +64 -0
  20. package/src/Display/Chip/Chip.tsx +105 -0
  21. package/src/Display/IdentityDisplay/IdentityDisplay.module.css +95 -0
  22. package/src/Display/IdentityDisplay/IdentityDisplay.tsx +119 -0
  23. package/src/Display/InputErrors/InputErrors.module.css +6 -0
  24. package/src/Display/InputErrors/InputErrors.tsx +52 -0
  25. package/src/Display/Kbd/Kbd.module.css +29 -0
  26. package/src/Display/Kbd/Kbd.tsx +39 -0
  27. package/src/Display/Kbd/_index.ts +2 -0
  28. package/src/Display/Kbd/buttonList.tsx +246 -0
  29. package/src/Display/LabeledValue/LabeledValue.module.css +32 -0
  30. package/src/Display/LabeledValue/LabeledValue.tsx +20 -0
  31. package/src/Display/List/List.module.css +143 -0
  32. package/src/Display/List/List.tsx +298 -0
  33. package/src/Display/PasswordStrength/PasswordStrength.module.css +45 -0
  34. package/src/Display/PasswordStrength/PasswordStrength.tsx +41 -0
  35. package/src/Display/PasswordStrength/usePasswordStrength.tsx +77 -0
  36. package/src/Display/Skeleton/Skeleton.module.css +54 -0
  37. package/src/Display/Skeleton/Skeleton.tsx +28 -0
  38. package/src/Display/Toast/Toaster.tsx +58 -0
  39. package/src/Display/Toast/_index.ts +2 -0
  40. package/src/Display/Toast/toast.ts +44 -0
  41. package/src/Display/Tooltip/Tooltip.module.css +128 -0
  42. package/src/Display/Tooltip/Tooltip.tsx +93 -0
  43. package/src/Display/Tooltip/getArrowDirection.ts +55 -0
  44. package/src/Display/Tooltip/useTooltip.tsx +63 -0
  45. package/src/Display/_index.ts +12 -0
  46. package/src/Forms/ConfirmationForm/ConfirmationForm.module.css +23 -0
  47. package/src/Forms/ConfirmationForm/ConfirmationForm.tsx +60 -0
  48. package/src/Forms/_index.ts +1 -0
  49. package/src/Inputs/Button/Button.module.css +131 -0
  50. package/src/Inputs/Button/Button.tsx +178 -0
  51. package/src/Inputs/Checkbox/Checkbox.module.css +77 -0
  52. package/src/Inputs/Checkbox/Checkbox.tsx +191 -0
  53. package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.module.css +10 -0
  54. package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.tsx +83 -0
  55. package/src/Inputs/Checkbox/CheckboxSelectAll.tsx +34 -0
  56. package/src/Inputs/Checkbox/_index.ts +3 -0
  57. package/src/Inputs/PasswordInput/PasswordInput.module.css +111 -0
  58. package/src/Inputs/PasswordInput/PasswordInput.tsx +229 -0
  59. package/src/Inputs/Select/Select.module.css +138 -0
  60. package/src/Inputs/Select/Select.tsx +136 -0
  61. package/src/Inputs/Switch/Switch.module.css +119 -0
  62. package/src/Inputs/Switch/Switch.tsx +195 -0
  63. package/src/Inputs/TextAreaInput/TextAreaInput.module.css +65 -0
  64. package/src/Inputs/TextAreaInput/TextAreaInput.tsx +97 -0
  65. package/src/Inputs/TextInput/TextInput.module.css +112 -0
  66. package/src/Inputs/TextInput/TextInput.tsx +142 -0
  67. package/src/Inputs/ThemeToggle/ThemeToggleListItem.tsx +80 -0
  68. package/src/Inputs/ThemeToggle/_index.ts +1 -0
  69. package/src/Inputs/_index.ts +8 -0
  70. package/src/Layout/Dialog/Dialog.module.css +15 -0
  71. package/src/Layout/Dialog/Dialog.tsx +115 -0
  72. package/src/Layout/PageLayout/PageLayout.module.css +20 -0
  73. package/src/Layout/PageLayout/PageLayout.tsx +79 -0
  74. package/src/Layout/PageLayoutPagination/PageLayoutPagination.module.css +5 -0
  75. package/src/Layout/PageLayoutPagination/PageLayoutPagination.tsx +40 -0
  76. package/src/Layout/PageLayoutTabs/PageLayoutTabs.module.css +3 -0
  77. package/src/Layout/PageLayoutTabs/PageLayoutTabs.tsx +62 -0
  78. package/src/Layout/Popover/Popover.module.css +9 -0
  79. package/src/Layout/Popover/Popover.tsx +145 -0
  80. package/src/Layout/SectionWrapper/SectionWrapper.module.css +31 -0
  81. package/src/Layout/SectionWrapper/SectionWrapper.tsx +62 -0
  82. package/src/Layout/Sidebar/Sidebar.module.css +17 -0
  83. package/src/Layout/Sidebar/Sidebar.tsx +39 -0
  84. package/src/Layout/Sidebar/SidebarBody/SidebarBody.module.css +31 -0
  85. package/src/Layout/Sidebar/SidebarBody/SidebarBody.tsx +18 -0
  86. package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.module.css +20 -0
  87. package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.tsx +19 -0
  88. package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.module.css +35 -0
  89. package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.tsx +19 -0
  90. package/src/Layout/Sidebar/SidebarHeader/SidebarHeader.tsx +14 -0
  91. package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.module.css +12 -0
  92. package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.tsx +11 -0
  93. package/src/Layout/Sidebar/_index.ts +6 -0
  94. package/src/Layout/Table/Table.module.css +46 -0
  95. package/src/Layout/Table/Table.tsx +222 -0
  96. package/src/Layout/Table/TableFooter.tsx +4 -0
  97. package/src/Layout/Table/TableHeader.tsx +4 -0
  98. package/src/Layout/Table/_index.ts +5 -0
  99. package/src/Layout/Table/tableUtils.ts +142 -0
  100. package/src/Layout/Table/types.ts +48 -0
  101. package/src/Layout/_index.ts +8 -0
  102. package/src/Misc/Cursor/Cursor.module.css +31 -0
  103. package/src/Misc/Cursor/Cursor.tsx +77 -0
  104. package/src/Misc/Logos.tsx +230 -0
  105. package/src/Misc/PoweredByBanner/PoweredByBanner.module.css +20 -0
  106. package/src/Misc/PoweredByBanner/PoweredByBanner.tsx +17 -0
  107. package/src/Misc/Ripple/Ripple.module.css +25 -0
  108. package/src/Misc/Ripple/Ripple.tsx +126 -0
  109. package/src/Misc/Spinner/Spinner.module.css +38 -0
  110. package/src/Misc/Spinner/Spinner.tsx +36 -0
  111. package/src/Misc/TransitionAnimation/TransitionAnimation.module.css +131 -0
  112. package/src/Misc/TransitionAnimation/TransitionAnimation.tsx +166 -0
  113. package/src/Misc/_index.ts +6 -0
  114. package/src/Navigation/Breadcrumbs/Breadcrumbs.module.css +22 -0
  115. package/src/Navigation/Breadcrumbs/Breadcrumbs.tsx +127 -0
  116. package/src/Navigation/Breadcrumbs/BreadcrumbsItem.tsx +31 -0
  117. package/src/Navigation/Breadcrumbs/_index.ts +3 -0
  118. package/src/Navigation/Breadcrumbs/useBreadcrumbs.tsx +74 -0
  119. package/src/Navigation/Pagination/Pagination.module.css +41 -0
  120. package/src/Navigation/Pagination/Pagination.tsx +187 -0
  121. package/src/Navigation/Pagination/PaginationItem.tsx +28 -0
  122. package/src/Navigation/Pagination/_index.ts +3 -0
  123. package/src/Navigation/Pagination/usePagination.tsx +65 -0
  124. package/src/Navigation/Tabs/Tab/Tab.module.css +43 -0
  125. package/src/Navigation/Tabs/Tab/Tab.tsx +155 -0
  126. package/src/Navigation/Tabs/Tabs.tsx +37 -0
  127. package/src/Navigation/Tabs/TabsBar/TabsBar.module.css +47 -0
  128. package/src/Navigation/Tabs/TabsBar/TabsBar.tsx +92 -0
  129. package/src/Navigation/Tabs/_index.ts +3 -0
  130. package/src/Navigation/_index.ts +3 -0
  131. package/src/Typography/ClampedText/ClampedText.module.css +5 -0
  132. package/src/Typography/ClampedText/ClampedText.tsx +77 -0
  133. package/src/Typography/CopyableText/CopyableText.module.css +21 -0
  134. package/src/Typography/CopyableText/CopyableText.tsx +120 -0
  135. package/src/Typography/PageTitle/PageTitle.module.css +47 -0
  136. package/src/Typography/PageTitle/PageTitle.tsx +35 -0
  137. package/src/Typography/_index.ts +3 -0
  138. package/src/declaration.d.ts +4 -0
  139. package/src/index.ts +8 -0
  140. package/tsconfig.json +32 -0
@@ -0,0 +1,229 @@
1
+ "use client";
2
+
3
+ import { cn } from "@studiocubics/utils";
4
+ import {
5
+ useEffect,
6
+ useState,
7
+ type ComponentProps,
8
+ type ReactNode,
9
+ } from "react";
10
+ import {
11
+ eventWithRipple,
12
+ useRipple,
13
+ type UseRippleProps,
14
+ } from "../../Misc/Ripple/Ripple";
15
+ import styles from "./PasswordInput.module.css";
16
+ import {
17
+ InputErrors,
18
+ type InputErrorsProps,
19
+ } from "../../Display/InputErrors/InputErrors";
20
+ import { Button } from "../Button/Button";
21
+ import { PasswordStrength } from "../../Display/PasswordStrength/PasswordStrength";
22
+ import {
23
+ ALL_PASSWORD_TESTS,
24
+ PASSWORD_REGEX,
25
+ usePasswordStrength,
26
+ type PasswordStrengthProps,
27
+ } from "../../Display/PasswordStrength/usePasswordStrength";
28
+
29
+ export interface PasswordInputProps
30
+ extends Omit<ComponentProps<"input">, "size"> {
31
+ startIcon?: ReactNode;
32
+ endIcon?: ReactNode;
33
+ label?: string;
34
+ error?: string | string[];
35
+ fullWidth?: boolean;
36
+ /**
37
+ * Use inputSize for <input size="10"/>
38
+ */
39
+ size?: "sm" | "md" | "lg";
40
+ inputSize?: ComponentProps<"input">["size"];
41
+ hideVisibilityToggle?: boolean;
42
+ disableStrengthMeter?: boolean;
43
+ disableEndIconGutters?: boolean;
44
+ disableStartIconGutters?: boolean;
45
+ slotProps?: {
46
+ ripple?: UseRippleProps;
47
+ startIcon?: ComponentProps<"span">;
48
+ endIcon?: ComponentProps<"span">;
49
+ root?: ComponentProps<"div">;
50
+ inputWrapper?: ComponentProps<"div">;
51
+ label?: ComponentProps<"label">;
52
+ error?: InputErrorsProps;
53
+ passwordStrength?: PasswordStrengthProps;
54
+ };
55
+ }
56
+
57
+ export function PasswordInput(props: PasswordInputProps) {
58
+ const {
59
+ startIcon,
60
+ endIcon,
61
+ label,
62
+ error,
63
+ fullWidth,
64
+ hideVisibilityToggle = false,
65
+ disableStrengthMeter = true,
66
+ disableEndIconGutters = false,
67
+ disableStartIconGutters = false,
68
+ size = "md",
69
+ inputSize,
70
+ onTouchStart,
71
+ onClick,
72
+ onBlur,
73
+ slotProps = {},
74
+ className: inputClass,
75
+ ...inputProps
76
+ } = props;
77
+ const { rippleElements, createRipple } = useRipple(slotProps.ripple);
78
+ const [isErrored, setIsErrored] = useState(error && !!error.length);
79
+ const [isVisible, setIsVisible] = useState(false);
80
+ const passwordStrength = usePasswordStrength({
81
+ password: typeof props.value == "string" ? props.value : undefined,
82
+ requiredTests:
83
+ slotProps.passwordStrength?.requiredTests ?? ALL_PASSWORD_TESTS,
84
+ disableStrengthMeter,
85
+ });
86
+
87
+ useEffect(() => {
88
+ setIsErrored(error && !!error.length);
89
+ }, [error]);
90
+
91
+ return (
92
+ <div
93
+ {...slotProps.root}
94
+ className={cn(
95
+ slotProps.root?.className,
96
+ styles.root,
97
+ styles[`size_${size}`],
98
+ fullWidth ? styles.fullWidth : undefined,
99
+ isErrored ? styles.errored : undefined,
100
+ )}
101
+ >
102
+ {label && (
103
+ <label
104
+ {...slotProps.label}
105
+ htmlFor={props.id || label}
106
+ className={cn(slotProps.label?.className, styles.label)}
107
+ >
108
+ {label}
109
+ </label>
110
+ )}
111
+ <div
112
+ {...slotProps.inputWrapper}
113
+ className={cn(slotProps.inputWrapper?.className, styles.inputWrapper)}
114
+ >
115
+ {startIcon && (
116
+ <span
117
+ {...slotProps.startIcon}
118
+ className={cn(
119
+ styles.iconContainer,
120
+ slotProps.startIcon?.className,
121
+ disableStartIconGutters ? styles.disableGutters : undefined,
122
+ )}
123
+ >
124
+ {startIcon}
125
+ </span>
126
+ )}
127
+ <input
128
+ type={isVisible ? "text" : "password"}
129
+ className={cn(inputClass, styles.input)}
130
+ onTouchStart={eventWithRipple(createRipple, onTouchStart)}
131
+ onClick={eventWithRipple(createRipple, onClick)}
132
+ onBlur={(e) => {
133
+ setIsErrored(false);
134
+ onBlur?.(e);
135
+ }}
136
+ pattern={
137
+ !disableStrengthMeter
138
+ ? passwordStrength?.testsPassed
139
+ .map((tp) => {
140
+ const regex = PASSWORD_REGEX[tp].ex;
141
+ return regex instanceof RegExp ? regex.source : regex;
142
+ })
143
+ .join("&")
144
+ : undefined
145
+ }
146
+ size={inputSize}
147
+ id={label}
148
+ {...inputProps}
149
+ />
150
+ {!hideVisibilityToggle && (
151
+ <Button
152
+ square
153
+ size="sm"
154
+ type="button"
155
+ onClick={() => setIsVisible(!isVisible)}
156
+ >
157
+ {isVisible ? (
158
+ <EyeIcon width={24} height={24} />
159
+ ) : (
160
+ <EyeOffIcon width={24} height={24} />
161
+ )}
162
+ </Button>
163
+ )}
164
+ {endIcon && (
165
+ <span
166
+ {...slotProps.endIcon}
167
+ className={cn(
168
+ styles.iconContainer,
169
+ slotProps.endIcon?.className,
170
+ disableEndIconGutters ? styles.disableGutters : undefined,
171
+ )}
172
+ >
173
+ {endIcon}
174
+ </span>
175
+ )}
176
+ {rippleElements}
177
+ </div>
178
+ {!disableStrengthMeter && (
179
+ <PasswordStrength
180
+ strength={passwordStrength?.strength}
181
+ {...slotProps.passwordStrength}
182
+ />
183
+ )}
184
+ {isErrored && (
185
+ <InputErrors
186
+ {...slotProps.error}
187
+ className={cn(slotProps.error?.className, styles.errorText)}
188
+ error={error}
189
+ />
190
+ )}
191
+ </div>
192
+ );
193
+ }
194
+ function EyeIcon(props: ComponentProps<"svg">) {
195
+ return (
196
+ <svg
197
+ {...props}
198
+ xmlns="http://www.w3.org/2000/svg"
199
+ viewBox="0 0 24 24"
200
+ fill="none"
201
+ stroke="currentColor"
202
+ strokeWidth="2.3"
203
+ strokeLinecap="round"
204
+ strokeLinejoin="round"
205
+ >
206
+ <path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
207
+ <circle cx="12" cy="12" r="3" />
208
+ </svg>
209
+ );
210
+ }
211
+ function EyeOffIcon(props: ComponentProps<"svg">) {
212
+ return (
213
+ <svg
214
+ {...props}
215
+ xmlns="http://www.w3.org/2000/svg"
216
+ viewBox="0 0 24 24"
217
+ fill="none"
218
+ stroke="currentColor"
219
+ strokeWidth="2.3"
220
+ strokeLinecap="round"
221
+ strokeLinejoin="round"
222
+ >
223
+ <path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
224
+ <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
225
+ <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
226
+ <path d="m2 2 20 20" />
227
+ </svg>
228
+ );
229
+ }
@@ -0,0 +1,138 @@
1
+ .root {
2
+ --input-border: var(--color-outline);
3
+ --input-color: var(--color-on-surface);
4
+ --input-fs: var(--fs-body2);
5
+ --input-background: var(--color-surface);
6
+ --input-focus-background: var(--color-background);
7
+
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--spacing-gap);
11
+ font-size: var(--input-fs);
12
+
13
+ &.fullWidth {
14
+ width: 100%;
15
+ }
16
+ &:has(.inputWrapper > .input:focus, .inputWrapper > .input:open) {
17
+ & > .inputWrapper {
18
+ outline: none;
19
+ background: var(--input-focus-background);
20
+ }
21
+ .label {
22
+ color: var(--input-color);
23
+ }
24
+ }
25
+ &:has(.inputWrapper > .input:disabled) {
26
+ opacity: 0.5;
27
+ }
28
+ }
29
+
30
+ .inputWrapper {
31
+ position: relative;
32
+ display: flex;
33
+ border-radius: var(--shape-br-sm);
34
+ outline: 1px solid var(--input-border);
35
+ background: var(--input-background);
36
+ }
37
+ .label {
38
+ font-size: 0.9em;
39
+ color: var(--color-on-background-faint);
40
+ margin-left: var(--spacing-gap);
41
+ width: fit-content;
42
+ cursor: pointer;
43
+ }
44
+ .input {
45
+ flex: 1 1 100%;
46
+ font-family: var(--font-p);
47
+ padding: var(--spacing-gap-2) calc(1.618 * var(--spacing-gap-2));
48
+ font-size: inherit;
49
+ border: none;
50
+ cursor: pointer;
51
+ background-color: var(--input-background);
52
+ color: var(--input-color);
53
+ border-radius: var(--shape-br-sm);
54
+ appearance: base-select;
55
+ transition: all var(--transition-time) var(--transition-tf);
56
+
57
+ &:focus,
58
+ &:open {
59
+ outline: none;
60
+ background-color: var(--input-focus-background);
61
+ }
62
+ & > option {
63
+ appearance: base-select;
64
+ cursor: pointer;
65
+ background-color: var(--input-background);
66
+ font-family: var(--font-p), sans-serif;
67
+ padding: var(--spacing-gap);
68
+ border-radius: var(--shape-br-sm);
69
+ transition: all var(--transition-time) var(--transition-tf);
70
+ &:hover {
71
+ background-color: var(--input-focus-background);
72
+ }
73
+ &:checked {
74
+ background-color: var(--color-primary-container);
75
+ color: var(--color-on-primary-container);
76
+ font-weight: bold;
77
+ transition: all var(--transition-time) var(--transition-tf);
78
+ &:hover {
79
+ background-color: var(--color-primary);
80
+ color: var(--color-on-primary);
81
+ }
82
+ }
83
+
84
+ &::checkmark {
85
+ display: none;
86
+ }
87
+ }
88
+ }
89
+ .input::picker(select) {
90
+ appearance: base-select;
91
+ border-radius: var(--shape-br-sm);
92
+ background-color: var(--input-background);
93
+ border: 1px solid var(--color-outline);
94
+ padding: var(--spacing-gap);
95
+ }
96
+ .input::picker-icon {
97
+ transition: rotate var(--transition-time) var(--transition-tf);
98
+ }
99
+ .input:open::picker-icon {
100
+ rotate: 180deg;
101
+
102
+ /* background-image: url('data:image/svg+xml;utf8,<svg fill="#FF0000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'); */
103
+ }
104
+ .iconContainer {
105
+ display: flex;
106
+ justify-content: center;
107
+ font-size: inherit;
108
+ align-items: center;
109
+ color: var(--color-on-background-faint);
110
+ /* Start Icon */
111
+ &:nth-of-type(1) {
112
+ margin-left: var(--spacing-gap-2);
113
+ &.disableGutters {
114
+ margin-left: 0;
115
+ }
116
+ }
117
+ /* End Icon */
118
+ &:nth-of-type(2) {
119
+ margin-right: var(--spacing-gap-2);
120
+ &.disableGutters {
121
+ margin-right: 0;
122
+ }
123
+ }
124
+ }
125
+
126
+ .errored {
127
+ --input-border: var(--color-error);
128
+ --input-color: var(--color-error);
129
+ & > .inputWrapper {
130
+ outline: 1px solid var(--input-border);
131
+ & > .iconContainer {
132
+ color: var(--input-color);
133
+ }
134
+ }
135
+ & > .label {
136
+ color: var(--input-color);
137
+ }
138
+ }
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ import { cn } from "@studiocubics/utils";
4
+ import {
5
+ useEffect,
6
+ useState,
7
+ type ComponentProps,
8
+ type ReactNode,
9
+ } from "react";
10
+ import styles from "./Select.module.css";
11
+ import {
12
+ eventWithRipple,
13
+ useRipple,
14
+ type UseRippleProps,
15
+ } from "../../Misc/Ripple/Ripple";
16
+ import {
17
+ InputErrors,
18
+ type InputErrorsProps,
19
+ } from "../../Display/InputErrors/InputErrors";
20
+
21
+ export interface SelectProps extends ComponentProps<"select"> {
22
+ startIcon?: ReactNode;
23
+ endIcon?: ReactNode;
24
+ label?: string;
25
+ error?: string | string[];
26
+ fullWidth?: boolean;
27
+ disableEndIconGutters?: boolean;
28
+ disableStartIconGutters?: boolean;
29
+ slotProps?: {
30
+ ripple?: UseRippleProps;
31
+ startIcon?: ComponentProps<"span">;
32
+ endIcon?: ComponentProps<"span">;
33
+ root?: ComponentProps<"div">;
34
+ inputWrapper?: ComponentProps<"div">;
35
+ label?: ComponentProps<"label">;
36
+ error?: InputErrorsProps;
37
+ };
38
+ }
39
+
40
+ export function Select(props: SelectProps) {
41
+ const {
42
+ startIcon,
43
+ endIcon,
44
+ label,
45
+ error,
46
+ fullWidth,
47
+ disableEndIconGutters = false,
48
+ disableStartIconGutters = false,
49
+ onTouchStart,
50
+ onClick,
51
+ onBlur,
52
+ slotProps: _slotProps,
53
+ className: inputClass,
54
+ ...inputProps
55
+ } = props;
56
+ const slotProps: NonNullable<SelectProps["slotProps"]> = _slotProps ?? {};
57
+ const { rippleElements, createRipple } = useRipple<HTMLSelectElement>(
58
+ slotProps.ripple,
59
+ );
60
+ const [isErrored, setIsErrored] = useState(error && !!error.length);
61
+
62
+ useEffect(() => {
63
+ setIsErrored(error && !!error.length);
64
+ }, [error]);
65
+
66
+ return (
67
+ <div
68
+ {...slotProps.root}
69
+ className={cn(
70
+ slotProps.root?.className,
71
+ styles.root,
72
+ fullWidth ? styles.fullWidth : undefined,
73
+ isErrored ? styles.errored : undefined,
74
+ )}
75
+ >
76
+ {label && (
77
+ <label
78
+ {...slotProps.label}
79
+ htmlFor={label}
80
+ className={cn(slotProps.label?.className, styles.label)}
81
+ >
82
+ {label}
83
+ </label>
84
+ )}
85
+ <div
86
+ {...slotProps.inputWrapper}
87
+ className={cn(slotProps.inputWrapper?.className, styles.inputWrapper)}
88
+ >
89
+ {startIcon && (
90
+ <span
91
+ {...slotProps.startIcon}
92
+ className={cn(
93
+ styles.iconContainer,
94
+ slotProps.startIcon?.className,
95
+ disableStartIconGutters ? styles.disableGutters : undefined,
96
+ )}
97
+ >
98
+ {startIcon}
99
+ </span>
100
+ )}
101
+ <select
102
+ className={cn(inputClass, styles.input)}
103
+ onTouchStart={eventWithRipple(createRipple, onTouchStart)}
104
+ //TODO Causes issue on Firefox where the ripple fires after closing the dropdown
105
+ onClick={eventWithRipple(createRipple, onClick)}
106
+ onBlur={(e) => {
107
+ setIsErrored(false);
108
+ onBlur?.(e);
109
+ }}
110
+ id={label}
111
+ {...inputProps}
112
+ />
113
+ {endIcon && (
114
+ <span
115
+ {...slotProps.endIcon}
116
+ className={cn(
117
+ styles.iconContainer,
118
+ slotProps.endIcon?.className,
119
+ disableEndIconGutters ? styles.disableGutters : undefined,
120
+ )}
121
+ >
122
+ {endIcon}
123
+ </span>
124
+ )}
125
+ {rippleElements}
126
+ </div>
127
+ {isErrored && (
128
+ <InputErrors
129
+ {...slotProps.error}
130
+ className={cn(slotProps.error?.className, styles.errorText)}
131
+ error={error}
132
+ />
133
+ )}
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,119 @@
1
+ .root {
2
+ --switch-track-width: 54px;
3
+ --switch-track-height: 30px;
4
+ --switch-thumb-width: 60%;
5
+ --switch-track-color: var(--color-background-alpha);
6
+ --switch-track-checked-color: var(--color-background);
7
+ --switch-thumb-color: var(--color-primary-container);
8
+ --switch-thumb-checked-color: var(--color-primary);
9
+ --switch-thumb-text-color: var(--color-on-primary-container);
10
+ --switch-thumb-checked-text-color: var(--color-on-primary);
11
+ display: flex;
12
+ gap: var(--spacing-gap-2);
13
+ align-items: center;
14
+ &:not(.disabled) .switch:hover .thumb {
15
+ scale: 1;
16
+ }
17
+ }
18
+ .inputWrapper {
19
+ width: var(--switch-track-width);
20
+ height: var(--switch-track-height);
21
+ position: relative;
22
+ overflow: hidden;
23
+ padding-inline: 0.2em;
24
+ }
25
+ .disabled {
26
+ --switch-thumb-color: var(--color-surface-alpha);
27
+ --switch-thumb-text-color: var(--color-on-surface);
28
+ --switch-thumb-checked-color: var(--color-background-faint);
29
+ --switch-thumb-checked-text-color: var(--color-on-background-faint);
30
+
31
+ & .switch,
32
+ & .label {
33
+ cursor: not-allowed;
34
+ }
35
+ }
36
+ .input {
37
+ /* visibility: hidden; */
38
+ position: absolute;
39
+ inset: 0;
40
+ opacity: 0;
41
+ /* left: 99999px; */
42
+ }
43
+ .inputWrapper:has(.input:focus) .switch {
44
+ border: var(--focus-ring);
45
+ background: var(--switch-track-checked-color);
46
+ }
47
+ .switch {
48
+ display: block;
49
+ width: 100%;
50
+ height: 100%;
51
+ cursor: pointer;
52
+ position: relative;
53
+ background-color: var(--switch-track-color);
54
+ border: 1px solid var(--color-outline);
55
+ border-radius: var(--shape-br-md);
56
+ &.checked {
57
+ background: var(--switch-track-checked-color);
58
+ & .thumb {
59
+ left: 50%;
60
+ color: var(--switch-thumb-checked-text-color);
61
+ animation: checkon var(--transition-time) var(--transition-tf) forwards;
62
+ background-color: var(--switch-thumb-checked-color);
63
+ }
64
+ }
65
+ }
66
+ .thumb {
67
+ display: flex;
68
+ justify-content: center;
69
+ align-items: center;
70
+ position: absolute;
71
+ left: 0;
72
+ top: 50%;
73
+ translate: 0% -50%;
74
+ width: var(--switch-thumb-width);
75
+ height: 100%;
76
+ background-color: var(--switch-thumb-color);
77
+ transition: all var(--transition-time) var(--transition-tf);
78
+ color: var(--switch-thumb-text-color);
79
+ border-radius: var(--shape-br-md);
80
+ scale: 80%;
81
+ animation: checkoff var(--transition-time) var(--transition-tf) forwards;
82
+ }
83
+ .label {
84
+ cursor: pointer;
85
+ }
86
+ @keyframes checkon {
87
+ 0% {
88
+ left: 0%;
89
+ width: var(--switch-thumb-width);
90
+ height: 100%;
91
+ }
92
+ 50% {
93
+ left: 0;
94
+ width: 100%;
95
+ height: 80%;
96
+ }
97
+ 100% {
98
+ left: calc(100% - var(--switch-thumb-width));
99
+ width: var(--switch-thumb-width);
100
+ height: 100%;
101
+ }
102
+ }
103
+ @keyframes checkoff {
104
+ 0% {
105
+ left: 50%;
106
+ width: var(--switch-thumb-width);
107
+ height: 100%;
108
+ }
109
+ 50% {
110
+ left: 0;
111
+ width: 100%;
112
+ height: 80%;
113
+ }
114
+ 100% {
115
+ left: 0;
116
+ width: var(--switch-thumb-width);
117
+ height: 100%;
118
+ }
119
+ }