@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,195 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ChangeEvent,
5
+ type ComponentProps,
6
+ type ReactNode,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import styles from "./Switch.module.css";
12
+ import { cn, mergeRefs } from "@studiocubics/utils";
13
+
14
+ /**
15
+ * Props for the Switch component
16
+ *
17
+ * @group switch
18
+ * @category inputs
19
+ */
20
+ export interface SwitchProps
21
+ extends Omit<ComponentProps<"input">, "defaultValue" | "value" | "onChange"> {
22
+ /**
23
+ * Default value of the switch in boolean
24
+ */
25
+ defaultValue?: boolean;
26
+ /**
27
+ * Value of the switch in boolean
28
+ */
29
+ value?: boolean | null;
30
+ /**
31
+ * Event handler for when the value of the switch changes
32
+ */
33
+ onChange?: (e: ChangeEvent<HTMLInputElement>, checked: boolean) => void;
34
+ /**
35
+ * Label for the switch
36
+ */
37
+ label?: string;
38
+ /**
39
+ * Icon to render in the thumb of the switch
40
+ */
41
+ icon?: ReactNode;
42
+ /**
43
+ * Switch slots props
44
+ */
45
+ slotProps?: {
46
+ inputWrapper?: ComponentProps<"div">;
47
+ label?: ComponentProps<"label">;
48
+ };
49
+ // TODO add size
50
+ // TODO add color
51
+ }
52
+
53
+ /**
54
+ * A switch can be used to show on/off state for form inputs, theme toggles etc.
55
+ *
56
+ * @group switch
57
+ * @category inputs
58
+ */
59
+ export function Switch(props: SwitchProps) {
60
+ const {
61
+ value,
62
+ defaultValue,
63
+ type = "checkbox",
64
+ onChange,
65
+ className,
66
+ ref,
67
+ label,
68
+ "aria-label": ariaLabel,
69
+ icon,
70
+ disabled,
71
+ slotProps = {},
72
+ ...rest
73
+ } = props;
74
+
75
+ const inputRef = useRef<HTMLInputElement>(null);
76
+ const thumbRef = useRef<HTMLSpanElement>(null);
77
+ const [checked, setChecked] = useState(value);
78
+ const [isDragging, setIsDragging] = useState(false);
79
+ const dragStartX = useRef(0);
80
+ const dragStartChecked = useRef(false);
81
+
82
+ useEffect(() => {
83
+ setChecked(value);
84
+ }, [value]);
85
+
86
+ const handleDragStart = (clientX: number) => {
87
+ if (disabled) return;
88
+ setIsDragging(true);
89
+ dragStartX.current = clientX;
90
+ dragStartChecked.current = checked ?? false;
91
+ };
92
+
93
+ const handleDragMove = (clientX: number) => {
94
+ if (!isDragging || disabled) return;
95
+
96
+ const delta = clientX - dragStartX.current;
97
+ const threshold = 10; // Minimum drag distance to trigger change
98
+
99
+ if (Math.abs(delta) > threshold) {
100
+ const newChecked = delta > 0;
101
+ if (newChecked !== checked) {
102
+ setChecked(newChecked);
103
+ if (inputRef.current && onChange) {
104
+ inputRef.current.checked = newChecked;
105
+ const event = new Event("change", { bubbles: true }) as any;
106
+ Object.defineProperty(event, "currentTarget", {
107
+ value: inputRef.current,
108
+ });
109
+ onChange(event, newChecked);
110
+ }
111
+ }
112
+ }
113
+ };
114
+
115
+ const handleDragEnd = () => {
116
+ setIsDragging(false);
117
+ };
118
+
119
+ useEffect(() => {
120
+ const handleMouseMove = (e: MouseEvent) => handleDragMove(e.clientX);
121
+ const handleTouchMove = (e: TouchEvent) =>
122
+ handleDragMove(e.touches[0].clientX);
123
+ const handleMouseUp = () => handleDragEnd();
124
+ const handleTouchEnd = () => handleDragEnd();
125
+
126
+ if (isDragging) {
127
+ document.addEventListener("mousemove", handleMouseMove);
128
+ document.addEventListener("mouseup", handleMouseUp);
129
+ document.addEventListener("touchmove", handleTouchMove);
130
+ document.addEventListener("touchend", handleTouchEnd);
131
+ }
132
+
133
+ return () => {
134
+ document.removeEventListener("mousemove", handleMouseMove);
135
+ document.removeEventListener("mouseup", handleMouseUp);
136
+ document.removeEventListener("touchmove", handleTouchMove);
137
+ document.removeEventListener("touchend", handleTouchEnd);
138
+ };
139
+ }, [isDragging, checked]);
140
+
141
+ return (
142
+ <div
143
+ className={cn(className, styles.root, disabled ? styles.disabled : "")}
144
+ >
145
+ <div
146
+ {...slotProps.inputWrapper}
147
+ className={cn(styles.inputWrapper, slotProps.inputWrapper?.className)}
148
+ >
149
+ <input
150
+ {...rest}
151
+ id={label}
152
+ type={type}
153
+ ref={mergeRefs(ref, inputRef)}
154
+ className={styles.input}
155
+ role="switch"
156
+ aria-checked={checked ?? false}
157
+ aria-label={ariaLabel || label}
158
+ disabled={disabled}
159
+ onChange={(e) => {
160
+ const c = e.currentTarget.checked;
161
+ setChecked(c);
162
+ if (onChange) onChange(e, c);
163
+ }}
164
+ />
165
+ <span
166
+ className={cn(
167
+ styles.switch,
168
+ checked ? styles.checked : "",
169
+ isDragging ? styles.dragging : ""
170
+ )}
171
+ onClick={() => !isDragging && inputRef.current?.click()}
172
+ aria-hidden="true"
173
+ >
174
+ <span
175
+ ref={thumbRef}
176
+ className={styles.thumb}
177
+ onMouseDown={(e) => handleDragStart(e.clientX)}
178
+ onTouchStart={(e) => handleDragStart(e.touches[0].clientX)}
179
+ >
180
+ {icon}
181
+ </span>
182
+ </span>
183
+ </div>
184
+ {label && (
185
+ <label
186
+ {...slotProps.label}
187
+ htmlFor={props.id || label}
188
+ className={cn(slotProps.label?.className, styles.label)}
189
+ >
190
+ {label}
191
+ </label>
192
+ )}
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,65 @@
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-background-faint);
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
+ &.fullWidth {
13
+ width: 100%;
14
+ }
15
+ &:has(.inputWrapper > .input:focus) {
16
+ & > .inputWrapper {
17
+ outline: none;
18
+ background: var(--input-focus-background);
19
+ }
20
+ .label {
21
+ color: var(--input-color);
22
+ }
23
+ }
24
+ &:has(.inputWrapper > .input:disabled) {
25
+ opacity: 0.5;
26
+ }
27
+ }
28
+ .inputWrapper {
29
+ position: relative;
30
+ display: flex;
31
+ border-radius: var(--shape-br-sm);
32
+ outline: 1px solid var(--input-border);
33
+ background: var(--input-background);
34
+ }
35
+ .label {
36
+ font-size: 0.9em;
37
+ color: var(--color-on-background-faint);
38
+ margin-left: var(--spacing-gap);
39
+ }
40
+ .input {
41
+ flex: 1 1 100%;
42
+ font-family: var(--font-p);
43
+ padding: calc(1.618 * var(--spacing-gap-2));
44
+ font-size: inherit;
45
+ border: none;
46
+ background: none;
47
+ color: var(--input-color);
48
+ &:focus {
49
+ outline: none;
50
+ }
51
+ &.disableResize {
52
+ resize: none;
53
+ }
54
+ }
55
+
56
+ .errored {
57
+ --input-border: var(--color-error);
58
+ --input-color: var(--color-error);
59
+ & > .inputWrapper {
60
+ outline: 1px solid var(--input-border);
61
+ }
62
+ & > .label {
63
+ color: var(--input-color);
64
+ }
65
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { cn } from "@studiocubics/utils";
4
+ import { useEffect, useState, type ComponentProps } from "react";
5
+ import {
6
+ eventWithRipple,
7
+ useRipple,
8
+ type UseRippleProps,
9
+ } from "../../Misc/Ripple/Ripple";
10
+ import styles from "./TextAreaInput.module.css";
11
+ import { InputErrors, type InputErrorsProps } from "../../Display/_index";
12
+
13
+ export interface TextAreaInputProps extends ComponentProps<"textarea"> {
14
+ label?: string;
15
+ error?: string | string[];
16
+ disableResize?: boolean;
17
+ fullWidth?: boolean;
18
+ slotProps?: {
19
+ ripple?: UseRippleProps;
20
+ root?: ComponentProps<"div">;
21
+ inputWrapper?: ComponentProps<"div">;
22
+ label?: ComponentProps<"label">;
23
+ error?: InputErrorsProps;
24
+ };
25
+ }
26
+
27
+ export function TextAreaInput(props: TextAreaInputProps) {
28
+ const {
29
+ label,
30
+ error,
31
+ fullWidth,
32
+ disableResize = false,
33
+ onTouchStart,
34
+ onClick,
35
+ onBlur,
36
+ slotProps = {},
37
+ className: inputClass,
38
+ ...inputProps
39
+ } = props;
40
+
41
+ const { rippleElements, createRipple } = useRipple(slotProps.ripple);
42
+ const [isErrored, setIsErrored] = useState(error && !!error.length);
43
+
44
+ useEffect(() => {
45
+ setIsErrored(error && !!error.length);
46
+ }, [error]);
47
+
48
+ return (
49
+ <div
50
+ {...slotProps.root}
51
+ className={cn(
52
+ slotProps.root?.className,
53
+ styles.root,
54
+ fullWidth ? styles.fullWidth : undefined,
55
+ isErrored ? styles.errored : undefined
56
+ )}
57
+ >
58
+ {label && (
59
+ <label
60
+ {...slotProps.label}
61
+ htmlFor={props.id || label}
62
+ className={cn(slotProps.label?.className, styles.label)}
63
+ >
64
+ {label}
65
+ </label>
66
+ )}
67
+ <div
68
+ {...slotProps.inputWrapper}
69
+ className={cn(slotProps.inputWrapper?.className, styles.inputWrapper)}
70
+ >
71
+ <textarea
72
+ className={cn(
73
+ inputClass,
74
+ styles.input,
75
+ disableResize ? styles.disableResize : undefined
76
+ )}
77
+ onTouchStart={eventWithRipple(createRipple, onTouchStart)}
78
+ onClick={eventWithRipple(createRipple, onClick)}
79
+ onBlur={(e) => {
80
+ setIsErrored(false);
81
+ onBlur?.(e);
82
+ }}
83
+ id={label}
84
+ {...inputProps}
85
+ />
86
+ {rippleElements}
87
+ </div>
88
+ {isErrored && (
89
+ <InputErrors
90
+ {...slotProps.error}
91
+ className={cn(slotProps.error?.className, styles.errorText)}
92
+ error={error}
93
+ />
94
+ )}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,112 @@
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-background-alpha);
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
+ &.fullWidth {
13
+ width: 100%;
14
+ }
15
+ &:has(.inputWrapper > .input:focus) {
16
+ & > .inputWrapper {
17
+ outline: none;
18
+ background: var(--input-focus-background);
19
+ }
20
+ .label {
21
+ color: var(--input-color);
22
+ }
23
+ }
24
+ &:has(.inputWrapper > .input:disabled) {
25
+ opacity: 0.5;
26
+ }
27
+ }
28
+
29
+ .inputWrapper {
30
+ position: relative;
31
+ display: flex;
32
+ border-radius: var(--shape-br-sm);
33
+ outline: 1px solid var(--input-border);
34
+ background: var(--input-background);
35
+ }
36
+ .label {
37
+ color: var(--color-on-background-faint);
38
+ margin-left: var(--spacing-gap);
39
+ width: max-content;
40
+ cursor: pointer;
41
+ }
42
+ .input {
43
+ flex: 1 1 100%;
44
+ font-family: var(--font-p);
45
+ font-size: inherit;
46
+ border: none;
47
+ background: none;
48
+ color: var(--input-color);
49
+ &:focus {
50
+ outline: none;
51
+ }
52
+ }
53
+ .iconContainer {
54
+ display: flex;
55
+ justify-content: center;
56
+ font-size: inherit;
57
+ align-items: center;
58
+ color: var(--color-on-background-faint);
59
+ /* Start Icon */
60
+ &:nth-of-type(1) {
61
+ margin-left: var(--spacing-gap-2);
62
+ &.disableGutters {
63
+ margin-left: 0;
64
+ }
65
+ }
66
+ /* End Icon */
67
+ &:nth-of-type(2) {
68
+ margin-right: var(--spacing-gap-2);
69
+ &.disableGutters {
70
+ margin-right: 0;
71
+ }
72
+ }
73
+ }
74
+
75
+ .errored {
76
+ --input-border: var(--color-error);
77
+ --input-color: var(--color-error);
78
+ & > .inputWrapper {
79
+ outline: 1px solid var(--input-border);
80
+ & > .iconContainer {
81
+ color: var(--input-color);
82
+ }
83
+ }
84
+ & > .label {
85
+ color: var(--input-color);
86
+ }
87
+ }
88
+
89
+ .size_sm {
90
+ .label {
91
+ font-size: 0.8em;
92
+ }
93
+ .input {
94
+ padding: var(--spacing-gap) calc(1.618 * var(--spacing-gap));
95
+ }
96
+ }
97
+ .size_md {
98
+ .label {
99
+ font-size: 0.9em;
100
+ }
101
+ .input {
102
+ padding: var(--spacing-gap-2) calc(1.618 * var(--spacing-gap-2));
103
+ }
104
+ }
105
+ .size_lg {
106
+ .label {
107
+ font-size: 1em;
108
+ }
109
+ .input {
110
+ padding: var(--spacing-gap-3) calc(1.618 * var(--spacing-gap-3));
111
+ }
112
+ }
@@ -0,0 +1,142 @@
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 "./TextInput.module.css";
16
+ import {
17
+ InputErrors,
18
+ type InputErrorsProps,
19
+ } from "../../Display/InputErrors/InputErrors";
20
+
21
+ export interface TextInputProps extends Omit<ComponentProps<"input">, "size"> {
22
+ startIcon?: ReactNode;
23
+ endIcon?: ReactNode;
24
+ label?: string;
25
+ error?: string | string[];
26
+ fullWidth?: boolean;
27
+ /**
28
+ * Use inputSize for <input size="10"/>
29
+ */
30
+ size?: "sm" | "md" | "lg";
31
+ inputSize?: ComponentProps<"input">["size"];
32
+ disableEndIconGutters?: boolean;
33
+ disableStartIconGutters?: boolean;
34
+ slotProps?: {
35
+ ripple?: UseRippleProps;
36
+ startIcon?: ComponentProps<"span">;
37
+ endIcon?: ComponentProps<"span">;
38
+ root?: ComponentProps<"div">;
39
+ inputWrapper?: ComponentProps<"div">;
40
+ label?: ComponentProps<"label">;
41
+ error?: InputErrorsProps;
42
+ };
43
+ }
44
+
45
+ export function TextInput(props: TextInputProps) {
46
+ const {
47
+ startIcon,
48
+ endIcon,
49
+ label,
50
+ error,
51
+ fullWidth,
52
+ disableEndIconGutters = false,
53
+ disableStartIconGutters = false,
54
+ size = "md",
55
+ inputSize,
56
+ onTouchStart,
57
+ onClick,
58
+ onBlur,
59
+ slotProps = {},
60
+ className: inputClass,
61
+ ...inputProps
62
+ } = props;
63
+ const { rippleElements, createRipple } = useRipple(slotProps.ripple);
64
+ const [isErrored, setIsErrored] = useState(error && !!error.length);
65
+
66
+ useEffect(() => {
67
+ setIsErrored(error && !!error.length);
68
+ }, [error]);
69
+
70
+ return (
71
+ <div
72
+ {...slotProps.root}
73
+ className={cn(
74
+ slotProps.root?.className,
75
+ styles.root,
76
+ styles[`size_${size}`],
77
+ fullWidth ? styles.fullWidth : undefined,
78
+ isErrored ? styles.errored : undefined
79
+ )}
80
+ >
81
+ {label && (
82
+ <label
83
+ {...slotProps.label}
84
+ htmlFor={props.id || label}
85
+ className={cn(slotProps.label?.className, styles.label)}
86
+ >
87
+ {label}
88
+ </label>
89
+ )}
90
+ <div
91
+ {...slotProps.inputWrapper}
92
+ className={cn(slotProps.inputWrapper?.className, styles.inputWrapper)}
93
+ >
94
+ {startIcon && (
95
+ <span
96
+ {...slotProps.startIcon}
97
+ className={cn(
98
+ styles.iconContainer,
99
+ slotProps.startIcon?.className,
100
+ disableStartIconGutters ? styles.disableGutters : undefined
101
+ )}
102
+ >
103
+ {startIcon}
104
+ </span>
105
+ )}
106
+ <input
107
+ type="text"
108
+ className={cn(inputClass, styles.input)}
109
+ onTouchStart={eventWithRipple(createRipple, onTouchStart)}
110
+ onClick={eventWithRipple(createRipple, onClick)}
111
+ onBlur={(e) => {
112
+ setIsErrored(false);
113
+ onBlur?.(e);
114
+ }}
115
+ size={inputSize}
116
+ id={label}
117
+ {...inputProps}
118
+ />
119
+ {endIcon && (
120
+ <span
121
+ {...slotProps.endIcon}
122
+ className={cn(
123
+ styles.iconContainer,
124
+ slotProps.endIcon?.className,
125
+ disableEndIconGutters ? styles.disableGutters : undefined
126
+ )}
127
+ >
128
+ {endIcon}
129
+ </span>
130
+ )}
131
+ {rippleElements}
132
+ </div>
133
+ {isErrored && (
134
+ <InputErrors
135
+ {...slotProps.error}
136
+ className={cn(slotProps.error?.className, styles.errorText)}
137
+ error={error}
138
+ />
139
+ )}
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,80 @@
1
+ import { useAnchorElement } from "@studiocubics/hooks";
2
+ import {
3
+ List,
4
+ type ListItemProps,
5
+ ListItem,
6
+ } from "../../Display/List/List";
7
+ import { type PopoverProps, Popover } from "../../Layout/Popover/Popover";
8
+ import type { MouseEvent, ReactNode } from "react";
9
+
10
+ export type ThemeObject = Record<
11
+ "dark" | "light" | "system",
12
+ {
13
+ text?: string;
14
+ icon?: ReactNode;
15
+ onClick?: (e: MouseEvent<HTMLElement>) => void;
16
+ }
17
+ >;
18
+
19
+ export interface ThemeToggleListItemProps
20
+ extends Omit<ListItemProps, "slotProps"> {
21
+ themeObject?: Partial<ThemeObject>;
22
+ currentTheme: keyof ThemeObject;
23
+ slotProps?: ListItemProps["slotProps"] & {
24
+ popover?: PopoverProps;
25
+ };
26
+ }
27
+
28
+ export function ThemeToggleListItem(props: ThemeToggleListItemProps) {
29
+ const { currentTheme, themeObject = {}, slotProps = {}, ...rest } = props;
30
+ const { open, anchorEl, handleClick, handleClose } = useAnchorElement();
31
+ const { popover = {}, ...listItemSlotProps } = slotProps;
32
+ const sanitisedThemeObject: ThemeObject = {
33
+ dark: {
34
+ text: "Dark Mode",
35
+ icon: <>🌚</>,
36
+ onClick: (e) => {
37
+ if (themeObject.dark?.onClick) themeObject.dark?.onClick(e);
38
+ if (open) handleClose();
39
+ else handleClick(e);
40
+ },
41
+ ...themeObject.dark,
42
+ },
43
+ light: { text: "Light Mode", icon: <>🌞</>, ...themeObject.light },
44
+ system: { text: "System Color", icon: <>💻</>, ...themeObject.system },
45
+ };
46
+
47
+ return (
48
+ <>
49
+ <ListItem
50
+ slotProps={listItemSlotProps}
51
+ startIcon={sanitisedThemeObject[currentTheme].icon}
52
+ onClick={open ? handleClose : handleClick}
53
+ {...rest}
54
+ >
55
+ {sanitisedThemeObject[currentTheme].text}
56
+ </ListItem>
57
+ <Popover
58
+ open={open}
59
+ anchorEl={anchorEl}
60
+ onClose={handleClose}
61
+ anchorPosition={{ vertical: "top", horizontal: "right" }}
62
+ transformOrigin={{ vertical: "bottom", horizontal: "left" }}
63
+ {...slotProps.popover}
64
+ >
65
+ <List className={"surfaceContainer"}>
66
+ {Object.entries(sanitisedThemeObject).map(([key, value]) => (
67
+ <ListItem
68
+ selected={currentTheme == key}
69
+ startIcon={value.icon}
70
+ key={key}
71
+ onClick={value.onClick}
72
+ >
73
+ {value.text}
74
+ </ListItem>
75
+ ))}
76
+ </List>
77
+ </Popover>
78
+ </>
79
+ );
80
+ }
@@ -0,0 +1 @@
1
+ export * from "./ThemeToggleListItem"
@@ -0,0 +1,8 @@
1
+ export * from "./Button/Button";
2
+ export * from "./Checkbox/_index";
3
+ export * from "./PasswordInput/PasswordInput";
4
+ export * from "./Select/Select";
5
+ export * from "./Switch/Switch";
6
+ export * from "./TextInput/TextInput";
7
+ export * from "./TextAreaInput/TextAreaInput";
8
+ export * from "./ThemeToggle/_index";