@gbgr/react 0.1.0
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/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/accordion/Accordion.d.ts +27 -0
- package/dist/accordion/Accordion.d.ts.map +1 -0
- package/dist/accordion/Accordion.js +66 -0
- package/dist/button/Button.d.ts +21 -0
- package/dist/button/Button.d.ts.map +1 -0
- package/dist/button/Button.js +26 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/mode-toggle/ModeToggle.d.ts +13 -0
- package/dist/mode-toggle/ModeToggle.d.ts.map +1 -0
- package/dist/mode-toggle/ModeToggle.js +11 -0
- package/dist/text-field/TextField.d.ts +21 -0
- package/dist/text-field/TextField.d.ts.map +1 -0
- package/dist/text-field/TextField.js +35 -0
- package/package.json +45 -0
- package/src/accordion/Accordion.tsx +207 -0
- package/src/accordion/accordion.css +178 -0
- package/src/button/Button.tsx +81 -0
- package/src/button/button.css +140 -0
- package/src/index.ts +25 -0
- package/src/mode-toggle/ModeToggle.tsx +51 -0
- package/src/mode-toggle/mode-toggle.css +92 -0
- package/src/styles.css +4 -0
- package/src/text-field/TextField.tsx +121 -0
- package/src/text-field/text-field.css +108 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
.gbgr-accordion {
|
|
2
|
+
--gbgr-accordion-set-max-width: 1600px;
|
|
3
|
+
--gbgr-accordion-row-max-width: 920px;
|
|
4
|
+
|
|
5
|
+
width: 100%;
|
|
6
|
+
border-radius: 20px;
|
|
7
|
+
background: var(--border-depth0);
|
|
8
|
+
padding: 40px;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: 24px;
|
|
12
|
+
align-items: center;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.gbgr-accordion[data-disabled] {
|
|
16
|
+
opacity: 0.7;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.gbgr-accordion__header {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
width: 100%;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.gbgr-accordion__header-title {
|
|
27
|
+
margin: 0;
|
|
28
|
+
text-align: center;
|
|
29
|
+
font-family: var(--font-families-pretendard, Pretendard);
|
|
30
|
+
font-weight: var(--font-weights-pretendard-0);
|
|
31
|
+
line-height: 1.5;
|
|
32
|
+
letter-spacing: 0;
|
|
33
|
+
color: var(--color-global-grey-800);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.gbgr-accordion__header[data-size="lg"] .gbgr-accordion__header-title {
|
|
37
|
+
font-size: 32px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.gbgr-accordion__header[data-size="md"] .gbgr-accordion__header-title {
|
|
41
|
+
font-size: 28px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.gbgr-accordion__set {
|
|
45
|
+
width: 100%;
|
|
46
|
+
max-width: var(--gbgr-accordion-set-max-width);
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
border-top: 1px solid var(--border-depth2);
|
|
52
|
+
border-bottom: 1px solid var(--border-depth2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.gbgr-accordion__item {
|
|
56
|
+
width: 100%;
|
|
57
|
+
max-width: var(--gbgr-accordion-row-max-width);
|
|
58
|
+
padding: 12px 20px;
|
|
59
|
+
border-bottom: 1px solid var(--border-depth2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.gbgr-accordion__item:last-child {
|
|
63
|
+
border-bottom: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.gbgr-accordion__trigger {
|
|
67
|
+
appearance: none;
|
|
68
|
+
width: 100%;
|
|
69
|
+
border: 0;
|
|
70
|
+
background: transparent;
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: space-between;
|
|
74
|
+
gap: var(--spacing-5);
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
color: inherit;
|
|
77
|
+
padding: 0;
|
|
78
|
+
text-align: left;
|
|
79
|
+
font-family: var(--font-families-pretendard, Pretendard);
|
|
80
|
+
font-size: 18px;
|
|
81
|
+
font-weight: var(--font-weights-pretendard-2);
|
|
82
|
+
line-height: 1.5;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.gbgr-accordion__left {
|
|
86
|
+
min-width: 0;
|
|
87
|
+
flex: 1 1 auto;
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
align-items: flex-start;
|
|
90
|
+
gap: 6px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.gbgr-accordion__label {
|
|
94
|
+
flex: 0 0 auto;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.gbgr-accordion__question {
|
|
99
|
+
min-width: 0;
|
|
100
|
+
flex: 0 1 494px;
|
|
101
|
+
white-space: pre-wrap;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.gbgr-accordion__trigger:focus-visible {
|
|
105
|
+
outline: 2px solid var(--border-active);
|
|
106
|
+
outline-offset: 2px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.gbgr-accordion__trigger:disabled {
|
|
110
|
+
cursor: not-allowed;
|
|
111
|
+
opacity: 0.6;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.gbgr-accordion__icons {
|
|
115
|
+
display: inline-flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
width: 24px;
|
|
119
|
+
height: 24px;
|
|
120
|
+
flex: 0 0 auto;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.gbgr-accordion__icon {
|
|
124
|
+
width: 24px;
|
|
125
|
+
height: 24px;
|
|
126
|
+
display: inline-flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.gbgr-accordion__icon svg {
|
|
132
|
+
width: 24px;
|
|
133
|
+
height: 24px;
|
|
134
|
+
display: block;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.gbgr-accordion__icon--open {
|
|
138
|
+
color: var(--color-global-grey-300);
|
|
139
|
+
display: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.gbgr-accordion__icon--closed {
|
|
143
|
+
color: var(--border-depth3);
|
|
144
|
+
display: inline-flex;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.gbgr-accordion__trigger[data-state="open"] .gbgr-accordion__icon--open {
|
|
148
|
+
display: inline-flex;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.gbgr-accordion__trigger[data-state="open"] .gbgr-accordion__icon--closed {
|
|
152
|
+
display: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.gbgr-accordion__content {
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-rows: 0fr;
|
|
158
|
+
transition: grid-template-rows 180ms ease;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.gbgr-accordion__content[hidden] {
|
|
163
|
+
display: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.gbgr-accordion__content[data-state="open"] {
|
|
167
|
+
grid-template-rows: 1fr;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.gbgr-accordion__content-inner {
|
|
171
|
+
min-height: 0;
|
|
172
|
+
padding: 12px 0 12px 20px;
|
|
173
|
+
font-family: var(--font-families-pretendard, Pretendard);
|
|
174
|
+
font-size: 16px;
|
|
175
|
+
font-weight: var(--font-weights-pretendard-2);
|
|
176
|
+
line-height: 1.5;
|
|
177
|
+
color: var(--color-global-grey-300);
|
|
178
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import clsx from "clsx"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
|
|
4
|
+
export type ButtonTone = "primary" | "sub" | "grey"
|
|
5
|
+
export type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl"
|
|
6
|
+
|
|
7
|
+
export type ButtonPressEvent = React.MouseEvent<HTMLButtonElement>
|
|
8
|
+
|
|
9
|
+
export type ButtonProps = Omit<
|
|
10
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
11
|
+
"type"
|
|
12
|
+
> & {
|
|
13
|
+
tone?: ButtonTone
|
|
14
|
+
size?: ButtonSize
|
|
15
|
+
type?: "button" | "submit" | "reset"
|
|
16
|
+
onPress?: (event: ButtonPressEvent) => void
|
|
17
|
+
startIcon?: React.ReactNode
|
|
18
|
+
endIcon?: React.ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
22
|
+
(props, ref) => {
|
|
23
|
+
const {
|
|
24
|
+
tone = "primary",
|
|
25
|
+
size = "md",
|
|
26
|
+
className,
|
|
27
|
+
type = "button",
|
|
28
|
+
onClick,
|
|
29
|
+
onPress,
|
|
30
|
+
startIcon,
|
|
31
|
+
endIcon,
|
|
32
|
+
children,
|
|
33
|
+
...rest
|
|
34
|
+
} = props
|
|
35
|
+
|
|
36
|
+
const handleClick = React.useCallback(
|
|
37
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
38
|
+
onClick?.(event)
|
|
39
|
+
if (!event.defaultPrevented) {
|
|
40
|
+
onPress?.(event)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[onClick, onPress],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
{...rest}
|
|
49
|
+
ref={ref}
|
|
50
|
+
type={type}
|
|
51
|
+
onClick={handleClick}
|
|
52
|
+
className={clsx(
|
|
53
|
+
"gbgr-button",
|
|
54
|
+
size === "xs"
|
|
55
|
+
? "gbgr-button--size-xs"
|
|
56
|
+
: size === "sm"
|
|
57
|
+
? "gbgr-button--size-sm"
|
|
58
|
+
: size === "md"
|
|
59
|
+
? "gbgr-button--size-md"
|
|
60
|
+
: size === "lg"
|
|
61
|
+
? "gbgr-button--size-lg"
|
|
62
|
+
: "gbgr-button--size-xl",
|
|
63
|
+
tone === "primary"
|
|
64
|
+
? "gbgr-button--tone-primary"
|
|
65
|
+
: tone === "sub"
|
|
66
|
+
? "gbgr-button--tone-sub"
|
|
67
|
+
: "gbgr-button--tone-grey",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{startIcon ? (
|
|
72
|
+
<span className="gbgr-button__icon">{startIcon}</span>
|
|
73
|
+
) : null}
|
|
74
|
+
{children}
|
|
75
|
+
{endIcon ? <span className="gbgr-button__icon">{endIcon}</span> : null}
|
|
76
|
+
</button>
|
|
77
|
+
)
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
Button.displayName = "Button"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
.gbgr-button {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: var(--spacing-2);
|
|
6
|
+
border: 0;
|
|
7
|
+
border-radius: var(--radius-full);
|
|
8
|
+
font-weight: var(--font-weights-pretendard-2);
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
user-select: none;
|
|
11
|
+
-webkit-tap-highlight-color: transparent;
|
|
12
|
+
transition:
|
|
13
|
+
background-color 120ms ease,
|
|
14
|
+
color 120ms ease,
|
|
15
|
+
transform 60ms ease;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.gbgr-button__icon {
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
width: 16px;
|
|
23
|
+
height: 16px;
|
|
24
|
+
flex: 0 0 auto;
|
|
25
|
+
color: currentColor;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.gbgr-button__icon > svg {
|
|
29
|
+
width: 16px;
|
|
30
|
+
height: 16px;
|
|
31
|
+
display: block;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Docs/demo only: simple circle placeholder inside the icon slot */
|
|
35
|
+
.gbgr-button__icon-placeholder {
|
|
36
|
+
width: 14px;
|
|
37
|
+
height: 14px;
|
|
38
|
+
border-radius: var(--radius-full);
|
|
39
|
+
border: 2px solid currentColor;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.gbgr-button:focus-visible {
|
|
43
|
+
outline: 2px solid var(--border-active);
|
|
44
|
+
outline-offset: 2px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.gbgr-button:disabled {
|
|
48
|
+
cursor: not-allowed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.gbgr-button--size-xs {
|
|
52
|
+
min-height: 32px;
|
|
53
|
+
padding: var(--spacing-2) var(--spacing-3);
|
|
54
|
+
font-size: var(--font-size-2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.gbgr-button--size-sm {
|
|
58
|
+
min-height: 36px;
|
|
59
|
+
padding: var(--spacing-2) var(--spacing-4);
|
|
60
|
+
font-size: var(--font-size-3);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.gbgr-button--size-md {
|
|
64
|
+
min-height: 40px;
|
|
65
|
+
padding: var(--spacing-3) var(--spacing-4);
|
|
66
|
+
font-size: var(--font-size-3);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.gbgr-button--size-lg {
|
|
70
|
+
min-height: 44px;
|
|
71
|
+
padding: var(--spacing-3) var(--spacing-5);
|
|
72
|
+
font-size: var(--font-size-4);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.gbgr-button--size-xl {
|
|
76
|
+
min-height: 54px;
|
|
77
|
+
padding: var(--spacing-4) var(--spacing-6);
|
|
78
|
+
font-size: var(--font-size-5);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.gbgr-button--tone-primary {
|
|
82
|
+
background: var(--color-component-button-base-bg-primary-default);
|
|
83
|
+
color: var(--color-component-button-base-text-primary-default);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.gbgr-button--tone-primary:hover:not(:disabled),
|
|
87
|
+
.gbgr-button--tone-primary[data-state="hover"]:not(:disabled) {
|
|
88
|
+
background: var(--color-component-button-base-bg-primary-hover);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.gbgr-button--tone-primary:active:not(:disabled),
|
|
92
|
+
.gbgr-button--tone-primary[data-state="pressed"]:not(:disabled) {
|
|
93
|
+
background: var(--color-component-button-base-bg-primary-pressed);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.gbgr-button--tone-primary:disabled {
|
|
97
|
+
background: var(--color-component-button-base-bg-primary-disabled);
|
|
98
|
+
color: var(--color-component-button-base-text-primary-disabled);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.gbgr-button--tone-sub {
|
|
102
|
+
background: var(--color-component-button-base-bg-sub-default);
|
|
103
|
+
color: var(--color-component-button-base-text-sub-default);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.gbgr-button--tone-sub:hover:not(:disabled),
|
|
107
|
+
.gbgr-button--tone-sub[data-state="hover"]:not(:disabled) {
|
|
108
|
+
background: var(--color-component-button-base-bg-sub-hover);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.gbgr-button--tone-sub:active:not(:disabled),
|
|
112
|
+
.gbgr-button--tone-sub[data-state="pressed"]:not(:disabled) {
|
|
113
|
+
background: var(--color-component-button-base-bg-sub-pressed);
|
|
114
|
+
color: var(--color-component-button-base-text-sub-pressed);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.gbgr-button--tone-sub:disabled {
|
|
118
|
+
background: var(--color-component-button-base-bg-sub-default);
|
|
119
|
+
color: var(--color-component-button-base-text-sub-disabled);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.gbgr-button--tone-grey {
|
|
123
|
+
background: var(--color-component-button-base-bg-grey-default);
|
|
124
|
+
color: var(--color-component-button-base-text-grey-default);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.gbgr-button--tone-grey:hover:not(:disabled),
|
|
128
|
+
.gbgr-button--tone-grey[data-state="hover"]:not(:disabled) {
|
|
129
|
+
background: var(--color-component-button-base-bg-grey-hover);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.gbgr-button--tone-grey:active:not(:disabled),
|
|
133
|
+
.gbgr-button--tone-grey[data-state="pressed"]:not(:disabled) {
|
|
134
|
+
background: var(--color-component-button-base-bg-grey-pressed);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.gbgr-button--tone-grey:disabled {
|
|
138
|
+
background: var(--color-component-button-base-bg-grey-disabled);
|
|
139
|
+
color: var(--color-component-button-base-text-grey-disabled);
|
|
140
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ButtonPressEvent,
|
|
3
|
+
ButtonProps,
|
|
4
|
+
ButtonSize,
|
|
5
|
+
ButtonTone,
|
|
6
|
+
} from "./button/Button"
|
|
7
|
+
export { Button } from "./button/Button"
|
|
8
|
+
export type {
|
|
9
|
+
AccordionContentProps,
|
|
10
|
+
AccordionItemProps,
|
|
11
|
+
AccordionProps,
|
|
12
|
+
AccordionTriggerProps,
|
|
13
|
+
} from "./accordion/Accordion"
|
|
14
|
+
export {
|
|
15
|
+
Accordion,
|
|
16
|
+
AccordionContent,
|
|
17
|
+
AccordionHeader,
|
|
18
|
+
AccordionItem,
|
|
19
|
+
AccordionTrigger,
|
|
20
|
+
} from "./accordion/Accordion"
|
|
21
|
+
export type { AccordionHeaderProps } from "./accordion/Accordion"
|
|
22
|
+
export type { ModeToggleProps, ModeToggleValue } from "./mode-toggle/ModeToggle"
|
|
23
|
+
export { ModeToggle } from "./mode-toggle/ModeToggle"
|
|
24
|
+
export type { TextFieldProps, TextFieldState } from "./text-field/TextField"
|
|
25
|
+
export { TextField } from "./text-field/TextField"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MoonIcon, SunIcon } from "@gbgr/icons"
|
|
2
|
+
import { useModeToggle } from "@gbgr/react-headless"
|
|
3
|
+
import clsx from "clsx"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
export type ModeToggleValue = "light" | "dark"
|
|
7
|
+
|
|
8
|
+
export type ModeToggleProps = Omit<
|
|
9
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
10
|
+
"type"
|
|
11
|
+
> & {
|
|
12
|
+
value?: ModeToggleValue
|
|
13
|
+
defaultValue?: ModeToggleValue
|
|
14
|
+
onValueChange?: (value: ModeToggleValue) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ModeToggle = React.forwardRef<HTMLButtonElement, ModeToggleProps>(
|
|
18
|
+
(props, ref) => {
|
|
19
|
+
const { className, ...rest } = props
|
|
20
|
+
|
|
21
|
+
const { value: currentValue, buttonProps } = useModeToggle(rest)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
{...buttonProps}
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={clsx("gbgr-mode-toggle", className)}
|
|
28
|
+
>
|
|
29
|
+
<span className="gbgr-mode-toggle__thumb" aria-hidden="true" />
|
|
30
|
+
<span
|
|
31
|
+
className={clsx(
|
|
32
|
+
"gbgr-mode-toggle__icon",
|
|
33
|
+
"gbgr-mode-toggle__icon--light",
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<SunIcon />
|
|
37
|
+
</span>
|
|
38
|
+
<span
|
|
39
|
+
className={clsx(
|
|
40
|
+
"gbgr-mode-toggle__icon",
|
|
41
|
+
"gbgr-mode-toggle__icon--dark",
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<MoonIcon />
|
|
45
|
+
</span>
|
|
46
|
+
</button>
|
|
47
|
+
)
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
ModeToggle.displayName = "ModeToggle"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
.gbgr-mode-toggle {
|
|
2
|
+
--gbgr-mode-toggle-width: 62px;
|
|
3
|
+
--gbgr-mode-toggle-height: 30px;
|
|
4
|
+
--gbgr-mode-toggle-padding: 3px;
|
|
5
|
+
--gbgr-mode-toggle-thumb: 24px;
|
|
6
|
+
--gbgr-mode-toggle-icon: 16px;
|
|
7
|
+
|
|
8
|
+
position: relative;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
width: var(--gbgr-mode-toggle-width);
|
|
13
|
+
height: var(--gbgr-mode-toggle-height);
|
|
14
|
+
padding: 0;
|
|
15
|
+
border: 0;
|
|
16
|
+
border-radius: var(--radius-full);
|
|
17
|
+
background: var(--color-component-button-mode-change-bg);
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
user-select: none;
|
|
20
|
+
-webkit-tap-highlight-color: transparent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.gbgr-mode-toggle:disabled {
|
|
24
|
+
cursor: not-allowed;
|
|
25
|
+
opacity: 0.6;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.gbgr-mode-toggle:focus-visible {
|
|
29
|
+
outline: 2px solid var(--border-active);
|
|
30
|
+
outline-offset: 2px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.gbgr-mode-toggle__thumb {
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: var(--gbgr-mode-toggle-padding);
|
|
36
|
+
left: var(--gbgr-mode-toggle-padding);
|
|
37
|
+
width: var(--gbgr-mode-toggle-thumb);
|
|
38
|
+
height: var(--gbgr-mode-toggle-thumb);
|
|
39
|
+
border-radius: var(--radius-full);
|
|
40
|
+
background: var(--color-component-button-mode-change-thumb-on);
|
|
41
|
+
transition:
|
|
42
|
+
transform 160ms ease,
|
|
43
|
+
background-color 160ms ease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.gbgr-mode-toggle[data-value="dark"] .gbgr-mode-toggle__thumb {
|
|
47
|
+
transform: translateX(
|
|
48
|
+
calc(
|
|
49
|
+
var(--gbgr-mode-toggle-width) -
|
|
50
|
+
(var(--gbgr-mode-toggle-padding) * 2) -
|
|
51
|
+
var(--gbgr-mode-toggle-thumb)
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.gbgr-mode-toggle__icon {
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 50%;
|
|
59
|
+
width: var(--gbgr-mode-toggle-icon);
|
|
60
|
+
height: var(--gbgr-mode-toggle-icon);
|
|
61
|
+
transform: translateY(-50%);
|
|
62
|
+
color: var(--color-component-button-mode-change-switch-off);
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
z-index: 1;
|
|
67
|
+
transition:
|
|
68
|
+
color 160ms ease,
|
|
69
|
+
opacity 160ms ease;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.gbgr-mode-toggle__icon svg {
|
|
73
|
+
width: var(--gbgr-mode-toggle-icon);
|
|
74
|
+
height: var(--gbgr-mode-toggle-icon);
|
|
75
|
+
display: block;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.gbgr-mode-toggle__icon--light {
|
|
79
|
+
left: calc(var(--gbgr-mode-toggle-padding) + 4px);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.gbgr-mode-toggle__icon--dark {
|
|
83
|
+
right: calc(var(--gbgr-mode-toggle-padding) + 4px);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.gbgr-mode-toggle[data-value="light"] .gbgr-mode-toggle__icon--light {
|
|
87
|
+
color: var(--color-component-button-mode-change-switch-on);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.gbgr-mode-toggle[data-value="dark"] .gbgr-mode-toggle__icon--dark {
|
|
91
|
+
color: var(--color-component-button-mode-change-switch-on);
|
|
92
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { HideIcon, InfoCircleIcon, ShowIcon } from "@gbgr/icons"
|
|
2
|
+
import { useTextField } from "@gbgr/react-headless"
|
|
3
|
+
import clsx from "clsx"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
export type TextFieldState = "default" | "success" | "error"
|
|
7
|
+
|
|
8
|
+
export type TextFieldProps = Omit<
|
|
9
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
10
|
+
"size"
|
|
11
|
+
> & {
|
|
12
|
+
state?: TextFieldState
|
|
13
|
+
subText?: string
|
|
14
|
+
subIcon?: React.ReactNode
|
|
15
|
+
endAdornment?: React.ReactNode
|
|
16
|
+
passwordVisible?: boolean
|
|
17
|
+
defaultPasswordVisible?: boolean
|
|
18
|
+
onPasswordVisibleChange?: (visible: boolean) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function SuccessIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
22
|
+
return (
|
|
23
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}>
|
|
24
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.8" />
|
|
25
|
+
<path
|
|
26
|
+
d="M8.5 12.3 11 14.7 15.7 9.8"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
strokeWidth="1.8"
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
/>
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|
37
|
+
(props, ref) => {
|
|
38
|
+
const {
|
|
39
|
+
state = "default",
|
|
40
|
+
subText,
|
|
41
|
+
subIcon,
|
|
42
|
+
endAdornment,
|
|
43
|
+
passwordVisible,
|
|
44
|
+
defaultPasswordVisible,
|
|
45
|
+
onPasswordVisibleChange,
|
|
46
|
+
className,
|
|
47
|
+
type = "text",
|
|
48
|
+
disabled,
|
|
49
|
+
...rest
|
|
50
|
+
} = props
|
|
51
|
+
|
|
52
|
+
const { isPassword, isPasswordVisible, inputType, togglePasswordVisible } =
|
|
53
|
+
useTextField({
|
|
54
|
+
type,
|
|
55
|
+
disabled,
|
|
56
|
+
passwordVisible,
|
|
57
|
+
defaultPasswordVisible,
|
|
58
|
+
onPasswordVisibleChange,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const resolvedEndAdornment = React.useMemo(() => {
|
|
62
|
+
if (endAdornment) return endAdornment
|
|
63
|
+
if (!isPassword) return null
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
className="gbgr-text-field__button"
|
|
69
|
+
onClick={togglePasswordVisible}
|
|
70
|
+
aria-label={isPasswordVisible ? "Hide password" : "Show password"}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
>
|
|
73
|
+
{isPasswordVisible ? <HideIcon /> : <ShowIcon />}
|
|
74
|
+
</button>
|
|
75
|
+
)
|
|
76
|
+
}, [
|
|
77
|
+
endAdornment,
|
|
78
|
+
isPassword,
|
|
79
|
+
isPasswordVisible,
|
|
80
|
+
togglePasswordVisible,
|
|
81
|
+
disabled,
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
const resolvedSubIcon =
|
|
85
|
+
subIcon ??
|
|
86
|
+
(state === "success" ? (
|
|
87
|
+
<SuccessIcon />
|
|
88
|
+
) : state === "error" ? (
|
|
89
|
+
<InfoCircleIcon />
|
|
90
|
+
) : null)
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={clsx("gbgr-text-field", className)} data-state={state}>
|
|
94
|
+
<div className="gbgr-text-field__control">
|
|
95
|
+
<input
|
|
96
|
+
{...rest}
|
|
97
|
+
ref={ref}
|
|
98
|
+
type={inputType}
|
|
99
|
+
disabled={disabled}
|
|
100
|
+
className="gbgr-text-field__input"
|
|
101
|
+
/>
|
|
102
|
+
{resolvedEndAdornment ? (
|
|
103
|
+
<span className="gbgr-text-field__end">{resolvedEndAdornment}</span>
|
|
104
|
+
) : null}
|
|
105
|
+
</div>
|
|
106
|
+
{subText ? (
|
|
107
|
+
<div className="gbgr-text-field__sub">
|
|
108
|
+
{resolvedSubIcon ? (
|
|
109
|
+
<span className="gbgr-text-field__sub-icon" aria-hidden="true">
|
|
110
|
+
{resolvedSubIcon}
|
|
111
|
+
</span>
|
|
112
|
+
) : null}
|
|
113
|
+
<span>{subText}</span>
|
|
114
|
+
</div>
|
|
115
|
+
) : null}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
TextField.displayName = "TextField"
|