@uniai-fe/uds-primitives 0.0.16 → 0.0.17
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/README.md +9 -1
- package/dist/styles.css +128 -35
- package/package.json +1 -1
- package/src/components/checkbox/img/check-large.svg +1 -1
- package/src/components/checkbox/img/check-medium.svg +1 -1
- package/src/components/checkbox/markup/Checkbox.tsx +6 -3
- package/src/components/checkbox/styles/index.scss +38 -25
- package/src/components/input/markup/text/AuthCode.tsx +145 -0
- package/src/components/input/markup/text/Base.tsx +63 -57
- package/src/components/input/markup/text/{EmailVerification.tsx → Email.tsx} +50 -31
- package/src/components/input/markup/text/InputUtilityButton.tsx +46 -0
- package/src/components/input/markup/text/Phone.tsx +65 -7
- package/src/components/input/markup/text/index.ts +4 -4
- package/src/components/input/styles/index.scss +98 -13
- package/src/components/input/markup/text/Identification.tsx +0 -159
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @uniai-fe/uds-primitives
|
|
2
2
|
|
|
3
|
-
`@uniai-fe/uds-foundation` 토큰 위에 Radix UI 컴포넌트를 얇게 감싼 **기초 UI 컴포넌트 컬렉션**입니다. Next.js 등 React 런타임에서 바로 import해 버튼·입력·네비게이션 등 공통 요소를 일관된 스타일로 사용할 수 있습니다.
|
|
3
|
+
`@uniai-fe/uds-foundation` 토큰 위에 Radix UI 컴포넌트를 얇게 감싼 **기초 UI 컴포넌트 컬렉션**입니다. Next.js 등 React 런타임에서 바로 import해 버튼·입력·네비게이션 등 공통 요소를 일관된 스타일로 사용할 수 있습니다. Templates(`@uniai-fe/uds-templates`) 패키지에서 사용하는 Phone Input/Email Verification Input/OneTimeCode Input 등의 인증 시나리오 컴포넌트도 이곳에서 제공합니다.
|
|
4
4
|
|
|
5
5
|
## 설치
|
|
6
6
|
|
|
@@ -192,8 +192,16 @@ src/components/{category}/
|
|
|
192
192
|
## 문서
|
|
193
193
|
|
|
194
194
|
- `CONTEXT.md` 및 `CONTEXT-*.md`: 각 컴포넌트의 상태/진행/디자인 근거
|
|
195
|
+
- `CONTEXT-INPUT.md`: Phone/Email/OneTimeCode 등 인증 입력 시나리오 규칙을 포함하며, templates `CONTEXT-SIGNUP*.md`와 항상 동기화해야 한다.
|
|
195
196
|
- `RADIX-SIZE-GUIDE.md`: primitives 사이즈 체계와 Radix 매핑 규칙
|
|
196
197
|
|
|
198
|
+
### Signup 인증 입력 컴포넌트
|
|
199
|
+
|
|
200
|
+
- **PhoneInput**: 기본은 마스킹된 전화번호 입력만 제공한다. `onRequestCode`(optional)를 주입하면 인증요청 버튼이 우측 right 슬롯에 노출된다. Step1 User Info에서는 단순 입력만 필요하므로 optional props를 생략한다.
|
|
201
|
+
- **EmailInput**: 이메일 입력 + 인증요청 버튼 + countdown + OneTimeCode 입력을 하나로 묶는다. `countdownText`, `codeVisible`, `codeLength`, `codeLabel`, `codeHelper`, `codeState`, `onCodeComplete`로 Step2 Verify & Agreement 상태를 제어한다.
|
|
202
|
+
- **AuthCodeInput**: length 지정형 OneTimeCode grid. EmailInput 내부에서 사용하지만 서비스 앱도 직접 import할 수 있다.
|
|
203
|
+
- 변경 시에는 다음 문서를 함께 업데이트한다: `packages/design/primitives/docs/CONTEXT-INPUT.md`, `packages/design/templates/docs/CONTEXT-SIGNUP.md`, `CONTEXT-SIGNUP-FLOW.md`, `packages/design/templates/docs/STORYBOOK.md`, `apps/design-storybook/src/stories/templates/auth/AuthSignup.stories.tsx`.
|
|
204
|
+
|
|
197
205
|
* **컨벤션**: 모든 컴포넌트/스토리/문서는 slot/prefix/suffix 용어를 사용하지 않고, 레이아웃 기준(`header/body/footer`, 2단 구조는 `upper/lower`)과 `util*` 키워드를 사용한다. 인터랙션 함수는 `on*` 접두사를 사용하고, JSDoc `@param`은 depth 전체를 풀어 쓴다.
|
|
198
206
|
|
|
199
207
|
필요한 컨텍스트를 확인한 뒤 컴포넌트를 import해 사용하면 됩니다.
|
package/dist/styles.css
CHANGED
|
@@ -1663,13 +1663,15 @@
|
|
|
1663
1663
|
--theme-checkbox-surface: var(--color-common-100);
|
|
1664
1664
|
--theme-checkbox-surface-selected: var(--color-primary-default);
|
|
1665
1665
|
--theme-checkbox-surface-disabled: var(--color-neutral-95);
|
|
1666
|
+
--theme-checkbox-surface-selected-disabled: rgba(26, 106, 255, 0.28);
|
|
1666
1667
|
--theme-checkbox-label-color: var(--color-label-strong);
|
|
1667
1668
|
--theme-checkbox-label-disabled: var(--color-label-disabled);
|
|
1668
1669
|
--theme-checkbox-helper-color: var(--color-label-neutral);
|
|
1669
1670
|
--theme-checkbox-helper-disabled: var(--color-label-disabled);
|
|
1670
|
-
--theme-checkbox-icon-
|
|
1671
|
+
--theme-checkbox-icon-default: transparent;
|
|
1672
|
+
--theme-checkbox-icon-selected: var(--color-common-100);
|
|
1673
|
+
--theme-checkbox-icon-disabled-selected: var(--color-common-100);
|
|
1671
1674
|
--theme-checkbox-focus-ring: rgba(2, 84, 255, 0.32);
|
|
1672
|
-
--theme-checkbox-disabled-selected-opacity: 0.28;
|
|
1673
1675
|
}
|
|
1674
1676
|
|
|
1675
1677
|
.checkbox {
|
|
@@ -1695,7 +1697,6 @@
|
|
|
1695
1697
|
border-radius: var(--theme-checkbox-control-radius-large);
|
|
1696
1698
|
}
|
|
1697
1699
|
.checkbox[data-disabled=true] {
|
|
1698
|
-
opacity: 0.6;
|
|
1699
1700
|
cursor: not-allowed;
|
|
1700
1701
|
}
|
|
1701
1702
|
|
|
@@ -1703,49 +1704,64 @@
|
|
|
1703
1704
|
box-shadow: 0 0 0 2px var(--theme-checkbox-focus-ring);
|
|
1704
1705
|
}
|
|
1705
1706
|
|
|
1706
|
-
.checkbox-
|
|
1707
|
+
.checkbox-surface {
|
|
1707
1708
|
inline-size: var(--theme-checkbox-indicator-size-medium);
|
|
1708
1709
|
block-size: var(--theme-checkbox-indicator-size-medium);
|
|
1709
1710
|
display: inline-flex;
|
|
1710
1711
|
align-items: center;
|
|
1711
1712
|
justify-content: center;
|
|
1712
|
-
color: var(--theme-checkbox-icon-color);
|
|
1713
1713
|
border: var(--theme-checkbox-border-width) solid var(--theme-checkbox-border-color);
|
|
1714
1714
|
border-radius: var(--theme-checkbox-control-radius-medium);
|
|
1715
1715
|
background-color: var(--theme-checkbox-surface);
|
|
1716
|
-
transition: background-color 0.15s ease, border-color 0.15s ease
|
|
1717
|
-
}
|
|
1718
|
-
.checkbox-indicator svg {
|
|
1719
|
-
display: block;
|
|
1720
|
-
inline-size: auto;
|
|
1721
|
-
block-size: auto;
|
|
1722
|
-
max-inline-size: 100%;
|
|
1723
|
-
max-block-size: 100%;
|
|
1716
|
+
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
1724
1717
|
}
|
|
1725
1718
|
|
|
1726
|
-
.checkbox[data-size=large] .checkbox-
|
|
1719
|
+
.checkbox[data-size=large] .checkbox-surface {
|
|
1727
1720
|
inline-size: var(--theme-checkbox-indicator-size-large);
|
|
1728
1721
|
block-size: var(--theme-checkbox-indicator-size-large);
|
|
1729
1722
|
border-radius: var(--theme-checkbox-control-radius-large);
|
|
1730
1723
|
}
|
|
1731
1724
|
|
|
1732
|
-
.checkbox[data-state=checked] .checkbox-
|
|
1733
|
-
.checkbox[data-state=indeterminate] .checkbox-
|
|
1725
|
+
.checkbox[data-state=checked] .checkbox-surface,
|
|
1726
|
+
.checkbox[data-state=indeterminate] .checkbox-surface {
|
|
1734
1727
|
background-color: var(--theme-checkbox-surface-selected);
|
|
1735
1728
|
border-color: var(--theme-checkbox-border-selected);
|
|
1736
1729
|
}
|
|
1737
1730
|
|
|
1738
|
-
.checkbox[data-disabled=true] .checkbox-
|
|
1731
|
+
.checkbox[data-disabled=true] .checkbox-surface {
|
|
1739
1732
|
background-color: var(--theme-checkbox-surface-disabled);
|
|
1740
1733
|
border-color: var(--theme-checkbox-border-color);
|
|
1741
|
-
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.checkbox[data-disabled=true][data-state=checked] .checkbox-surface,
|
|
1737
|
+
.checkbox[data-disabled=true][data-state=indeterminate] .checkbox-surface {
|
|
1738
|
+
background-color: var(--theme-checkbox-surface-selected-disabled);
|
|
1739
|
+
border-color: var(--theme-checkbox-border-color);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
.checkbox-indicator {
|
|
1743
|
+
display: inline-flex;
|
|
1744
|
+
align-items: center;
|
|
1745
|
+
justify-content: center;
|
|
1746
|
+
color: var(--theme-checkbox-icon-default);
|
|
1747
|
+
transition: color 0.15s ease;
|
|
1748
|
+
}
|
|
1749
|
+
.checkbox-indicator svg {
|
|
1750
|
+
display: block;
|
|
1751
|
+
inline-size: auto;
|
|
1752
|
+
block-size: auto;
|
|
1753
|
+
max-inline-size: 100%;
|
|
1754
|
+
max-block-size: 100%;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
.checkbox[data-state=checked] .checkbox-indicator,
|
|
1758
|
+
.checkbox[data-state=indeterminate] .checkbox-indicator {
|
|
1759
|
+
color: var(--theme-checkbox-icon-selected);
|
|
1742
1760
|
}
|
|
1743
1761
|
|
|
1744
1762
|
.checkbox[data-disabled=true][data-state=checked] .checkbox-indicator,
|
|
1745
1763
|
.checkbox[data-disabled=true][data-state=indeterminate] .checkbox-indicator {
|
|
1746
|
-
|
|
1747
|
-
border-color: var(--theme-checkbox-border-selected);
|
|
1748
|
-
opacity: var(--theme-checkbox-disabled-selected-opacity);
|
|
1764
|
+
color: var(--theme-checkbox-icon-disabled-selected);
|
|
1749
1765
|
}
|
|
1750
1766
|
|
|
1751
1767
|
.checkbox-field {
|
|
@@ -2248,24 +2264,53 @@ figure.chip {
|
|
|
2248
2264
|
padding-block: var(--spacing-padding-4);
|
|
2249
2265
|
background-color: transparent;
|
|
2250
2266
|
}
|
|
2267
|
+
.input-field[data-priority=secondary][data-state=active], .input-field[data-priority=secondary][data-state=focused] {
|
|
2268
|
+
border-bottom-color: var(--theme-input-border-active);
|
|
2269
|
+
border-bottom-width: var(--theme-input-border-width-emphasis);
|
|
2270
|
+
}
|
|
2271
|
+
.input-field[data-priority=secondary][data-state=success] {
|
|
2272
|
+
border-bottom-color: var(--theme-input-border-success);
|
|
2273
|
+
border-bottom-width: var(--theme-input-border-width-emphasis);
|
|
2274
|
+
}
|
|
2275
|
+
.input-field[data-priority=secondary][data-state=error] {
|
|
2276
|
+
border-bottom-color: var(--theme-input-border-error);
|
|
2277
|
+
border-bottom-width: var(--theme-input-border-width-emphasis);
|
|
2278
|
+
}
|
|
2279
|
+
.input-field[data-priority=secondary][data-state=disabled] {
|
|
2280
|
+
border-bottom-color: var(--theme-input-border-underline-disabled);
|
|
2281
|
+
border-bottom-width: var(--theme-input-border-width-default);
|
|
2282
|
+
}
|
|
2251
2283
|
.input-field[data-priority=tertiary] {
|
|
2252
2284
|
border-radius: var(--theme-input-radius-tertiary);
|
|
2253
2285
|
background-color: var(--theme-input-surface);
|
|
2254
2286
|
min-height: var(--theme-input-height-tertiary);
|
|
2255
|
-
flex-wrap: wrap;
|
|
2256
2287
|
row-gap: var(--spacing-gap-1);
|
|
2257
2288
|
column-gap: var(--theme-input-gap);
|
|
2289
|
+
flex-wrap: wrap;
|
|
2290
|
+
align-items: center;
|
|
2291
|
+
}
|
|
2292
|
+
.input-field[data-priority=tertiary] .input-field__control {
|
|
2293
|
+
display: grid;
|
|
2294
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
2295
|
+
column-gap: var(--theme-input-gap);
|
|
2296
|
+
row-gap: var(--spacing-gap-1);
|
|
2297
|
+
align-items: center;
|
|
2298
|
+
flex: 1 1 auto;
|
|
2299
|
+
min-width: 0;
|
|
2258
2300
|
}
|
|
2259
2301
|
.input-field[data-priority=tertiary] .input-inline-label {
|
|
2260
|
-
|
|
2302
|
+
grid-column: 1/-1;
|
|
2303
|
+
margin: 0;
|
|
2304
|
+
align-self: flex-start;
|
|
2261
2305
|
}
|
|
2262
2306
|
.input-field[data-priority=tertiary] .input-element {
|
|
2263
2307
|
min-height: var(--theme-size-medium-2);
|
|
2264
2308
|
width: auto;
|
|
2265
2309
|
flex: 1 1 auto;
|
|
2266
2310
|
}
|
|
2267
|
-
.input-field[data-priority=tertiary] .input-
|
|
2268
|
-
|
|
2311
|
+
.input-field[data-priority=tertiary] .input-field__utilities {
|
|
2312
|
+
align-self: center;
|
|
2313
|
+
margin-left: 0;
|
|
2269
2314
|
}
|
|
2270
2315
|
.input-field:not([data-priority=secondary])[data-state=active], .input-field:not([data-priority=secondary])[data-state=focused] {
|
|
2271
2316
|
border-color: var(--theme-input-border-active);
|
|
@@ -2311,6 +2356,25 @@ figure.chip {
|
|
|
2311
2356
|
box-shadow: none;
|
|
2312
2357
|
}
|
|
2313
2358
|
|
|
2359
|
+
.input-field__control {
|
|
2360
|
+
display: flex;
|
|
2361
|
+
align-items: center;
|
|
2362
|
+
gap: var(--theme-input-gap);
|
|
2363
|
+
flex: 1 1 auto;
|
|
2364
|
+
min-width: 0;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
.input-field__utilities {
|
|
2368
|
+
display: flex;
|
|
2369
|
+
align-items: center;
|
|
2370
|
+
gap: var(--spacing-gap-2, 8px);
|
|
2371
|
+
flex-shrink: 0;
|
|
2372
|
+
margin-left: var(--spacing-gap-3, 12px);
|
|
2373
|
+
}
|
|
2374
|
+
.input-field__utilities .input-affix {
|
|
2375
|
+
margin-left: 0;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2314
2378
|
.input-inline-label {
|
|
2315
2379
|
order: -2;
|
|
2316
2380
|
flex-basis: 100%;
|
|
@@ -2340,7 +2404,7 @@ figure.chip {
|
|
|
2340
2404
|
}
|
|
2341
2405
|
|
|
2342
2406
|
.input-affix {
|
|
2343
|
-
display:
|
|
2407
|
+
display: flex;
|
|
2344
2408
|
align-items: center;
|
|
2345
2409
|
justify-content: center;
|
|
2346
2410
|
min-width: 20px;
|
|
@@ -2414,8 +2478,7 @@ figure.chip {
|
|
|
2414
2478
|
background-color: transparent;
|
|
2415
2479
|
}
|
|
2416
2480
|
|
|
2417
|
-
.input-password-toggle
|
|
2418
|
-
.input-action-button {
|
|
2481
|
+
.input-password-toggle {
|
|
2419
2482
|
border: none;
|
|
2420
2483
|
background: transparent;
|
|
2421
2484
|
color: var(--theme-input-label-accent-color);
|
|
@@ -2425,8 +2488,7 @@ figure.chip {
|
|
|
2425
2488
|
padding: 0;
|
|
2426
2489
|
cursor: pointer;
|
|
2427
2490
|
}
|
|
2428
|
-
.input-password-toggle:disabled
|
|
2429
|
-
.input-action-button:disabled {
|
|
2491
|
+
.input-password-toggle:disabled {
|
|
2430
2492
|
color: var(--theme-input-helper-disabled-color);
|
|
2431
2493
|
cursor: not-allowed;
|
|
2432
2494
|
}
|
|
@@ -2473,16 +2535,47 @@ figure.chip {
|
|
|
2473
2535
|
color: var(--theme-input-helper-color);
|
|
2474
2536
|
}
|
|
2475
2537
|
|
|
2476
|
-
.email-verification
|
|
2538
|
+
.email-verification,
|
|
2539
|
+
.phone-verification {
|
|
2477
2540
|
display: flex;
|
|
2478
2541
|
flex-direction: column;
|
|
2479
2542
|
gap: var(--spacing-gap-4);
|
|
2480
2543
|
}
|
|
2481
2544
|
|
|
2482
|
-
.
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2545
|
+
.auth-code-input__actions,
|
|
2546
|
+
.email-verification__code-actions,
|
|
2547
|
+
.phone-verification__code-actions {
|
|
2548
|
+
display: flex;
|
|
2549
|
+
align-items: center;
|
|
2550
|
+
justify-content: flex-end;
|
|
2551
|
+
gap: var(--spacing-gap-3);
|
|
2552
|
+
min-width: 0;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
.auth-code-input__countdown,
|
|
2556
|
+
.email-verification__countdown,
|
|
2557
|
+
.phone-verification__countdown {
|
|
2558
|
+
display: flex;
|
|
2559
|
+
align-items: center;
|
|
2560
|
+
font-weight: 500;
|
|
2561
|
+
font-style: normal;
|
|
2562
|
+
font-size: 13px;
|
|
2563
|
+
line-height: 1em;
|
|
2564
|
+
letter-spacing: -0.0025em;
|
|
2565
|
+
color: var(--color-primary-default);
|
|
2566
|
+
flex-shrink: 0;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
.button.input-utility-button {
|
|
2570
|
+
min-height: 32px;
|
|
2571
|
+
padding: var(--spacing-padding-2, 4px) var(--spacing-padding-6, 24px);
|
|
2572
|
+
border-radius: var(--shape-rounded-1, 8px);
|
|
2573
|
+
}
|
|
2574
|
+
.button.input-utility-button .button-label {
|
|
2575
|
+
font-size: var(--font-body-xxsmall-size);
|
|
2576
|
+
line-height: var(--font-body-xxsmall-line-height);
|
|
2577
|
+
letter-spacing: var(--font-body-xxsmall-letter-spacing);
|
|
2578
|
+
font-weight: var(--font-body-xxsmall-weight);
|
|
2486
2579
|
}
|
|
2487
2580
|
|
|
2488
2581
|
/* TODO(label): 스타일을 SOT 토큰 값으로 정의한다. */
|
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<path d="M0.699997 3.52844L4.23553 7.06398L10.5995 0.700012" stroke="
|
|
2
|
+
<path d="M0.699997 3.52844L4.23553 7.06398L10.5995 0.700012" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
3
|
</svg>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<path d="M0.170857 3.35307C-0.0569492 3.12526 -0.0569492 2.75569 0.170857 2.52788C0.398662 2.30008 0.768239 2.30008 0.996045 2.52788L3.52973 5.06157L8.42044 0.170859C8.64824 -0.0569466 9.01782 -0.056947 9.24562 0.170859C9.47343 0.398665 9.47343 0.768242 9.24562 0.996047L3.94232 6.29935C3.83293 6.40874 3.68444 6.47025 3.52973 6.47025C3.37502 6.47025 3.22653 6.40874 3.11713 6.29935L0.170857 3.35307Z" fill="
|
|
2
|
+
<path d="M0.170857 3.35307C-0.0569492 3.12526 -0.0569492 2.75569 0.170857 2.52788C0.398662 2.30008 0.768239 2.30008 0.996045 2.52788L3.52973 5.06157L8.42044 0.170859C8.64824 -0.0569466 9.01782 -0.056947 9.24562 0.170859C9.47343 0.398665 9.47343 0.768242 9.24562 0.996047L3.94232 6.29935C3.83293 6.40874 3.68444 6.47025 3.52973 6.47025C3.37502 6.47025 3.22653 6.40874 3.11713 6.29935L0.170857 3.35307Z" fill="currentColor"/>
|
|
3
3
|
</svg>
|
|
@@ -6,6 +6,7 @@ import CheckLargeIcon from "../img/check-large.svg";
|
|
|
6
6
|
import CheckMediumIcon from "../img/check-medium.svg";
|
|
7
7
|
|
|
8
8
|
const CHECKBOX_CLASSNAME = "checkbox";
|
|
9
|
+
const CHECKBOX_SURFACE_CLASSNAME = "checkbox-surface";
|
|
9
10
|
const CHECKBOX_INDICATOR_CLASSNAME = "checkbox-indicator";
|
|
10
11
|
const CHECKBOX_FIELD_CLASSNAME = "checkbox-field";
|
|
11
12
|
const CHECKBOX_LABEL_WRAPPER_CLASSNAME = "checkbox-label-wrapper";
|
|
@@ -46,9 +47,11 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
|
46
47
|
className={clsx(CHECKBOX_CLASSNAME, className)}
|
|
47
48
|
{...restProps}
|
|
48
49
|
>
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
50
|
+
<span className={CHECKBOX_SURFACE_CLASSNAME} aria-hidden="true">
|
|
51
|
+
<CheckboxPrimitive.Indicator className={CHECKBOX_INDICATOR_CLASSNAME}>
|
|
52
|
+
<IndicatorIcon aria-hidden />
|
|
53
|
+
</CheckboxPrimitive.Indicator>
|
|
54
|
+
</span>
|
|
52
55
|
</CheckboxPrimitive.Root>
|
|
53
56
|
);
|
|
54
57
|
},
|
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
--theme-checkbox-surface: var(--color-common-100);
|
|
14
14
|
--theme-checkbox-surface-selected: var(--color-primary-default);
|
|
15
15
|
--theme-checkbox-surface-disabled: var(--color-neutral-95);
|
|
16
|
+
--theme-checkbox-surface-selected-disabled: rgba(26, 106, 255, 0.28);
|
|
16
17
|
--theme-checkbox-label-color: var(--color-label-strong);
|
|
17
18
|
--theme-checkbox-label-disabled: var(--color-label-disabled);
|
|
18
19
|
--theme-checkbox-helper-color: var(--color-label-neutral);
|
|
19
20
|
--theme-checkbox-helper-disabled: var(--color-label-disabled);
|
|
20
|
-
--theme-checkbox-icon-
|
|
21
|
+
--theme-checkbox-icon-default: transparent;
|
|
22
|
+
--theme-checkbox-icon-selected: var(--color-common-100);
|
|
23
|
+
--theme-checkbox-icon-disabled-selected: var(--color-common-100);
|
|
21
24
|
--theme-checkbox-focus-ring: rgba(2, 84, 255, 0.32);
|
|
22
|
-
--theme-checkbox-disabled-selected-opacity: 0.28;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
.checkbox {
|
|
@@ -51,7 +53,6 @@
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
&[data-disabled="true"] {
|
|
54
|
-
opacity: 0.6;
|
|
55
56
|
cursor: not-allowed;
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -60,58 +61,70 @@
|
|
|
60
61
|
box-shadow: 0 0 0 2px var(--theme-checkbox-focus-ring);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
.checkbox-
|
|
64
|
+
.checkbox-surface {
|
|
64
65
|
inline-size: var(--theme-checkbox-indicator-size-medium);
|
|
65
66
|
block-size: var(--theme-checkbox-indicator-size-medium);
|
|
66
67
|
display: inline-flex;
|
|
67
68
|
align-items: center;
|
|
68
69
|
justify-content: center;
|
|
69
|
-
color: var(--theme-checkbox-icon-color);
|
|
70
70
|
border: var(--theme-checkbox-border-width) solid
|
|
71
71
|
var(--theme-checkbox-border-color);
|
|
72
72
|
border-radius: var(--theme-checkbox-control-radius-medium);
|
|
73
73
|
background-color: var(--theme-checkbox-surface);
|
|
74
74
|
transition:
|
|
75
75
|
background-color 0.15s ease,
|
|
76
|
-
border-color 0.15s ease
|
|
77
|
-
color 0.15s ease,
|
|
78
|
-
opacity 0.15s ease;
|
|
79
|
-
|
|
80
|
-
svg {
|
|
81
|
-
display: block;
|
|
82
|
-
inline-size: auto;
|
|
83
|
-
block-size: auto;
|
|
84
|
-
max-inline-size: 100%;
|
|
85
|
-
max-block-size: 100%;
|
|
86
|
-
}
|
|
76
|
+
border-color 0.15s ease;
|
|
87
77
|
}
|
|
88
78
|
|
|
89
|
-
.checkbox[data-size="large"] .checkbox-
|
|
79
|
+
.checkbox[data-size="large"] .checkbox-surface {
|
|
90
80
|
inline-size: var(--theme-checkbox-indicator-size-large);
|
|
91
81
|
block-size: var(--theme-checkbox-indicator-size-large);
|
|
92
82
|
border-radius: var(--theme-checkbox-control-radius-large);
|
|
93
83
|
}
|
|
94
84
|
|
|
95
85
|
// 인디케이터 영역(16x16 / 20x20)에만 상태 색상을 입혀 Figma 레이아웃과 동일하게 유지한다.
|
|
96
|
-
.checkbox[data-state="checked"] .checkbox-
|
|
97
|
-
.checkbox[data-state="indeterminate"] .checkbox-
|
|
86
|
+
.checkbox[data-state="checked"] .checkbox-surface,
|
|
87
|
+
.checkbox[data-state="indeterminate"] .checkbox-surface {
|
|
98
88
|
background-color: var(--theme-checkbox-surface-selected);
|
|
99
89
|
border-color: var(--theme-checkbox-border-selected);
|
|
100
90
|
}
|
|
101
91
|
|
|
102
|
-
.checkbox[data-disabled="true"] .checkbox-
|
|
92
|
+
.checkbox[data-disabled="true"] .checkbox-surface {
|
|
103
93
|
background-color: var(--theme-checkbox-surface-disabled);
|
|
104
94
|
border-color: var(--theme-checkbox-border-color);
|
|
105
|
-
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.checkbox[data-disabled="true"][data-state="checked"] .checkbox-surface,
|
|
98
|
+
.checkbox[data-disabled="true"][data-state="indeterminate"] .checkbox-surface {
|
|
99
|
+
background-color: var(--theme-checkbox-surface-selected-disabled);
|
|
100
|
+
border-color: var(--theme-checkbox-border-color);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.checkbox-indicator {
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
color: var(--theme-checkbox-icon-default);
|
|
108
|
+
transition: color 0.15s ease;
|
|
109
|
+
|
|
110
|
+
svg {
|
|
111
|
+
display: block;
|
|
112
|
+
inline-size: auto;
|
|
113
|
+
block-size: auto;
|
|
114
|
+
max-inline-size: 100%;
|
|
115
|
+
max-block-size: 100%;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.checkbox[data-state="checked"] .checkbox-indicator,
|
|
120
|
+
.checkbox[data-state="indeterminate"] .checkbox-indicator {
|
|
121
|
+
color: var(--theme-checkbox-icon-selected);
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
.checkbox[data-disabled="true"][data-state="checked"] .checkbox-indicator,
|
|
109
125
|
.checkbox[data-disabled="true"][data-state="indeterminate"]
|
|
110
126
|
.checkbox-indicator {
|
|
111
|
-
|
|
112
|
-
background-color: var(--theme-checkbox-surface-selected);
|
|
113
|
-
border-color: var(--theme-checkbox-border-selected);
|
|
114
|
-
opacity: var(--theme-checkbox-disabled-selected-opacity);
|
|
127
|
+
color: var(--theme-checkbox-icon-disabled-selected);
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
.checkbox-field {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { forwardRef, useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { Text } from "./Base";
|
|
4
|
+
import { InputUtilityButton } from "./InputUtilityButton";
|
|
5
|
+
import type { InputUtilityButtonClickHandler } from "./InputUtilityButton";
|
|
6
|
+
import type { InputProps } from "../../types";
|
|
7
|
+
|
|
8
|
+
const normalizeDigits = (value?: string) => (value ?? "").replace(/\D/g, "");
|
|
9
|
+
const clampLength = (length?: number) => Math.max(4, Math.min(8, length ?? 6));
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AuthCodeInput props. 이메일/휴대폰 컴포넌트와 동일한 UX로 인증코드를 입력한다.
|
|
13
|
+
* @property {string} [value] 제어형 값.
|
|
14
|
+
* @property {string} [defaultValue] 비제어 초기값.
|
|
15
|
+
* @property {(value: string) => void} [onValueChange] 값 변경 시 호출.
|
|
16
|
+
* @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
|
|
17
|
+
* @property {number} [length=6] 허용 자리수(4~8 사이로 보정).
|
|
18
|
+
* @property {(code: string) => void} [onComplete] length만큼 입력되면 호출.
|
|
19
|
+
* @property {string} [placeholder="인증코드 입력"] placeholder 텍스트.
|
|
20
|
+
* @property {ReactNode} [countdownText] 제한 시간 텍스트.
|
|
21
|
+
* @property {ReactNode} [countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨.
|
|
22
|
+
* @property {InputUtilityButtonClickHandler} [onCountdownAction] 제한 시간 연장 핸들러.
|
|
23
|
+
* @property {boolean} [countdownActionDisabled] 제한 시간 연장 버튼 disabled.
|
|
24
|
+
*/
|
|
25
|
+
export interface AuthCodeInputProps extends Omit<
|
|
26
|
+
InputProps,
|
|
27
|
+
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
28
|
+
> {
|
|
29
|
+
value?: string;
|
|
30
|
+
defaultValue?: string;
|
|
31
|
+
onValueChange?: (value: string) => void;
|
|
32
|
+
onChange?: ComponentPropsWithoutRef<"input">["onChange"];
|
|
33
|
+
length?: number;
|
|
34
|
+
onComplete?: (code: string) => void;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
countdownText?: ReactNode;
|
|
37
|
+
countdownActionLabel?: ReactNode;
|
|
38
|
+
onCountdownAction?: InputUtilityButtonClickHandler;
|
|
39
|
+
countdownActionDisabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_PLACEHOLDER = "인증코드 입력";
|
|
43
|
+
const DEFAULT_COUNTDOWN_ACTION_LABEL = "시간연장";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* AuthCodeInput — Text Input priority secondary 스타일로 구성된 인증번호 입력.
|
|
47
|
+
* @component
|
|
48
|
+
* @param {AuthCodeInputProps} props 인증코드 입력 props
|
|
49
|
+
*/
|
|
50
|
+
const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
|
|
51
|
+
(
|
|
52
|
+
{
|
|
53
|
+
value,
|
|
54
|
+
defaultValue,
|
|
55
|
+
onValueChange,
|
|
56
|
+
onChange,
|
|
57
|
+
length = 6,
|
|
58
|
+
onComplete,
|
|
59
|
+
placeholder = DEFAULT_PLACEHOLDER,
|
|
60
|
+
countdownText,
|
|
61
|
+
countdownActionLabel = DEFAULT_COUNTDOWN_ACTION_LABEL,
|
|
62
|
+
onCountdownAction,
|
|
63
|
+
countdownActionDisabled,
|
|
64
|
+
priority = "secondary",
|
|
65
|
+
right,
|
|
66
|
+
...restProps
|
|
67
|
+
},
|
|
68
|
+
forwardedRef,
|
|
69
|
+
) => {
|
|
70
|
+
const safeLength = clampLength(length);
|
|
71
|
+
const [innerValue, setInnerValue] = useState(() =>
|
|
72
|
+
normalizeDigits(defaultValue),
|
|
73
|
+
);
|
|
74
|
+
const isControlled = value !== undefined;
|
|
75
|
+
const resolvedValue = isControlled ? normalizeDigits(value) : innerValue;
|
|
76
|
+
|
|
77
|
+
const handleChange = useCallback(
|
|
78
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
79
|
+
const digits = normalizeDigits(event.currentTarget.value).slice(
|
|
80
|
+
0,
|
|
81
|
+
safeLength,
|
|
82
|
+
);
|
|
83
|
+
if (!isControlled) {
|
|
84
|
+
setInnerValue(digits);
|
|
85
|
+
}
|
|
86
|
+
if (event.currentTarget.value !== digits) {
|
|
87
|
+
event.currentTarget.value = digits;
|
|
88
|
+
}
|
|
89
|
+
onValueChange?.(digits);
|
|
90
|
+
if (digits.length === safeLength) {
|
|
91
|
+
onComplete?.(digits);
|
|
92
|
+
}
|
|
93
|
+
onChange?.(event);
|
|
94
|
+
},
|
|
95
|
+
[isControlled, onChange, onComplete, onValueChange, safeLength],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const countdownActions = useMemo(() => {
|
|
99
|
+
if (!countdownText && !onCountdownAction) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="auth-code-input__actions">
|
|
105
|
+
{countdownText ? (
|
|
106
|
+
<span className="auth-code-input__countdown">{countdownText}</span>
|
|
107
|
+
) : null}
|
|
108
|
+
{onCountdownAction ? (
|
|
109
|
+
<InputUtilityButton
|
|
110
|
+
priority="tertiary"
|
|
111
|
+
onClick={onCountdownAction}
|
|
112
|
+
disabled={countdownActionDisabled}
|
|
113
|
+
>
|
|
114
|
+
{countdownActionLabel}
|
|
115
|
+
</InputUtilityButton>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}, [
|
|
120
|
+
countdownActionDisabled,
|
|
121
|
+
countdownActionLabel,
|
|
122
|
+
countdownText,
|
|
123
|
+
onCountdownAction,
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Text
|
|
128
|
+
{...restProps}
|
|
129
|
+
ref={forwardedRef}
|
|
130
|
+
priority={priority}
|
|
131
|
+
type="text"
|
|
132
|
+
inputMode="numeric"
|
|
133
|
+
placeholder={placeholder}
|
|
134
|
+
value={resolvedValue}
|
|
135
|
+
onChange={handleChange}
|
|
136
|
+
maxLength={safeLength}
|
|
137
|
+
right={right ?? countdownActions}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
AuthCodeInput.displayName = "AuthCodeInput";
|
|
144
|
+
|
|
145
|
+
export { AuthCodeInput };
|