@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.
@@ -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,4 @@
1
+ @import "./button/button.css";
2
+ @import "./accordion/accordion.css";
3
+ @import "./mode-toggle/mode-toggle.css";
4
+ @import "./text-field/text-field.css";
@@ -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"