@uniai-fe/uds-primitives 0.0.15 → 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 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-color: var(--color-common-100);
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-indicator {
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, color 0.15s ease, opacity 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-indicator {
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-indicator,
1733
- .checkbox[data-state=indeterminate] .checkbox-indicator {
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-indicator {
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
- opacity: 1;
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
- background-color: var(--theme-checkbox-surface-selected);
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
- flex-basis: 100%;
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-element + .input-affix {
2268
- margin-left: auto;
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: inline-flex;
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
- .email-verification__countdown {
2483
- font-size: var(--font-caption-medium-size);
2484
- line-height: var(--font-caption-medium-line-height);
2485
- color: var(--theme-input-helper-color);
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 토큰 값으로 정의한다. */
@@ -2626,6 +2719,10 @@ figure.chip {
2626
2719
  --pagination-dot-size: 8px;
2627
2720
  --pagination-dot-bg: var(--color-cool-gray-85, #d2d3d7);
2628
2721
  --pagination-dot-active-bg: var(--color-primary-default, #0061ff);
2722
+ --pagination-dot-active-bg-secondary: var(
2723
+ --color-bg-surface-heavy,
2724
+ #313235
2725
+ );
2629
2726
  --pagination-carousel-height: 8px;
2630
2727
  --pagination-carousel-dot-width: 8px;
2631
2728
  --pagination-carousel-active-width: 20px;
@@ -2701,6 +2798,13 @@ figure.chip {
2701
2798
  align-items: center;
2702
2799
  }
2703
2800
 
2801
+ .pagination--variant-carousel[data-priority=secondary] {
2802
+ --pagination-dot-active-bg: var(
2803
+ --pagination-dot-active-bg-secondary,
2804
+ var(--color-secondary-strong, #ccdeff)
2805
+ );
2806
+ }
2807
+
2704
2808
  .pagination--variant-carousel .pagination-button {
2705
2809
  width: auto;
2706
2810
  height: var(--pagination-carousel-height);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -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="white" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
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="white"/>
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
- <CheckboxPrimitive.Indicator className={CHECKBOX_INDICATOR_CLASSNAME}>
50
- <IndicatorIcon aria-hidden />
51
- </CheckboxPrimitive.Indicator>
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-color: var(--color-common-100);
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-indicator {
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-indicator {
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-indicator,
97
- .checkbox[data-state="indeterminate"] .checkbox-indicator {
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-indicator {
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
- opacity: 1;
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
- // Figma 기준 disabled selected 상태는 primary 면을 유지하며 투명도로 구분한다.
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 };