@hjmds/design-contracts 1.12.0 → 1.13.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/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +25 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +17 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +5 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +376 -0
- package/docs/sheet.md +12 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +7 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +104 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +215 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +138 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +151 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +274 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# NumberField
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [NumberField](../../number-field.md), [DurationField](../../compound-controls.md#durationfield), `src/number-field.ts`(`numberFieldRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/숫자 입력`, `배포/컴포넌트/입력/소요 시간 입력`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
범위가 정해진 **정확한 수 하나**를 입력받을 때 쓴다. 예약 인원, 수량, 글자 수 제한처럼
|
|
14
|
+
“이 숫자 그대로”가 중요한 입력이다. 직접 타이핑과 한 단계씩 증감하는 버튼을 함께 제공한다.
|
|
15
|
+
경과 시간(정수 초)은 NumberField 세 개를 합성한 `DurationField`를 쓴다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 대략적인 값을 끌어서 고름 | [Slider](slider.md) |
|
|
22
|
+
| 전화번호·우편번호·카드번호처럼 숫자로 된 문자열 | [Field](field.md)의 TextField |
|
|
23
|
+
| 인증번호 | [OtpField](otp-field.md) |
|
|
24
|
+
| 시각(몇 시 몇 분)·날짜 | [DatePicker](date-picker.md) |
|
|
25
|
+
| 수치를 보여 주기만 함 | [Statistic](statistic.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `NumberField` | 기본 | `@hjmds/react`, `/forms`, `/number-field` | `@hjmds/react-native`, `/inputs`, `/number-field` |
|
|
32
|
+
| `DurationField` | 시·분·초 세 칸 합성(optional-extension, 추가 peer 없음) | `/duration-field` | `/duration-field` |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { NumberField } from "@hjmds/react/number-field";
|
|
39
|
+
|
|
40
|
+
<NumberField
|
|
41
|
+
label={t("booking.guests")}
|
|
42
|
+
min={1}
|
|
43
|
+
max={10}
|
|
44
|
+
value={guests}
|
|
45
|
+
onValueChange={setGuests}
|
|
46
|
+
decrementLabel={t("booking.guests.decrease")}
|
|
47
|
+
incrementLabel={t("booking.guests.increase")}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// Native
|
|
53
|
+
import { NumberField } from "@hjmds/react-native/number-field";
|
|
54
|
+
|
|
55
|
+
<NumberField
|
|
56
|
+
label={t("booking.guests")}
|
|
57
|
+
min={1}
|
|
58
|
+
max={10}
|
|
59
|
+
value={guests}
|
|
60
|
+
onValueChange={setGuests}
|
|
61
|
+
decrementLabel={t("booking.guests.decrease")}
|
|
62
|
+
incrementLabel={t("booking.guests.increase")}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Web — DurationField(Native도 같은 props, import는 `@hjmds/react-native/duration-field`). value는 정수 초
|
|
68
|
+
import { DurationField } from "@hjmds/react/duration-field";
|
|
69
|
+
|
|
70
|
+
const increaseKey = { hours: "timer.increase.hours", minutes: "timer.increase.minutes", seconds: "timer.increase.seconds" } as const;
|
|
71
|
+
const decreaseKey = { hours: "timer.decrease.hours", minutes: "timer.decrease.minutes", seconds: "timer.decrease.seconds" } as const;
|
|
72
|
+
|
|
73
|
+
<DurationField
|
|
74
|
+
value={seconds}
|
|
75
|
+
max={3 * 3600}
|
|
76
|
+
onValueChange={setSeconds}
|
|
77
|
+
labels={{
|
|
78
|
+
label: t("timer.duration"), hours: t("unit.hours"), minutes: t("unit.minutes"), seconds: t("unit.seconds"),
|
|
79
|
+
increment: (unit) => t(increaseKey[unit]), decrement: (unit) => t(decreaseKey[unit]),
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 축과 기본값
|
|
85
|
+
|
|
86
|
+
| prop | 값 | 기본값 | 설명 |
|
|
87
|
+
| --- | --- | --- | --- |
|
|
88
|
+
| `value` · `defaultValue` | `number \| null` | `null` | `null`은 아직 입력하지 않은 상태 |
|
|
89
|
+
| `onValueChange` | `(value: number \| null) => void` | — | blur 확정·증감 때 호출. 지운 채 확정하면 `null` |
|
|
90
|
+
| `min` · `max` | 숫자 | 필수 | |
|
|
91
|
+
| `step` | 양수 | 1 | |
|
|
92
|
+
| `size` | `medium` · `large` | `medium` | |
|
|
93
|
+
| `decrementLabel` · `incrementLabel` | 문자열 | 필수 | 증감 버튼의 접근성 이름 |
|
|
94
|
+
| `getValueText` | `(value: number) => string` | — | 보조기기용 값 문구(단위·통화 등). 편집 문자열은 바꾸지 않는다 |
|
|
95
|
+
| `inputMode` | `decimal` · `numeric` · `text` | 자동 | 생략하면 `min < 0`이면 `text`, 정수 step이면 `numeric`, 아니면 `decimal` |
|
|
96
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 필드 전체 배치. Web `style`은 안쪽 input에 붙는다 |
|
|
97
|
+
| `value`(DurationField) | 정수 초 | 필수 | 제어형만 |
|
|
98
|
+
| `onValueChange`(DurationField) | `(seconds: number) => void` | 필수 | 단위 하나를 바꿔도 합친 초로 넘긴다(범위로 clamp) |
|
|
99
|
+
| `labels`(DurationField) | `{ label, hours, minutes, seconds, increment: (unit) => string, decrement: (unit) => string }` | 필수 | `unit`은 `"hours" \| "minutes" \| "seconds"`. 키는 위 예처럼 상수 표로 고른다 |
|
|
100
|
+
| `min`(DurationField) | 정수 초 | 0 | |
|
|
101
|
+
| `max`(DurationField) | 정수 초 | 필수 | 1시간 미만이면 시 칸이 비활성 |
|
|
102
|
+
|
|
103
|
+
타이핑은 blur에서 clamp·step snap으로 확정되고, 증감 버튼과 ↑/↓는 즉시 확정된다.
|
|
104
|
+
|
|
105
|
+
## 배치
|
|
106
|
+
|
|
107
|
+
| 항목 | 값 | 근거 |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| 크기 | 높이 `medium` 44(`fieldFrameContract.minHeight`) · `large` 52(`control.buttonHeight.large`). 증감 버튼 각 44×44(`control.minTouchTarget`) | `numberFieldRecipe.sizes`, `.hjm-number-field__stepper` |
|
|
110
|
+
| 간격 | 라벨·입력·설명·오류 사이 `spacing.xs` 8. 값 좌우 `medium` 16(`spacing.md`) · `large` 20(`spacing.lg`). 버튼과 값은 1px 구분선. DurationField 칸 사이 `spacing.md` 16, 라벨과 칸 사이 `spacing.sm` 12 | `formSupportContract.gap`, `.hjm-number-field__input`, `duration-field.tsx` |
|
|
111
|
+
| 순서·정렬 | `[−] [ 값 ] [+]`: 감소가 시작 쪽, 증가가 끝 쪽(RTL 반전). 값은 가운데 정렬·고정폭 숫자. DurationField는 시 → 분 → 초 | `.hjm-number-field__*`, `duration-field.tsx` |
|
|
112
|
+
| 고정·스크롤 | 폼 안에서 다른 Field와 같은 열. 폭은 부모를 따르고, 짧은 값이면 `layoutStyle`/`className`으로 폭을 줄여 라벨 아래에 둔다 | — |
|
|
113
|
+
| 좁은 폭·큰 글자 | DurationField는 줄바꿈된다(Web 칸 최소 `10ch`, Native 칸 기준 폭 `spacing.xxxl` × 3 × 글자 배율) | `duration-field.tsx`(Web·Native) |
|
|
114
|
+
|
|
115
|
+
## 꼭 지킬 것
|
|
116
|
+
|
|
117
|
+
- `decrementLabel`·`incrementLabel`은 필수이며 i18n 키로 넣는다. 화면에 보이지 않아도 접근성 이름이다.
|
|
118
|
+
- 단위·통화·소수 자릿수 표시는 만들지 않는다. 보조기기용 문구가 필요하면 `getValueText`로 제품이 포맷한다.
|
|
119
|
+
- 제어형과 비제어형을 렌더 사이에 바꾸지 않는다(`value`를 넣었다 뺐다 하면 던진다).
|
|
120
|
+
- `min ≥ max`, `step ≤ 0`, 범위 밖 `value`는 던진다. DurationField도 범위 밖·정수 아닌 초를 던진다.
|
|
121
|
+
- 색·높이를 덮지 않는다. 배치는 `layoutStyle`로 한다. Native `inputStyle`·`containerStyle`은 deprecated(개발 모드 경고, 다음 major 제거) — 높이·글꼴은 `size`, 배치는 `layoutStyle`.
|
|
122
|
+
|
|
123
|
+
## 플랫폼 차이
|
|
124
|
+
|
|
125
|
+
| 항목 | Web | Native |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| 역할 | `spinbutton`, `type="text"` | 입력 + 증감 button, accessibility action |
|
|
128
|
+
| label·description·error 타입 | `ReactNode` | `string` |
|
|
129
|
+
| 추가 이름 | 없음 | `accessibilityLabel`, `accessibilityHint` |
|
|
130
|
+
| 클래스·스타일 | `className`, `inputClassName`, `layoutStyle`(필드 전체). `style`은 안쪽 input | `layoutStyle`(필드 전체). `inputStyle`·`containerStyle`은 deprecated |
|
|
131
|
+
| DurationField 배치 | `className`, `layoutStyle`(fieldset) | `layoutStyle`(미게시(1.12.1 이후), 1.12.1은 감싸는 View) |
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# OnboardingScreen
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
|
|
9
|
+
- 스토리북: `배포/화면/소개/온보딩`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
첫 실행 소개·초기 설정처럼 **몇 단계를 차례로 넘기는 화면**에 쓴다. 현재 단계의 제목·설명·본문,
|
|
14
|
+
진행 문구(“2/4”), 다음·이전·건너뛰기·완료 행동을 [ScreenLayout](screen-layout.md) 위에 조합한다.
|
|
15
|
+
단계 범위가 틀리면 렌더 중 던진다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 단계 표시만 필요(본문은 자유 배치) | [Steps](steps.md) |
|
|
22
|
+
| 화면 위에 겹치는 기능 안내 | [Tour](tour.md) |
|
|
23
|
+
| 권한 하나를 요청하는 단계 | [PermissionScreen](permission-screen.md) |
|
|
24
|
+
| 좌우로 넘기는 이미지 소개 | [Carousel](carousel.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `OnboardingScreen` | supplemental, 루트 barrel에 없음 | `/screen-flows` | `/screen-flows` |
|
|
31
|
+
|
|
32
|
+
`@hjmds/react/screen-flows`, `@hjmds/react-native/screen-flows`로만 import한다. 추가 optional peer는 없다.
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { OnboardingScreen } from "@hjmds/react/screen-flows";
|
|
39
|
+
|
|
40
|
+
<OnboardingScreen
|
|
41
|
+
steps={[
|
|
42
|
+
{ id: "welcome", title: t("onboarding.welcome.title"), description: t("onboarding.welcome.body"), content: welcomeArt },
|
|
43
|
+
{ id: "goal", title: t("onboarding.goal.title"), description: t("onboarding.goal.body"), content: goalPicker },
|
|
44
|
+
]}
|
|
45
|
+
index={index}
|
|
46
|
+
onIndexChange={setIndex}
|
|
47
|
+
nextLabel={t("common.next")}
|
|
48
|
+
backLabel={t("common.back")}
|
|
49
|
+
complete={{ label: t("onboarding.start"), onAction: finish, pending: saving }}
|
|
50
|
+
skip={{ label: t("common.skip"), onAction: finish }}
|
|
51
|
+
progressLabel={(current, total) => t("onboarding.progress", { current, total })}
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// Native — props는 Web과 같다
|
|
57
|
+
import { OnboardingScreen } from "@hjmds/react-native/screen-flows";
|
|
58
|
+
|
|
59
|
+
<OnboardingScreen
|
|
60
|
+
steps={[
|
|
61
|
+
{ id: "welcome", title: t("onboarding.welcome.title"), description: t("onboarding.welcome.body"), content: welcomeArt },
|
|
62
|
+
{ id: "goal", title: t("onboarding.goal.title"), description: t("onboarding.goal.body"), content: goalPicker },
|
|
63
|
+
]}
|
|
64
|
+
index={index}
|
|
65
|
+
onIndexChange={setIndex}
|
|
66
|
+
nextLabel={t("common.next")}
|
|
67
|
+
backLabel={t("common.back")}
|
|
68
|
+
complete={{ label: t("onboarding.start"), onAction: finish, pending: saving }}
|
|
69
|
+
skip={{ label: t("common.skip"), onAction: finish }}
|
|
70
|
+
progressLabel={(current, total) => t("onboarding.progress", { current, total })}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 제품이 공급할 것
|
|
75
|
+
|
|
76
|
+
| prop | 내용 |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `steps` | `{ id, title, description, content }[]`. 1개 이상. 제목·설명은 지역화 문자열, `content`는 단계 본문 |
|
|
79
|
+
| `index`, `onIndexChange` | 현재 단계(0부터, 제어형). 범위를 벗어나면 던진다 |
|
|
80
|
+
| `nextLabel`, `backLabel` | 다음·이전 문구. 첫 단계에는 이전이 없다 |
|
|
81
|
+
| `complete` | 마지막 단계의 주 행동 `{ label, onAction, disabled?, pending? }` |
|
|
82
|
+
| `skip` | 선택. 상단 actions 자리에 보조 버튼으로 놓인다 |
|
|
83
|
+
| `progressLabel(current, total)` | 1부터 센 현재 단계와 전체 수로 진행 문구를 만든다 |
|
|
84
|
+
|
|
85
|
+
## 배치
|
|
86
|
+
|
|
87
|
+
| 항목 | 값 | 근거 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| 크기 | 한 단계의 ScreenLayout 폭(최대 720); 다음·완료·이전은 `Button` 기본 크기 | `OnboardingScreen` |
|
|
90
|
+
| 간격 | 화면 padding `spacing.md` 16(진행 문구 notice는 좌우만); footer 다음/완료–이전 `spacing.sm` 12; 단계 본문 안 간격은 `content`(제품) 소유 | Web·Native `OnboardingScreen` `Stack gap="sm"` |
|
|
91
|
+
| 순서·정렬 | 헤더(단계 제목·설명 → 건너뛰기 ghost) → 진행 문구(caption) → 단계 `content` → footer(다음 또는 완료 primary → 이전 ghost, 첫 단계는 이전 없음) | 렌더 순서 |
|
|
92
|
+
| 고정·스크롤 | 헤더·진행 문구·footer 고정, 단계 본문 화면 스크롤 | `ScreenLayout` |
|
|
93
|
+
| 좁은 폭·큰 글자 | 제목 열 최소 폭 120 × 글자 배율, 모자라면 건너뛰기가 다음 줄로 내려간다; footer 버튼은 세로로 쌓인다 | `screenPatternRecipe.headerMinWidth` |
|
|
94
|
+
|
|
95
|
+
## 꼭 지킬 것
|
|
96
|
+
|
|
97
|
+
- 단계에서 고른 값의 저장, 완료 여부 저장, 다시 보여 주지 않기는 제품 소유다. `complete.onAction`에서 처리한다.
|
|
98
|
+
- ScreenLayout의 `header`·`state`·`contentInset` 같은 화면 props는 받지 않는다. 화면 틀을 바꿔야 하면 ScreenLayout으로 직접 조합한다.
|
|
99
|
+
배치 prop은 `layoutStyle` 하나이며 Web·Native 모두 화면 루트(ScreenLayout)에 넘긴다.
|
|
100
|
+
- 문구·일러스트·브랜드 이미지는 제품 소유다. `content`에 제품 자산을 넣는다.
|
|
101
|
+
|
|
102
|
+
## 플랫폼 차이
|
|
103
|
+
|
|
104
|
+
| 항목 | Web | Native |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| 배치 prop | `layoutStyle`(ScreenLayout 루트, 미게시(1.12.1 이후)) | `layoutStyle`(ScreenLayout 루트, 미게시(1.12.1 이후)) |
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# OtpField
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [OtpField](../../otp-field.md), `src/otp-field.ts`(`otpFieldRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/인증번호 입력`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
문자·메일로 받은 **숫자 인증번호**를 칸 모양으로 입력받을 때 쓴다. 화면에는 칸이 여러 개 보이지만
|
|
14
|
+
실제 입력은 하나라서 붙여넣기·지우기·SMS 자동 채움이 플랫폼 기본 동작으로 처리된다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 비밀번호·PIN처럼 가려야 하는 값 | [PasswordField](password-field.md) |
|
|
21
|
+
| 영문이 섞인 코드(쿠폰·초대 코드) | [Field](field.md)의 TextField (`align="center"`) |
|
|
22
|
+
| 범위가 있는 수량 | [NumberField](number-field.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `OtpField` | 기본 | `@hjmds/react`, `/forms`, `/otp-field` | `@hjmds/react-native`, `/inputs`, `/otp-field` |
|
|
29
|
+
|
|
30
|
+
## 최소 사용 예
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Web
|
|
34
|
+
import { OtpField } from "@hjmds/react/otp-field";
|
|
35
|
+
|
|
36
|
+
<OtpField
|
|
37
|
+
label={t("verify.code")}
|
|
38
|
+
length={6}
|
|
39
|
+
value={code}
|
|
40
|
+
onValueChange={setCode}
|
|
41
|
+
onComplete={submitCode}
|
|
42
|
+
busy={verifying}
|
|
43
|
+
error={codeError ? t("verify.code.invalid") : undefined}
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// Native
|
|
49
|
+
import { OtpField } from "@hjmds/react-native/otp-field";
|
|
50
|
+
|
|
51
|
+
<OtpField
|
|
52
|
+
label={t("verify.code")}
|
|
53
|
+
length={6}
|
|
54
|
+
value={code}
|
|
55
|
+
onValueChange={setCode}
|
|
56
|
+
onComplete={submitCode}
|
|
57
|
+
busy={verifying}
|
|
58
|
+
{...(codeError ? { error: t("verify.code.invalid") } : {})}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 축과 기본값
|
|
63
|
+
|
|
64
|
+
| prop | 값 | 기본값 | 설명 |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `length` | 2 이상의 정수 | 필수 | 값은 숫자만 남기고 `length`로 자른다(붙여넣은 하이픈·공백 허용) |
|
|
67
|
+
| `value` · `defaultValue` | 숫자 문자열 | 비제어 `""` | 숫자가 아니거나 `length`보다 긴 제어값은 던진다 |
|
|
68
|
+
| `onValueChange` | `(value: string) => void` | — | 정리된 숫자 문자열 |
|
|
69
|
+
| `onComplete` | `(value: string) => void` | — | 값이 `length`에 **도달하는 순간** 한 번 호출된다. 처음부터 꽉 찬 값으로 마운트하면 호출되지 않는다 |
|
|
70
|
+
| `size` | `medium` · `large` | `medium` | |
|
|
71
|
+
| `presentation` | `boxes` · `underline` | `boxes` | |
|
|
72
|
+
| `busy` | `boolean` | `false` | 서버 확인 중. 입력은 포커스를 유지한 채 읽기 전용이 된다 |
|
|
73
|
+
| `description` · `error` | Web `ReactNode` · Native `string` | — | Native는 `undefined`를 받지 않아 조건부 spread로 넘긴다(위 예) |
|
|
74
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 필드 전체(라벨·칸) 배치. Native는 1.13부터 숨은 TextInput이 아니라 바깥 frame에 붙는다. Web `style`은 안쪽 input에 붙는다 |
|
|
75
|
+
|
|
76
|
+
자동 채움은 고정이다: Web `autoComplete="one-time-code"`·`inputMode="numeric"`, Native `textContentType="oneTimeCode"`·`keyboardType="number-pad"`.
|
|
77
|
+
|
|
78
|
+
## 배치
|
|
79
|
+
|
|
80
|
+
| 항목 | 값 | 근거 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| 크기 | 칸 `medium` 44(`control.minTouchTarget`) · `large` 52(`control.buttonHeight.large`). 전체 폭 최대 칸 × `length` + 간격 × (`length` − 1)(6자리 `medium` 304), 부모가 좁으면 칸이 줄어든다 | `otpFieldRecipe.sizes`, `.hjm-otp-field__control`, Native `maxWidth` |
|
|
83
|
+
| 간격 | 칸 사이 `medium` 8(`spacing.xs`) · `large` 12(`spacing.sm`). 라벨·칸·설명·오류 사이 `spacing.xs` 8 | `otpFieldRecipe`, `formSupportContract.gap` |
|
|
84
|
+
| 순서·정렬 | 안내 문구 아래, 다시 보내기·확인 행동 위. 칸 묶음은 시작 쪽, 가운데 정렬은 부모가 한다. RTL에서도 숫자는 왼쪽→오른쪽(`direction: ltr`) | `.hjm-otp-field__control`, Native `direction: "ltr"` |
|
|
85
|
+
| 고정·스크롤 | 본문 흐름 안. `onComplete`로 자동 확인하면 확인 버튼을 생략할 수 있다. 확인 버튼을 두면 단독 화면 폼은 [BottomCTA](bottom-cta.md), Card 안 구성은 [인증번호 확인과 다시 입력](../compositions/stea-otp-verify.md)처럼 필드 아래 본문에 둔다 | — |
|
|
86
|
+
| 좁은 폭·큰 글자 | Native 칸 높이 = max(칸 크기, 줄 높이 × 글자 배율 + 위아래 `spacing.xs`) | `react-native/src/inputs.tsx`(`OtpField`) |
|
|
87
|
+
|
|
88
|
+
## 꼭 지킬 것
|
|
89
|
+
|
|
90
|
+
- 칸마다 별도 input을 만들거나 OtpField를 칸별로 쪼개 쓰지 않는다. 접근성 이름·값이 하나여야 한다([계약](../../otp-field.md)).
|
|
91
|
+
- `onComplete`에서 확인 요청을 보내고 `busy`로 잠근다. 실패하면 `error`에 지역화 문구를 넣고 값 초기화 여부는 제품이 정한다.
|
|
92
|
+
- 숫자가 아닌 `value`, `length`보다 긴 `value`를 제어값으로 넣으면 던진다.
|
|
93
|
+
- 배치는 `layoutStyle`(Web은 `className`도)로만 한다. 칸 색·테두리는 recipe 소유다.
|
|
94
|
+
|
|
95
|
+
## 플랫폼 차이
|
|
96
|
+
|
|
97
|
+
| 항목 | Web | Native |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| 이름 | `label` | `label` 또는 `accessibilityLabel`만 |
|
|
100
|
+
| 칸 스타일 통로 | 없음 | `slotStyle`, `slotTextStyle`은 deprecated(개발 모드 경고, 다음 major 제거). 외형은 `size`·`presentation` |
|
|
101
|
+
| busy 표현 | read-only + `aria-busy` | `editable={false}` + `accessibilityState.busy` |
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Pagination
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Pagination](../../pagination.md), `src/pagination.ts`(`paginationRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/탐색/페이지 이동`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
총 개수(또는 총 페이지 수)가 정해진 결과 집합에서 사용자가 **임의의 페이지로 바로 이동**해야 할 때
|
|
14
|
+
Web에서 쓴다. 검색 결과, 관리자 테이블, 기록 목록이 여기에 속한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 총량을 모르거나 계속 이어지는 피드 | [LoadMore](load-more.md) |
|
|
21
|
+
| Native 긴 목록 | [LoadMore](load-more.md), [VirtualList](virtual-list.md) (Pagination은 Web 전용) |
|
|
22
|
+
| 단계형 흐름의 이전/다음 | [Steps](steps.md) |
|
|
23
|
+
| 이미지·카드 넘기기 | [Carousel](carousel.md) |
|
|
24
|
+
|
|
25
|
+
한 목록에는 Pagination과 LoadMore 중 하나의 탐색 모델만 쓴다.
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `Pagination` | 기본 | `@hjmds/react`, `/navigation`, `/pagination` | — |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { Pagination } from "@hjmds/react/pagination";
|
|
38
|
+
|
|
39
|
+
<Pagination
|
|
40
|
+
label={t("records.pagination")}
|
|
41
|
+
descriptor={{ currentPage: page, totalCount: total, pageSize: 20 }}
|
|
42
|
+
labels={{ previous: t("pagination.previous"), next: t("pagination.next") }}
|
|
43
|
+
composeAccessibleName={({ page, totalPages, current }) =>
|
|
44
|
+
t(current ? "pagination.current" : "pagination.goTo", { page, totalPages })}
|
|
45
|
+
onPageChange={(next) => setPage(next)}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Native 예는 없다(renderer 없음).
|
|
50
|
+
|
|
51
|
+
## 축과 기본값
|
|
52
|
+
|
|
53
|
+
타입은 `@hjmds/design-contracts/components/pagination`에서 가져온다.
|
|
54
|
+
|
|
55
|
+
| prop | 값 | 기본값 | 설명 |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| `label` | 문자열 | 필수 | `<nav>` 이름. 비면 던진다 |
|
|
58
|
+
| `descriptor` | `{ currentPage, totalCount, pageSize, siblingCount?, boundaryCount? }` 또는 `{ currentPage, totalPages, siblingCount?, boundaryCount? }` | 필수 | 정확히 하나의 형태. `currentPage`는 1부터 시작하는 정수 |
|
|
59
|
+
| `siblingCount` · `boundaryCount`(descriptor 안) | 정수 | 1 | 생략된 구간은 말줄임으로 표시한다 |
|
|
60
|
+
| `labels` | `{ previous: string; next: string }` | 필수 | 이전·다음 버튼 문구 |
|
|
61
|
+
| `composeAccessibleName` | `(info: { page: number; totalPages: number; current: boolean }) => string` | 필수 | 페이지 버튼마다 부른다. 어순은 제품 i18n이 정한다 |
|
|
62
|
+
| `onPageChange` | `(page: number, reason: "previous" \| "next" \| "page") => void` | 필수 | 제품이 `currentPage`를 바꾼다 |
|
|
63
|
+
| `className` · `layoutStyle` | 문자열 · 배치 전용 style 객체 | — | 루트 배치만 |
|
|
64
|
+
|
|
65
|
+
## 배치
|
|
66
|
+
|
|
67
|
+
| 항목 | 값 | 근거 |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| 크기 | 각 칸 최소 44×44(`control.minTouchTarget`), radius `radius.md` 12 | `paginationRecipe.item`, `.hjm-pagination__item` |
|
|
70
|
+
| 간격 | 칸 안쪽 `spacing.xs` 8, 칸 사이 `spacing.xxs` 4 | `paginationRecipe.gap`, `.hjm-pagination__list` |
|
|
71
|
+
| 순서·정렬 | `[‹ 이전] [1] … [4] [5] [6] … [20] [다음 ›]`. 이전·다음은 아이콘 버튼(문구는 접근성 이름), RTL에서 화살표 반전. 정렬은 놓는 레이아웃이 정한다(관리자 테이블은 끝, 검색 결과는 가운데가 흔하다) | `src/pagination.tsx`, `.hjm-pagination__previous`·`__next` |
|
|
72
|
+
| 고정·스크롤 | 목록·테이블 **바로 아래** 한 번. 페이지를 바꾸면 목록 시작으로 스크롤·포커스를 옮기는 것은 제품이 한다 | — |
|
|
73
|
+
| 좁은 폭·큰 글자 | 폭이 모자라면 줄바꿈된다(`flex-wrap`). 좁은 폭에서는 `siblingCount`·`boundaryCount`를 0~1로 줄여 한 줄을 유지한다 | `.hjm-pagination__list` |
|
|
74
|
+
|
|
75
|
+
## 꼭 지킬 것
|
|
76
|
+
|
|
77
|
+
- `label`(nav 이름)은 비어 있으면 던진다. `labels`와 `composeAccessibleName`이 만드는 페이지 이름까지 모두 제품 i18n에서 만든다.
|
|
78
|
+
어순·조사는 제품이 조립하고, 현재 페이지 여부·총 페이지 계산은 HJM이 넘겨 준다.
|
|
79
|
+
- `currentPage`는 제어값이다. `onPageChange`에서 제품 상태를 바꾸고 데이터 요청·URL 동기화는 제품이 한다.
|
|
80
|
+
- 범위 밖 `currentPage`(0 이하, 총 페이지 초과)는 던진다. 결과 개수가 줄면 페이지를 먼저 보정한다.
|
|
81
|
+
- 페이지 크기 변경·페이지 번호 직접 입력은 없다. 필요하면 별도 컴포넌트로 옆에 합성한다.
|
|
82
|
+
- `className`·`layoutStyle`은 배치에만 쓴다.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# PasswordField
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [PasswordField](../../password-field.md), `src/password-field.ts`(`passwordFieldRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/비밀번호 입력`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
비밀번호를 입력받고, 필요할 때만 값을 눈으로 확인하게 할 때 쓴다. 로그인, 가입,
|
|
14
|
+
비밀번호 변경, 이메일 계정 연결 화면의 비밀번호 칸이 여기에 속한다.
|
|
15
|
+
|
|
16
|
+
소셜 전용 제품의 이메일·비밀번호 칸은 스토어 심사자 폼뿐이다(루트 `docs/LOGIN_SCREEN_STANDARD.md` LS-07). 서버 env와
|
|
17
|
+
`review=1`이 모두 맞을 때만 [AuthScreenLayout](auth-screen-layout.md) `main` 카드 안, 개발 버튼 다음에 제목
|
|
18
|
+
(`Text variant="label"`)과 한 줄 설명을 붙여 그린다. 제출 버튼은 제공자 버튼보다 낮은 `tone="secondary"`, `size="medium"`이다.
|
|
19
|
+
|
|
20
|
+
## 쓰지 않을 때
|
|
21
|
+
|
|
22
|
+
| 상황 | 대신 쓸 것 |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| 문자로 받은 숫자 인증번호 | [OtpField](otp-field.md) |
|
|
25
|
+
| 가릴 필요가 없는 일반 텍스트 | [Field](field.md)의 TextField |
|
|
26
|
+
| 소셜 로그인 | [AuthProviderButton](auth-provider-button.md) |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `PasswordField` | 기본 | `@hjmds/react`, `/forms`, `/password-field` | `@hjmds/react-native`, `/inputs`, `/password-field` |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { PasswordField } from "@hjmds/react/password-field";
|
|
39
|
+
|
|
40
|
+
<PasswordField
|
|
41
|
+
label={t("auth.password")}
|
|
42
|
+
autofillHint="current"
|
|
43
|
+
revealLabel={t("auth.password.show")}
|
|
44
|
+
concealLabel={t("auth.password.hide")}
|
|
45
|
+
value={password}
|
|
46
|
+
onValueChange={setPassword}
|
|
47
|
+
error={passwordError}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// Native — 로그인 폼: return 키로 아이디 → 비밀번호 → 제출(LS-10)
|
|
53
|
+
import { useRef } from "react";
|
|
54
|
+
import type { TextInput } from "react-native";
|
|
55
|
+
import { TextField } from "@hjmds/react-native/inputs";
|
|
56
|
+
import { PasswordField } from "@hjmds/react-native/password-field";
|
|
57
|
+
|
|
58
|
+
const passwordRef = useRef<TextInput>(null);
|
|
59
|
+
|
|
60
|
+
<>
|
|
61
|
+
<TextField
|
|
62
|
+
label={t("auth.email")}
|
|
63
|
+
keyboardType="email-address"
|
|
64
|
+
autoCapitalize="none"
|
|
65
|
+
textContentType="username"
|
|
66
|
+
returnKeyType="next"
|
|
67
|
+
onSubmitEditing={() => passwordRef.current?.focus()}
|
|
68
|
+
value={email}
|
|
69
|
+
onValueChange={setEmail}
|
|
70
|
+
/>
|
|
71
|
+
<PasswordField
|
|
72
|
+
ref={passwordRef}
|
|
73
|
+
label={t("auth.password")}
|
|
74
|
+
autofillHint="current"
|
|
75
|
+
revealLabel={t("auth.password.show")}
|
|
76
|
+
concealLabel={t("auth.password.hide")}
|
|
77
|
+
returnKeyType="go"
|
|
78
|
+
onSubmitEditing={() => { if (canSubmit) void submit(); }}
|
|
79
|
+
value={password}
|
|
80
|
+
onValueChange={setPassword}
|
|
81
|
+
{...(passwordError ? { error: passwordError } : {})}
|
|
82
|
+
/>
|
|
83
|
+
</>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 축과 기본값
|
|
87
|
+
|
|
88
|
+
| prop | 값 | 기본값 | 설명 |
|
|
89
|
+
| --- | --- | --- | --- |
|
|
90
|
+
| `value` · `defaultValue` | 문자열 | 비제어 `""` | |
|
|
91
|
+
| `onValueChange` | `(value: string) => void` | — | Web은 DOM `onChange`도 함께 받는다 |
|
|
92
|
+
| `autofillHint` | `current`(로그인) · `new`(가입·변경) | 필수 | Web은 `current-password`/`new-password`, Native는 iOS `textContentType` `password`/`newPassword`로 번역된다 |
|
|
93
|
+
| `revealLabel` · `concealLabel` | 문자열 | 필수 | 토글의 접근성 이름. 각각 "누르면 보임"·"누르면 숨김" 행동 문구 |
|
|
94
|
+
| `revealed` · `defaultRevealed` | `boolean` | `false` | 가림 상태(제어·비제어). 값과 독립된 축이다 |
|
|
95
|
+
| `onRevealedChange` | `(revealed: boolean) => void` | — | 토글을 누를 때 |
|
|
96
|
+
| `size` | `medium` · `large` | `medium` | |
|
|
97
|
+
| `renderToggleIcon` | `(props: { name: "visibility" \| "visibilityOff"; color; size: number; revealed: boolean; disabled: boolean }) => ReactNode` | HJM 기본 아이콘 | 토글 아이콘을 바꿀 때 쓴다. `color`는 Web `"currentColor"`, Native 테마 색 문자열 |
|
|
98
|
+
| `description` · `error` | Web `ReactNode` · Native `string` | — | 규칙 안내·검증 실패 문구 |
|
|
99
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 필드 전체 배치. Web `style`은 안쪽 input에 붙는다 |
|
|
100
|
+
| `ref` | Web `HTMLInputElement` · Native `TextInput` | — | return 키로 다음 칸 focus를 옮길 때 쓴다 |
|
|
101
|
+
|
|
102
|
+
## 배치
|
|
103
|
+
|
|
104
|
+
| 항목 | 값 | 근거 |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| 크기 | 높이 `medium` 44(`fieldFrameContract.minHeight`) · `large` 52(`control.buttonHeight.large`). 토글 44×44 원형(`control.minTouchTarget`) | `passwordFieldRecipe.sizes`, `.hjm-password-field__toggle` |
|
|
107
|
+
| 간격 | 라벨·입력·설명·오류 사이 `spacing.xs` 8. 입력 좌우 `medium` 16(`spacing.md`) · `large` 20(`spacing.lg`) | `formSupportContract.gap`, `passwordFieldRecipe.sizes` |
|
|
108
|
+
| 순서·정렬 | 아이디(이메일) Field 바로 아래. 가입·변경은 새 비밀번호 → 확인. 토글은 입력 칸 **끝**(RTL에서는 시작) 안쪽 | `.hjm-password-field__toggle` |
|
|
109
|
+
| 고정·스크롤 | 폼 흐름 안. 제출은 이 칸 아래, 같은 폼의 행동이다(Web [Form](form.md) `actions`, Native는 Form 내장 버튼 또는 `Stack` + `Button`). 로그인·심사자 폼은 AuthScreenLayout `main` 카드 안에 두고 하단에 고정하지 않는다. 그 안에서는 바깥 `ScrollView`·`Container`·`KeyboardAvoidingView`로 다시 감싸지 않는다(키보드 inset·드래그 내리기·탭 유지는 레이아웃이 가진다, LS-10) | LS-07, LS-10, `authScreenRecipe` |
|
|
110
|
+
| 좁은 폭·큰 글자 | 폭은 같은 폼의 다른 Field와 맞춘다 | — |
|
|
111
|
+
|
|
112
|
+
## 꼭 지킬 것
|
|
113
|
+
|
|
114
|
+
- `revealLabel`(가려져 있을 때 = 누르면 보임)과 `concealLabel`(보일 때 = 누르면 숨김)은 **행동** 문구로 지역화한다. “숨겨짐” 같은 상태 문구를 넣지 않는다.
|
|
115
|
+
- `autofillHint`는 화면 목적에 맞게 제품이 정한다. 로그인 화면에 `new`를 주면 OS·브라우저 자동 채움이 깨진다.
|
|
116
|
+
- `type`·`autoComplete`(Web), `secureTextEntry`·`textContentType`(Native)은 Props에서 제외돼 있다. 직접 넘기지 않는다.
|
|
117
|
+
- 비밀번호 규칙 문구는 `description`, 검증 실패는 `error`에 지역화해 넣는다. 규칙 자체는 제품 소유다.
|
|
118
|
+
- 배치는 `layoutStyle`(Web은 `className`/`fieldClassName`도)로만 한다.
|
|
119
|
+
- 토글에 눌림·선택 상태를 덧붙이지 않는다. 이름이 이미 다음 행동("비밀번호 보이기/숨기기")을 말하므로 상태를 더하면
|
|
120
|
+
"비밀번호 숨기기, 선택됨"처럼 두 답이 읽힌다(계약 [PasswordField](../../password-field.md) 2026-10-06 정정).
|
|
121
|
+
- Native 로그인 폼은 return 키를 잇는다(LS-10). 아이디 칸은 `returnKeyType="next"` + `onSubmitEditing`에서 PasswordField
|
|
122
|
+
`ref.focus()`, PasswordField는 `returnKeyType="go"` + `onSubmitEditing`에서 제출하되 버튼의 disabled 조건을 그대로 따른다.
|
|
123
|
+
Native `Form`은 밖에서 제출을 부를 수 없으므로 return 제출이 필요한 폼은 Form 대신 `Stack` + `Button`(`loading`)으로 짠다.
|
|
124
|
+
|
|
125
|
+
## 플랫폼 차이
|
|
126
|
+
|
|
127
|
+
| 항목 | Web | Native |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| 이름 | `label` | `label` 또는 `accessibilityLabel`만 |
|
|
130
|
+
| 토글 | 다음 행동을 이름으로 가진 `<button>`(상태 속성 `aria-pressed` 없음), 토글 후 선택 영역 복원 | `button` + 다음 행동 `accessibilityLabel`, `accessibilityState`는 `disabled`만(`selected` 없음) |
|
|
131
|
+
| 이벤트 | `onValueChange`와 DOM `onChange` 둘 다 | `onValueChange` |
|
|
132
|
+
| ref·나머지 props | `HTMLInputElement`, `<input>` HTML 속성(`type`·`autoComplete` 제외) | `TextInput`, RN `TextInput` props(`returnKeyType`·`onSubmitEditing` 등, `secureTextEntry`·`textContentType`·`style` 제외) |
|
|
133
|
+
|
|
134
|
+
## 함정
|
|
135
|
+
|
|
136
|
+
- Native `error`·`description`은 `string`이라 `exactOptionalPropertyTypes`에서 `undefined`를 받지 않는다(Web은 `ReactNode`라 통과).
|
|
137
|
+
값이 없을 수 있으면 위 예처럼 조건부 spread로 넘긴다.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# PermissionScreen
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
|
|
9
|
+
- 스토리북: `배포/화면/소개/권한 안내`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
카메라·위치·알림 같은 권한이 **왜 필요한지 설명하고 다음 행동을 고르게 하는** 화면에 쓴다.
|
|
14
|
+
제품이 넘긴 권한 상태에 따라 하단 주 행동 하나를 고른다.
|
|
15
|
+
|
|
16
|
+
| `status` | 주 행동 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `prompt` | `request` |
|
|
19
|
+
| `denied` | `settings` |
|
|
20
|
+
| `granted` | `continueAction` |
|
|
21
|
+
| `unavailable` | 없음(`skip`만 표시 가능) |
|
|
22
|
+
|
|
23
|
+
PermissionScreen은 **OS 권한을 요청하지도, 조회하지도 않는다.** 소스(`screen-flows.tsx`, 두 renderer)는
|
|
24
|
+
`react`·`react-native`의 `View` 외에 권한·설정 API를 import하지 않고, 버튼은 넘겨받은 `onAction`만 호출한다.
|
|
25
|
+
실제 권한 요청, 설정 앱 열기, 앱 복귀 후 권한 재조회와 `status` 갱신은 모두 제품이 한다.
|
|
26
|
+
|
|
27
|
+
## 쓰지 않을 때
|
|
28
|
+
|
|
29
|
+
| 상황 | 대신 쓸 것 |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| 화면 일부에 “권한이 꺼져 있음” 안내 | [Notice](notice.md) |
|
|
32
|
+
| 접근 제한으로 본문을 대체 | [ScreenLayout](screen-layout.md)의 `state={{ kind: "restricted", ... }}` |
|
|
33
|
+
| 사진 앨범·촬영 선택 | [PhotoSourceSheet](photo-source-sheet.md) |
|
|
34
|
+
| 여러 단계 첫 실행 소개 | [OnboardingScreen](onboarding-screen.md) |
|
|
35
|
+
|
|
36
|
+
## 공개 이름과 import
|
|
37
|
+
|
|
38
|
+
| 이름 | 역할 | Web | Native |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `PermissionScreen` | supplemental, 루트 barrel에 없음 | `/screen-flows` | `/screen-flows` |
|
|
41
|
+
|
|
42
|
+
`@hjmds/react/screen-flows`, `@hjmds/react-native/screen-flows`로만 import한다. 추가 optional peer는 없다.
|
|
43
|
+
|
|
44
|
+
## 최소 사용 예
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Web — 브라우저는 설정 화면을 열 수 없으므로 settings는 브라우저 권한 안내로 연결한다
|
|
48
|
+
import { Text } from "@hjmds/react/layout";
|
|
49
|
+
import { PermissionScreen } from "@hjmds/react/screen-flows";
|
|
50
|
+
|
|
51
|
+
<PermissionScreen
|
|
52
|
+
title={t("permission.camera.title")}
|
|
53
|
+
status={cameraStatus /* 제품이 navigator.permissions·getUserMedia 결과로 매핑 */}
|
|
54
|
+
illustration={cameraArt}
|
|
55
|
+
explanation={<Text>{t("permission.camera.why")}</Text>}
|
|
56
|
+
request={{ label: t("permission.allow"), onAction: requestCamera, pending: requesting }}
|
|
57
|
+
settings={{ label: t("permission.browserHelp"), onAction: openBrowserPermissionHelp }}
|
|
58
|
+
continueAction={{ label: t("common.continue"), onAction: goNext }}
|
|
59
|
+
skip={{ label: t("common.later"), onAction: goNext }}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// Native
|
|
65
|
+
import { Text } from "@hjmds/react-native/primitives";
|
|
66
|
+
import { PermissionScreen } from "@hjmds/react-native/screen-flows";
|
|
67
|
+
import { Linking } from "react-native";
|
|
68
|
+
|
|
69
|
+
<PermissionScreen
|
|
70
|
+
title={t("permission.camera.title")}
|
|
71
|
+
status={cameraStatus /* 제품이 OS에서 조회한 값 */}
|
|
72
|
+
illustration={cameraArt}
|
|
73
|
+
explanation={<Text>{t("permission.camera.why")}</Text>}
|
|
74
|
+
request={{ label: t("permission.allow"), onAction: requestCamera, pending: requesting }}
|
|
75
|
+
settings={{ label: t("permission.openSettings"), onAction: () => Linking.openSettings() }}
|
|
76
|
+
continueAction={{ label: t("common.continue"), onAction: goNext }}
|
|
77
|
+
skip={{ label: t("common.later"), onAction: goNext }}
|
|
78
|
+
/>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 제품이 공급할 것
|
|
82
|
+
|
|
83
|
+
| prop | 내용 |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `status` | `prompt` · `denied` · `granted` · `unavailable`. OS 조회 결과를 제품이 매핑한다 |
|
|
86
|
+
| `explanation` | 권한이 필요한 이유(필수, 지역화) |
|
|
87
|
+
| `illustration` | 선택. 제품 일러스트 |
|
|
88
|
+
| `request`, `settings`, `continueAction` | 세 행동 모두 필수 `{ label, onAction, disabled?, pending? }`. 상태에 맞는 하나만 보인다 |
|
|
89
|
+
| `skip` | 선택 보조 행동 |
|
|
90
|
+
| ScreenLayout props | `title`, `description`, `header`, `leading`, `notice`, `state` 등(`footer` 제외) |
|
|
91
|
+
|
|
92
|
+
## 배치
|
|
93
|
+
|
|
94
|
+
| 항목 | 값 | 근거 |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| 크기 | ScreenLayout 폭(최대 720); 그림 크기는 `illustration`(제품) 소유; 행동은 `Button` 기본 크기 | `PermissionScreen` |
|
|
97
|
+
| 간격 | 화면 padding `spacing.md` 16; 본문 그림–설명 `spacing.xl` 24(가운데 정렬); footer 주 행동–나중에 `spacing.sm` 12 | Web·Native `PermissionScreen` `Stack gap="xl" align="center"`·`gap="sm"` |
|
|
98
|
+
| 순서·정렬 | 헤더(제목) → 본문(그림 → 설명, 가운데) → footer(상태별 주 행동 primary → 나중에 ghost) | 렌더 순서, `resolvePermissionAction` |
|
|
99
|
+
| 고정·스크롤 | 헤더·footer 고정, 본문 화면 스크롤; OS 권한 창은 제품이 `request.onAction`에서 호출 | `ScreenLayout` |
|
|
100
|
+
| 좁은 폭·큰 글자 | 설명은 줄바꿈되고 본문이 스크롤된다; footer 버튼은 세로로 쌓인다; `unavailable`이면 주 행동 없이 `skip`만 남는다 | `PermissionScreen` |
|
|
101
|
+
|
|
102
|
+
## 꼭 지킬 것
|
|
103
|
+
|
|
104
|
+
- 설정 앱에서 돌아온 뒤 권한을 다시 조회해 `status`를 갱신한다. HJM은 앱 복귀를 감지하지 않는다.
|
|
105
|
+
- 렌더링만으로 권한 요청을 띄우지 않는다. 요청은 사용자가 `request` 버튼을 눌렀을 때만 제품이 실행한다.
|
|
106
|
+
- 알 수 없는 `status` 값은 던진다.
|
|
107
|
+
- 권한 설명 문구·스토어 심사용 사용 목적 문자열은 제품 소유다.
|