@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.
- package/CHANGELOG.md +11 -0
- package/README.md +71 -0
- package/eslint.config.js +21 -0
- package/package.json +66 -0
- package/rollup.config.js +34 -0
- package/src/Cards/Card/Card.module.css +27 -0
- package/src/Cards/Card/Card.tsx +105 -0
- package/src/Cards/CollectionItemCard/CollectionItemCard.module.css +84 -0
- package/src/Cards/CollectionItemCard/CollectionItemCard.tsx +170 -0
- package/src/Cards/CollectionItemCard/CollectionItemCardActions.tsx +85 -0
- package/src/Cards/CollectionItemCard/_index.ts +2 -0
- package/src/Cards/GlassCard/GlassCard.module.css +71 -0
- package/src/Cards/GlassCard/GlassCard.tsx +80 -0
- package/src/Cards/_index.ts +3 -0
- package/src/Display/Accordion/Accordion.module.css +69 -0
- package/src/Display/Accordion/Accordion.tsx +61 -0
- package/src/Display/Accordion/AccordionItem.tsx +135 -0
- package/src/Display/Accordion/_index.ts +2 -0
- package/src/Display/Chip/Chip.module.css +64 -0
- package/src/Display/Chip/Chip.tsx +105 -0
- package/src/Display/IdentityDisplay/IdentityDisplay.module.css +95 -0
- package/src/Display/IdentityDisplay/IdentityDisplay.tsx +119 -0
- package/src/Display/InputErrors/InputErrors.module.css +6 -0
- package/src/Display/InputErrors/InputErrors.tsx +52 -0
- package/src/Display/Kbd/Kbd.module.css +29 -0
- package/src/Display/Kbd/Kbd.tsx +39 -0
- package/src/Display/Kbd/_index.ts +2 -0
- package/src/Display/Kbd/buttonList.tsx +246 -0
- package/src/Display/LabeledValue/LabeledValue.module.css +32 -0
- package/src/Display/LabeledValue/LabeledValue.tsx +20 -0
- package/src/Display/List/List.module.css +143 -0
- package/src/Display/List/List.tsx +298 -0
- package/src/Display/PasswordStrength/PasswordStrength.module.css +45 -0
- package/src/Display/PasswordStrength/PasswordStrength.tsx +41 -0
- package/src/Display/PasswordStrength/usePasswordStrength.tsx +77 -0
- package/src/Display/Skeleton/Skeleton.module.css +54 -0
- package/src/Display/Skeleton/Skeleton.tsx +28 -0
- package/src/Display/Toast/Toaster.tsx +58 -0
- package/src/Display/Toast/_index.ts +2 -0
- package/src/Display/Toast/toast.ts +44 -0
- package/src/Display/Tooltip/Tooltip.module.css +128 -0
- package/src/Display/Tooltip/Tooltip.tsx +93 -0
- package/src/Display/Tooltip/getArrowDirection.ts +55 -0
- package/src/Display/Tooltip/useTooltip.tsx +63 -0
- package/src/Display/_index.ts +12 -0
- package/src/Forms/ConfirmationForm/ConfirmationForm.module.css +23 -0
- package/src/Forms/ConfirmationForm/ConfirmationForm.tsx +60 -0
- package/src/Forms/_index.ts +1 -0
- package/src/Inputs/Button/Button.module.css +131 -0
- package/src/Inputs/Button/Button.tsx +178 -0
- package/src/Inputs/Checkbox/Checkbox.module.css +77 -0
- package/src/Inputs/Checkbox/Checkbox.tsx +191 -0
- package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.module.css +10 -0
- package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.tsx +83 -0
- package/src/Inputs/Checkbox/CheckboxSelectAll.tsx +34 -0
- package/src/Inputs/Checkbox/_index.ts +3 -0
- package/src/Inputs/PasswordInput/PasswordInput.module.css +111 -0
- package/src/Inputs/PasswordInput/PasswordInput.tsx +229 -0
- package/src/Inputs/Select/Select.module.css +138 -0
- package/src/Inputs/Select/Select.tsx +136 -0
- package/src/Inputs/Switch/Switch.module.css +119 -0
- package/src/Inputs/Switch/Switch.tsx +195 -0
- package/src/Inputs/TextAreaInput/TextAreaInput.module.css +65 -0
- package/src/Inputs/TextAreaInput/TextAreaInput.tsx +97 -0
- package/src/Inputs/TextInput/TextInput.module.css +112 -0
- package/src/Inputs/TextInput/TextInput.tsx +142 -0
- package/src/Inputs/ThemeToggle/ThemeToggleListItem.tsx +80 -0
- package/src/Inputs/ThemeToggle/_index.ts +1 -0
- package/src/Inputs/_index.ts +8 -0
- package/src/Layout/Dialog/Dialog.module.css +15 -0
- package/src/Layout/Dialog/Dialog.tsx +115 -0
- package/src/Layout/PageLayout/PageLayout.module.css +20 -0
- package/src/Layout/PageLayout/PageLayout.tsx +79 -0
- package/src/Layout/PageLayoutPagination/PageLayoutPagination.module.css +5 -0
- package/src/Layout/PageLayoutPagination/PageLayoutPagination.tsx +40 -0
- package/src/Layout/PageLayoutTabs/PageLayoutTabs.module.css +3 -0
- package/src/Layout/PageLayoutTabs/PageLayoutTabs.tsx +62 -0
- package/src/Layout/Popover/Popover.module.css +9 -0
- package/src/Layout/Popover/Popover.tsx +145 -0
- package/src/Layout/SectionWrapper/SectionWrapper.module.css +31 -0
- package/src/Layout/SectionWrapper/SectionWrapper.tsx +62 -0
- package/src/Layout/Sidebar/Sidebar.module.css +17 -0
- package/src/Layout/Sidebar/Sidebar.tsx +39 -0
- package/src/Layout/Sidebar/SidebarBody/SidebarBody.module.css +31 -0
- package/src/Layout/Sidebar/SidebarBody/SidebarBody.tsx +18 -0
- package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.module.css +20 -0
- package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.tsx +19 -0
- package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.module.css +35 -0
- package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.tsx +19 -0
- package/src/Layout/Sidebar/SidebarHeader/SidebarHeader.tsx +14 -0
- package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.module.css +12 -0
- package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.tsx +11 -0
- package/src/Layout/Sidebar/_index.ts +6 -0
- package/src/Layout/Table/Table.module.css +46 -0
- package/src/Layout/Table/Table.tsx +222 -0
- package/src/Layout/Table/TableFooter.tsx +4 -0
- package/src/Layout/Table/TableHeader.tsx +4 -0
- package/src/Layout/Table/_index.ts +5 -0
- package/src/Layout/Table/tableUtils.ts +142 -0
- package/src/Layout/Table/types.ts +48 -0
- package/src/Layout/_index.ts +8 -0
- package/src/Misc/Cursor/Cursor.module.css +31 -0
- package/src/Misc/Cursor/Cursor.tsx +77 -0
- package/src/Misc/Logos.tsx +230 -0
- package/src/Misc/PoweredByBanner/PoweredByBanner.module.css +20 -0
- package/src/Misc/PoweredByBanner/PoweredByBanner.tsx +17 -0
- package/src/Misc/Ripple/Ripple.module.css +25 -0
- package/src/Misc/Ripple/Ripple.tsx +126 -0
- package/src/Misc/Spinner/Spinner.module.css +38 -0
- package/src/Misc/Spinner/Spinner.tsx +36 -0
- package/src/Misc/TransitionAnimation/TransitionAnimation.module.css +131 -0
- package/src/Misc/TransitionAnimation/TransitionAnimation.tsx +166 -0
- package/src/Misc/_index.ts +6 -0
- package/src/Navigation/Breadcrumbs/Breadcrumbs.module.css +22 -0
- package/src/Navigation/Breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/Navigation/Breadcrumbs/BreadcrumbsItem.tsx +31 -0
- package/src/Navigation/Breadcrumbs/_index.ts +3 -0
- package/src/Navigation/Breadcrumbs/useBreadcrumbs.tsx +74 -0
- package/src/Navigation/Pagination/Pagination.module.css +41 -0
- package/src/Navigation/Pagination/Pagination.tsx +187 -0
- package/src/Navigation/Pagination/PaginationItem.tsx +28 -0
- package/src/Navigation/Pagination/_index.ts +3 -0
- package/src/Navigation/Pagination/usePagination.tsx +65 -0
- package/src/Navigation/Tabs/Tab/Tab.module.css +43 -0
- package/src/Navigation/Tabs/Tab/Tab.tsx +155 -0
- package/src/Navigation/Tabs/Tabs.tsx +37 -0
- package/src/Navigation/Tabs/TabsBar/TabsBar.module.css +47 -0
- package/src/Navigation/Tabs/TabsBar/TabsBar.tsx +92 -0
- package/src/Navigation/Tabs/_index.ts +3 -0
- package/src/Navigation/_index.ts +3 -0
- package/src/Typography/ClampedText/ClampedText.module.css +5 -0
- package/src/Typography/ClampedText/ClampedText.tsx +77 -0
- package/src/Typography/CopyableText/CopyableText.module.css +21 -0
- package/src/Typography/CopyableText/CopyableText.tsx +120 -0
- package/src/Typography/PageTitle/PageTitle.module.css +47 -0
- package/src/Typography/PageTitle/PageTitle.tsx +35 -0
- package/src/Typography/_index.ts +3 -0
- package/src/declaration.d.ts +4 -0
- package/src/index.ts +8 -0
- 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,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,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
|
+
}
|