@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,178 @@
1
+ /**
2
+ * Button is love button is life
3
+ */
4
+ "use client";
5
+
6
+ import type {
7
+ PolymorphicComponentProps,
8
+ PolymorphicComponentType,
9
+ } from "@studiocubics/types";
10
+ import {
11
+ type ComponentProps,
12
+ type CSSProperties,
13
+ type ElementType,
14
+ type ReactNode,
15
+ } from "react";
16
+ import {
17
+ eventWithRipple,
18
+ useRipple,
19
+ type UseRippleProps,
20
+ } from "../../Misc/Ripple/Ripple";
21
+ import { cn } from "@studiocubics/utils";
22
+ import styles from "./Button.module.css";
23
+
24
+ /**
25
+ * Props specific to the Button component.
26
+ *
27
+ * These extend the intrinsic element props of whatever element is passed via `as`.
28
+ * @group button
29
+ * @category inputs
30
+ */
31
+ export interface ButtonBaseProps {
32
+ /** Renders an icon at the beginning of the button. */
33
+ startIcon?: ReactNode;
34
+
35
+ /** Renders an icon at the end of the button. */
36
+ endIcon?: ReactNode;
37
+
38
+ /** Marks the button as selected. */
39
+ selected?: boolean;
40
+
41
+ /** Visual style variant. */
42
+ variant?: "contained" | "text" | "outlined";
43
+
44
+ /** Expands width to 100%. */
45
+ fullWidth?: boolean;
46
+
47
+ /** Force a 1:1 aspect ratio. */
48
+ square?: boolean;
49
+
50
+ /** Button size. */
51
+ size?: "sm" | "md" | "lg";
52
+
53
+ /** Overrides the CSS `position` property of the root element. */
54
+ position?: CSSProperties["position"];
55
+
56
+ color?: "primary" | "secondary" | "error";
57
+ /**
58
+ * Slot props for customizing internal elements.
59
+ */
60
+ slotProps?: {
61
+ /** Props for the ripple effect. */
62
+ ripple?: UseRippleProps;
63
+
64
+ /** Wrapper span for the start icon. */
65
+ startIcon?: ComponentProps<"span">;
66
+
67
+ /** Wrapper span for the end icon. */
68
+ endIcon?: ComponentProps<"span">;
69
+ };
70
+ }
71
+ const defaultElement = "button";
72
+ type DefaultElement = typeof defaultElement;
73
+ /**
74
+ * Polymorphic props for the Button component.
75
+ *
76
+ * `C` defines the element type rendered by the component (e.g. `"button"`, `"a"`, `"div"`).
77
+ * All intrinsic props for `C` are supported unless overridden by `ButtonBaseProps`.
78
+ *
79
+ * @group button
80
+ * @category inputs
81
+ */
82
+ export type ButtonProps<C extends ElementType = DefaultElement> =
83
+ PolymorphicComponentProps<C, ButtonBaseProps>;
84
+
85
+ /**
86
+ * Base implementation for the Button component.
87
+ *
88
+ * This is a polymorphic component that defaults to rendering a `<button>`.
89
+ * Use the `as` prop to change the underlying element.
90
+ *
91
+ * @typeParam C - The intrinsic or custom element type to render.
92
+ *
93
+ * @group button
94
+ * @category inputs
95
+ */
96
+ export function ButtonBase<C extends ElementType = DefaultElement>(
97
+ props: ButtonProps<C>
98
+ ) {
99
+ const {
100
+ as,
101
+ className,
102
+ children,
103
+ variant = "text",
104
+ size = "md",
105
+ startIcon,
106
+ color,
107
+ endIcon,
108
+ square,
109
+ fullWidth = false,
110
+ onTouchStart,
111
+ onClick,
112
+ position,
113
+ slotProps: _slotProps,
114
+ ...restProps
115
+ } = props as ButtonProps<DefaultElement>;
116
+ const slotProps: NonNullable<ButtonBaseProps["slotProps"]> = _slotProps ?? {};
117
+ const Component = (as || defaultElement) as ElementType;
118
+ const { rippleElements, createRipple } = useRipple(slotProps.ripple);
119
+
120
+ const componentProps = {
121
+ className: cn(
122
+ className,
123
+ styles.root,
124
+ square ? styles.square : "",
125
+ fullWidth ? styles.fullWidth : "",
126
+ position ? styles[`position_${position}`] : "",
127
+ styles[`size_${size}`],
128
+ styles[variant]
129
+ ),
130
+ onTouchStart: eventWithRipple(createRipple, onTouchStart),
131
+ onClick: eventWithRipple(createRipple, onClick),
132
+ "data-color": color,
133
+ ...restProps,
134
+ };
135
+
136
+ return (
137
+ <Component {...componentProps}>
138
+ {rippleElements}
139
+ {startIcon && (
140
+ <span
141
+ {...slotProps.startIcon}
142
+ className={cn(styles.iconContainer, slotProps.startIcon?.className)}
143
+ >
144
+ {startIcon}
145
+ </span>
146
+ )}
147
+ {children}
148
+ {endIcon && (
149
+ <span
150
+ {...slotProps.endIcon}
151
+ className={cn(styles.iconContainer, slotProps.endIcon?.className)}
152
+ >
153
+ {endIcon}
154
+ </span>
155
+ )}
156
+ </Component>
157
+ );
158
+ }
159
+ ButtonBase.displayName = "Button";
160
+
161
+ /**
162
+ * A polymorphic button component.
163
+ *
164
+ * By default it renders a `<button>`, but any element can be used via the `as` prop:
165
+ *
166
+ * ```tsx
167
+ * <Button as="a" href="/docs">Read docs</Button>
168
+ * ```
169
+ *
170
+ * Supports variants, sizes, icons, and ripple effects.
171
+ *
172
+ * @group button
173
+ * @category inputs
174
+ */
175
+ export const Button = ButtonBase as PolymorphicComponentType<
176
+ ButtonBaseProps,
177
+ DefaultElement
178
+ >;
@@ -0,0 +1,77 @@
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
+ --input-checked-color: var(--color-surface);
8
+ --input-checked-background: var(--color-on-surface);
9
+ --input-indeterminate-color: var(--color-surface);
10
+ --input-indeterminate-background: var(--color-on-surface);
11
+ display: flex;
12
+ align-items: center;
13
+ gap: var(--spacing-gap-2);
14
+ }
15
+
16
+ .inputWrapper {
17
+ position: relative;
18
+ height: fit-content;
19
+ display: flex;
20
+ border-radius: var(--shape-br-xs);
21
+ overflow: hidden;
22
+
23
+ & > input[type="checkbox"] {
24
+ font: inherit;
25
+ color: var(--input-color);
26
+ background: var(--input-background);
27
+ width: var(--spacing-gap-4);
28
+ height: var(--spacing-gap-4);
29
+ border: 2.3px solid var(--input-border);
30
+ border-radius: inherit;
31
+ cursor: pointer;
32
+ transition: all var(--transition-time) var(--transition-tf);
33
+ }
34
+
35
+ &:has(input[type="checkbox"]:checked) {
36
+ --input-color: var(--input-checked-color);
37
+ --input-background: var(--input-checked-background);
38
+ }
39
+
40
+ &:has(input[type="checkbox"]:indeterminate) {
41
+ --input-color: var(--input-indeterminate-color);
42
+ --input-background: var(--input-indeterminate-background);
43
+ }
44
+ }
45
+ .inputWrapper > span {
46
+ position: absolute;
47
+ inset: 0;
48
+ color: var(--input-color);
49
+ pointer-events: none;
50
+ height: 100%;
51
+ & > svg {
52
+ position: absolute;
53
+ }
54
+ }
55
+ .show {
56
+ display: block;
57
+ /* opacity: 1; */
58
+ }
59
+ .hide {
60
+ display: none;
61
+ }
62
+ .show path {
63
+ stroke-dasharray: 45; /* path length, adjust per path */
64
+ stroke-dashoffset: 45; /* start hidden */
65
+ animation: draw-check var(--transition-time) var(--transition-tf) forwards;
66
+ }
67
+ .hide path {
68
+ animation: draw-check var(--transition-time) var(--transition-tf) reverse;
69
+ }
70
+ @keyframes draw-check {
71
+ to {
72
+ stroke-dashoffset: 0; /* reveal path */
73
+ }
74
+ }
75
+ .label {
76
+ cursor: pointer;
77
+ }
@@ -0,0 +1,191 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ComponentProps,
5
+ type ReactElement,
6
+ useContext,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { CheckboxGroupContext } from "./CheckboxGroup/CheckboxGroup";
12
+
13
+ import { cn } from "@studiocubics/utils";
14
+ import styles from "./Checkbox.module.css";
15
+ import {
16
+ eventWithRipple,
17
+ useRipple,
18
+ type UseRippleProps,
19
+ } from "../../Misc/Ripple/Ripple";
20
+
21
+ interface CheckboxProps
22
+ extends Omit<ComponentProps<"input">, "onChange" | "type"> {
23
+ label?: string;
24
+ checked?: boolean;
25
+ indeterminate?: boolean;
26
+ onChange?: (
27
+ event: React.ChangeEvent<HTMLInputElement>,
28
+ checked: boolean,
29
+ ) => void;
30
+ checkedIcon?: ReactElement<
31
+ Required<Pick<CheckboxProps, "checked">> & ComponentProps<"svg">
32
+ >;
33
+ indeterminateIcon?: ReactElement<
34
+ Required<Pick<CheckboxProps, "indeterminate">> & ComponentProps<"svg">
35
+ >;
36
+ slotProps?: {
37
+ ripple?: UseRippleProps;
38
+ startIcon?: ComponentProps<"span">;
39
+ endIcon?: ComponentProps<"span">;
40
+ root?: ComponentProps<"div">;
41
+ inputWrapper?: ComponentProps<"div">;
42
+ label?: ComponentProps<"label">;
43
+ error?: ComponentProps<"p">;
44
+ };
45
+ /** INTERNAL: prevent group registration */
46
+ skipGroup?: boolean;
47
+ // TODO add size
48
+ // TODO add color
49
+ }
50
+
51
+ export function Checkbox(props: CheckboxProps) {
52
+ const {
53
+ label,
54
+ checked,
55
+ indeterminate = false,
56
+ skipGroup,
57
+ checkedIcon,
58
+ indeterminateIcon,
59
+ slotProps = {},
60
+ "aria-label": ariaLabel,
61
+ onChange,
62
+ onTouchStart,
63
+ onClick,
64
+ ...inputProps
65
+ } = props;
66
+ const group = useContext(CheckboxGroupContext);
67
+ const [index, setIndex] = useState<number | null>(null);
68
+ const [selfChecked, setSelfChecked] = useState(false);
69
+
70
+ const inputRef = useRef<HTMLInputElement>(null);
71
+ const isRegistered = useRef(false);
72
+
73
+ const { rippleElements, createRipple } = useRipple();
74
+
75
+ const isChecked =
76
+ group && !skipGroup && index !== null
77
+ ? group.values[index]
78
+ : (checked ?? selfChecked);
79
+
80
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
81
+ const next = e.target.checked;
82
+ if (group && !skipGroup && index !== null) {
83
+ group.update(index, next);
84
+ } else {
85
+ setSelfChecked(next);
86
+ }
87
+ onChange?.(e, next);
88
+ };
89
+ // Register with group if present
90
+ useEffect(() => {
91
+ if (!group || skipGroup || isRegistered.current) return;
92
+ isRegistered.current = true;
93
+ setIndex(group.register());
94
+ }, [group, skipGroup, index]);
95
+
96
+ // Set indeterminate state
97
+ useEffect(() => {
98
+ if (inputRef.current) {
99
+ inputRef.current.indeterminate = indeterminate;
100
+ }
101
+ }, [indeterminate]);
102
+
103
+ return (
104
+ <div
105
+ {...slotProps.root}
106
+ className={cn(styles.root, slotProps.root?.className)}
107
+ >
108
+ <div
109
+ {...slotProps.inputWrapper}
110
+ className={cn(styles.inputWrapper, slotProps.inputWrapper?.className)}
111
+ >
112
+ {rippleElements}
113
+ <input
114
+ ref={inputRef}
115
+ type="checkbox"
116
+ checked={isChecked}
117
+ aria-checked={isChecked ?? false}
118
+ aria-label={ariaLabel || label}
119
+ onChange={handleChange}
120
+ id={label}
121
+ onTouchStart={eventWithRipple(createRipple, onTouchStart)}
122
+ onClick={eventWithRipple(createRipple, onClick)}
123
+ {...inputProps}
124
+ />
125
+ <span>
126
+ {checkedIcon ?? <CheckboxCheckedIcon checked={isChecked} />}
127
+ {indeterminateIcon ?? (
128
+ <CheckboxIndeterminateIcon indeterminate={indeterminate} />
129
+ )}
130
+ </span>
131
+ </div>
132
+ {label && (
133
+ <label
134
+ {...slotProps.label}
135
+ htmlFor={label}
136
+ className={cn(slotProps.label?.className, styles.label)}
137
+ >
138
+ {label}
139
+ </label>
140
+ )}
141
+ </div>
142
+ );
143
+ }
144
+ function CheckboxCheckedIcon(
145
+ props: Required<Pick<CheckboxProps, "checked">> & ComponentProps<"svg">,
146
+ ) {
147
+ const { checked, className, ...rest } = props;
148
+ return (
149
+ <svg
150
+ {...rest}
151
+ xmlns="http://www.w3.org/2000/svg"
152
+ viewBox="0 0 24 24"
153
+ fill="none"
154
+ stroke="currentColor"
155
+ strokeWidth="2.3"
156
+ strokeLinecap="round"
157
+ strokeLinejoin="round"
158
+ className={cn(
159
+ checked ? styles.show : styles.hide,
160
+ styles.checked,
161
+ className,
162
+ )}
163
+ >
164
+ <path d="M4 12 9 17l11 -11" />
165
+ </svg>
166
+ );
167
+ }
168
+
169
+ function CheckboxIndeterminateIcon(
170
+ props: Required<Pick<CheckboxProps, "indeterminate">> & ComponentProps<"svg">,
171
+ ) {
172
+ const { indeterminate, className, ...rest } = props;
173
+ return (
174
+ <svg
175
+ {...rest}
176
+ viewBox="0 0 24 24"
177
+ xmlns="http://www.w3.org/2000/svg"
178
+ className={cn(
179
+ indeterminate ? styles.show : styles.hide,
180
+ styles.indeterminate,
181
+ className,
182
+ )}
183
+ stroke="currentColor"
184
+ strokeWidth="2.3"
185
+ strokeLinecap="round"
186
+ strokeLinejoin="round"
187
+ >
188
+ <path d="M5 12h14" />
189
+ </svg>
190
+ );
191
+ }
@@ -0,0 +1,10 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--spacing-gap-2);
5
+ padding-top: var(--spacing-gap-3);
6
+ }
7
+ .legend {
8
+ font-weight: bold;
9
+ font-family: var(--font-h);
10
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ type ReactNode,
6
+ useCallback,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import styles from "./CheckboxGroup.module.css";
12
+
13
+ interface CheckboxGroupContextProps {
14
+ values: boolean[];
15
+ register: () => number;
16
+ update: (index: number, checked: boolean) => void;
17
+ setAll: (checked: boolean) => void;
18
+ }
19
+
20
+ interface CheckboxGroupProps {
21
+ children: ReactNode;
22
+ onChange?: (checked: boolean[]) => void;
23
+ label?: string;
24
+ }
25
+
26
+ export const CheckboxGroupContext =
27
+ createContext<CheckboxGroupContextProps | null>(null);
28
+
29
+ export function CheckboxGroup({
30
+ children,
31
+ onChange,
32
+ label,
33
+ }: CheckboxGroupProps) {
34
+ const [values, setValues] = useState<boolean[]>([]);
35
+ const indexRef = useRef(-1);
36
+
37
+ const register = useCallback(() => {
38
+ const index = indexRef.current++;
39
+ setValues((prev) => [...prev, false]);
40
+ return index;
41
+ }, []);
42
+
43
+ const update = useCallback(
44
+ (index: number, checked: boolean) => {
45
+ setValues((prev) => {
46
+ const next = [...prev];
47
+ next[index] = checked;
48
+ onChange?.(next);
49
+ return next;
50
+ });
51
+ },
52
+ [onChange]
53
+ );
54
+
55
+ const setAll = useCallback(
56
+ (checked: boolean) => {
57
+ setValues((prev) => {
58
+ const next = prev.map(() => checked);
59
+ onChange?.(next);
60
+ return next;
61
+ });
62
+ },
63
+ [onChange]
64
+ );
65
+
66
+ const context = useMemo(
67
+ () => ({ values, register, update, setAll }),
68
+ [values, register, update, setAll]
69
+ );
70
+
71
+ return (
72
+ <fieldset className={styles.root}>
73
+ {label && (
74
+ <legend className={styles.legend} id={label}>
75
+ {label}
76
+ </legend>
77
+ )}
78
+ <CheckboxGroupContext.Provider value={context}>
79
+ {children}
80
+ </CheckboxGroupContext.Provider>
81
+ </fieldset>
82
+ );
83
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { useContext } from "react";
4
+ import { Checkbox } from "./Checkbox";
5
+ import { CheckboxGroupContext } from "./CheckboxGroup/CheckboxGroup";
6
+
7
+ interface CheckboxSelectAllProps {
8
+ label: string;
9
+ }
10
+
11
+ export function CheckboxSelectAll({ label }: CheckboxSelectAllProps) {
12
+ const group = useContext(CheckboxGroupContext);
13
+
14
+ if (!group) {
15
+ throw new Error(
16
+ "<CheckboxSelectAll/> must be used inside <CheckboxGroup/>",
17
+ );
18
+ }
19
+
20
+ const { values, setAll } = group;
21
+
22
+ const allChecked = values.length > 0 && values.every(Boolean);
23
+ const someChecked = values.some(Boolean);
24
+
25
+ return (
26
+ <Checkbox
27
+ label={label}
28
+ checked={allChecked}
29
+ indeterminate={!allChecked && someChecked}
30
+ onChange={(_, checked) => setAll(checked)}
31
+ skipGroup
32
+ />
33
+ );
34
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./Checkbox";
2
+ export * from "./CheckboxSelectAll";
3
+ export * from "./CheckboxGroup/CheckboxGroup";
@@ -0,0 +1,111 @@
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
+ .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
+ color: var(--color-on-background-faint);
37
+ margin-left: var(--spacing-gap);
38
+ width: max-content;
39
+ cursor: pointer;
40
+ }
41
+ .input {
42
+ flex: 1 1 100%;
43
+ font-family: var(--font-p);
44
+ font-size: inherit;
45
+ border: none;
46
+ background: none;
47
+ color: var(--input-color);
48
+ &:focus {
49
+ outline: none;
50
+ }
51
+ }
52
+ .iconContainer {
53
+ display: flex;
54
+ justify-content: center;
55
+ font-size: inherit;
56
+ align-items: center;
57
+ color: var(--color-on-background-faint);
58
+ /* Start Icon */
59
+ &:nth-of-type(1) {
60
+ margin-left: var(--spacing-gap-2);
61
+ &.disableGutters {
62
+ margin-left: 0;
63
+ }
64
+ }
65
+ /* End Icon */
66
+ &:nth-of-type(2) {
67
+ margin-right: var(--spacing-gap-2);
68
+ &.disableGutters {
69
+ margin-right: 0;
70
+ }
71
+ }
72
+ }
73
+
74
+ .errored {
75
+ --input-border: var(--color-error);
76
+ --input-color: var(--color-error);
77
+ & > .inputWrapper {
78
+ outline: 1px solid var(--input-border);
79
+ & > .iconContainer {
80
+ color: var(--input-color);
81
+ }
82
+ }
83
+ & > .label {
84
+ color: var(--input-color);
85
+ }
86
+ }
87
+
88
+ .size_sm {
89
+ .label {
90
+ font-size: 0.8em;
91
+ }
92
+ .input {
93
+ padding: var(--spacing-gap) calc(1.618 * var(--spacing-gap));
94
+ }
95
+ }
96
+ .size_md {
97
+ .label {
98
+ font-size: 0.9em;
99
+ }
100
+ .input {
101
+ padding: var(--spacing-gap-2) calc(1.618 * var(--spacing-gap-2));
102
+ }
103
+ }
104
+ .size_lg {
105
+ .label {
106
+ font-size: 1em;
107
+ }
108
+ .input {
109
+ padding: var(--spacing-gap-3) calc(1.618 * var(--spacing-gap-3));
110
+ }
111
+ }