@hjmds/design-contracts 1.12.1 → 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,124 @@
|
|
|
1
|
+
# SwipeActions
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: 별도 보조 기능(supplemental), 계약 함수 `validateActions`·`RowAction`(`components/interaction-adapters`), [optional adapters](../../optional-adapters.md)
|
|
9
|
+
- 스토리북: `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
목록 행 하나에 붙은 **삭제·보관 같은 행 단위 행동**을, Native에서는 행을 밀어 드러내고 Web에서는
|
|
14
|
+
행 아래 버튼으로 바로 보여 줄 때 쓴다. 스와이프 전용 기능은 없다. 같은 행동이 항상 다른 경로로도 열린다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 행을 눌러 상세로 이동, 설정 행 | [ListRow](list-row.md), [List](list.md) |
|
|
21
|
+
| 행 행동이 많거나 메뉴로 묶어야 함 | [Menu](menu.md), [ContextMenu](context-menu.md) |
|
|
22
|
+
| 순서 바꾸기 | [SortableCollection](sortable-collection.md) |
|
|
23
|
+
| 되돌릴 수 없는 삭제의 최종 확인 | 행동 처리 안에서 [AlertDialog](alert-dialog.md) |
|
|
24
|
+
| 시트·화면 닫기 스와이프 | [Sheet](sheet.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `SwipeActions` | 보조(supplemental) | `/swipe-actions` | `/swipe-actions` |
|
|
31
|
+
|
|
32
|
+
granular subpath로만 import 된다. Native는 `react-native-gesture-handler/ReanimatedSwipeable`을 import 하므로
|
|
33
|
+
optional peer `react-native-gesture-handler` 2.32.0과 Reanimated(`react-native-reanimated` ^4.5.1,
|
|
34
|
+
`react-native-worklets` ^0.10.1)를 앱에 설치하고 앱 루트의 GestureHandlerRootView·Reanimated 설정을 갖춰야 한다
|
|
35
|
+
([optional adapters](../../optional-adapters.md)의 설치 절). Web은 추가 peer가 없다.
|
|
36
|
+
|
|
37
|
+
## 최소 사용 예
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// Web
|
|
41
|
+
import { SwipeActions } from "@hjmds/react/swipe-actions";
|
|
42
|
+
|
|
43
|
+
<SwipeActions
|
|
44
|
+
label={t("inbox.rowActions", { title: item.title })}
|
|
45
|
+
actions={[{ id: "archive", label: t("inbox.archive") }, { id: "delete", label: t("inbox.delete"), intent: "danger" }]}
|
|
46
|
+
onAction={(id) => handleRowAction(item.id, id)}
|
|
47
|
+
onError={showErrorToast}
|
|
48
|
+
>
|
|
49
|
+
<InboxRow item={item} />
|
|
50
|
+
</SwipeActions>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// Native — 목록 전체가 openRowId 하나를 공유한다
|
|
55
|
+
import { SwipeActions } from "@hjmds/react-native/swipe-actions";
|
|
56
|
+
|
|
57
|
+
<SwipeActions
|
|
58
|
+
rowId={item.id}
|
|
59
|
+
openRowId={openRowId}
|
|
60
|
+
onOpenRowChange={setOpenRowId}
|
|
61
|
+
label={t("inbox.rowActions", { title: item.title })}
|
|
62
|
+
actionsLabel={t("inbox.showActions")}
|
|
63
|
+
actions={rowActions}
|
|
64
|
+
onAction={(id) => handleRowAction(item.id, id)}
|
|
65
|
+
onError={showErrorToast}
|
|
66
|
+
>
|
|
67
|
+
<InboxRow item={item} />
|
|
68
|
+
</SwipeActions>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 축과 기본값
|
|
72
|
+
|
|
73
|
+
| prop | 값 | 기본값 | 설명 |
|
|
74
|
+
| --- | --- | --- | --- |
|
|
75
|
+
| `actions` | `{ id, label, intent?: "default" \| "danger", disabled? }[]` | — (필수) | `id`는 고유, `label`이 비면 `TypeError`. `danger`는 `danger` 버튼, 나머지는 `ghost` 버튼 |
|
|
76
|
+
| `busy` | `boolean` | — | 모든 행동을 막는다. 처리 중에는 두 번째 행동을 받지 않는다(`onAction`이 끝날 때까지) |
|
|
77
|
+
| `onAction` | `(id: string) => void \| Promise<void>` | — (필수) | 누른 행동의 `id`. Promise를 돌려주면 끝날 때까지 다음 행동을 받지 않는다 |
|
|
78
|
+
| `onError` | `(error: unknown) => void` | — (필수) | `onAction`이 던지거나 reject 하면 받는다 |
|
|
79
|
+
| `rowId`(Native) | `string` | — (필수) | 데이터의 안정된 행 id |
|
|
80
|
+
| `openRowId` + `onOpenRowChange`(Native) | `string \| null`, `(id: string \| null) => void` | — (필수) | 목록 전체가 공유하는 열린 행. 닫히면 `null` |
|
|
81
|
+
| `actionsLabel`(Native) | `string` | — (필수) | 스와이프 대체 버튼 이름 |
|
|
82
|
+
| `layoutStyle`(Web) | `HjmCompositionStyleProp` | — | 루트 `group` 배치. Native에는 없다 |
|
|
83
|
+
|
|
84
|
+
## 배치
|
|
85
|
+
|
|
86
|
+
| 항목 | 값 | 근거 |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| 크기 | 목록 행 하나를 감싼다. 행 높이·여백은 `children`(보통 [ListRow](list-row.md))이 정한다. 행동 버튼은 `medium` 높이 44의 Button(`danger` 또는 `ghost`) | `react/src/swipe-actions.tsx`, `react-native/src/swipe-actions.tsx` |
|
|
89
|
+
| 간격 | 행동 버튼 사이 `spacing.xs` 8, 넘치면 줄바꿈 | 같은 두 파일 |
|
|
90
|
+
| 순서·정렬 | Web: 행 바로 아래에 버튼 줄이 항상 보인다. Native: 버튼은 행 오른쪽 뒤(RTL은 왼쪽)에 숨어 있다가 밀면 드러난다. 행 아래에는 `actionsLabel` ghost 버튼이 항상 있고, 누르거나 reduced motion이면 그 아래에 행동 버튼 줄이 펼쳐진다. 파괴 행동(`danger`)은 마지막에 두고, 2~3개를 넘으면 [Menu](menu.md)로 묶는다 | `react-native/src/swipe-actions.tsx` |
|
|
91
|
+
| 고정·스크롤 | — | — |
|
|
92
|
+
| 좁은 폭·큰 글자 | 행동 버튼 줄은 `flexWrap: wrap`으로 줄바꿈된다 | 같은 두 파일 |
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
Native(LTR) Web
|
|
96
|
+
┌────────────────────┬────────────┐ ┌──────────────────────────┐
|
|
97
|
+
│ 행(children) ← 밀기│[보관][삭제]│ │ 행(children) │
|
|
98
|
+
└────────────────────┴────────────┘ ├──────────────────────────┤
|
|
99
|
+
[ actionsLabel ] ← 스와이프 대체 경로 │ [보관] [삭제] 항상 표시 │
|
|
100
|
+
└──────────────────────────┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 꼭 지킬 것
|
|
104
|
+
|
|
105
|
+
- 행동 label·행 label은 i18n 키로 넣는다. 어떤 행동을 두는지는 제품이 정한다.
|
|
106
|
+
- Native는 `openRowId`/`onOpenRowChange`를 목록 단위 상태로 두어 한 번에 한 행만 열리게 한다.
|
|
107
|
+
- `actionsLabel` 버튼은 스와이프를 쓰지 못하는 사용자의 경로다. 숨기거나 빼지 않는다.
|
|
108
|
+
- 행 모양은 `children`이 소유한다. Web은 `layoutStyle`로 바깥 배치만 하고, Native는 스타일·`layoutStyle` prop이 없다.
|
|
109
|
+
|
|
110
|
+
## 플랫폼 차이
|
|
111
|
+
|
|
112
|
+
| 항목 | Web | Native |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| 표시 방식 | 행 아래 버튼을 항상 표시(`role="group"`) | 스와이프로 드러냄 + `actionsLabel` 버튼으로 펼침 |
|
|
115
|
+
| 필수 prop | `label`·`actions`·`onAction`·`onError` | 더해 `rowId`·`openRowId`·`onOpenRowChange`·`actionsLabel` |
|
|
116
|
+
| reduced motion | 해당 없음 | 스와이프를 끄고 버튼을 펼쳐 보여 줌 |
|
|
117
|
+
| RTL | 해당 없음 | 행동을 왼쪽에서 드러냄 |
|
|
118
|
+
| 백그라운드 전환 | 해당 없음 | 열린 행을 닫음 |
|
|
119
|
+
| `layoutStyle` | 있음 | 없음 |
|
|
120
|
+
|
|
121
|
+
## 함정
|
|
122
|
+
|
|
123
|
+
- FlashList처럼 행 뷰를 재활용하는 목록에서도 `rowId`가 바뀌면 진행 중 표시와 펼친 메뉴가 초기화된다.
|
|
124
|
+
`rowId`는 데이터의 안정된 id로 넘긴다.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Switch
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [제품 채택 1.4 §설정 한 행](../../product-adoption-1.4.md), `src/component-recipes.ts`(`switchRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/스위치`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
켜고 끄는 즉시 반영되는 설정 하나에 쓴다. 알림 받기, 다크 모드, 자동 재생 같은 설정 화면의 행이
|
|
14
|
+
대표적이다. 설정 화면에서는 `presentation="row"`로 **Switch 하나가 행 전체**가 된다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 폼 제출 때 함께 보내는 동의·선택 | [Checkbox](checkbox.md) |
|
|
21
|
+
| 여러 항목을 함께 고름 | [CheckboxGroup](checkbox-group.md) |
|
|
22
|
+
| 둘 이상 중 하나를 고름 | [SegmentedControl](segmented-control.md), [RadioGroup](radio-group.md) |
|
|
23
|
+
| 버튼 모양의 켜짐/꺼짐 | [Button](button.md)의 `selected`, [ToggleGroup](toggle-group.md) |
|
|
24
|
+
| 약관 동의 묶음 | [Agreement](agreement.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `Switch` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { Switch } from "@hjmds/react/selection";
|
|
37
|
+
|
|
38
|
+
<Switch
|
|
39
|
+
presentation="row"
|
|
40
|
+
label={t("settings.push.title")}
|
|
41
|
+
description={t("settings.push.description")}
|
|
42
|
+
checked={pushEnabled}
|
|
43
|
+
onCheckedChange={setPushEnabled}
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// Native
|
|
49
|
+
import { Switch } from "@hjmds/react-native/inputs";
|
|
50
|
+
|
|
51
|
+
<Switch
|
|
52
|
+
presentation="row"
|
|
53
|
+
label={t("settings.push.title")}
|
|
54
|
+
description={t("settings.push.description")}
|
|
55
|
+
checked={pushEnabled}
|
|
56
|
+
onCheckedChange={setPushEnabled}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 축과 기본값
|
|
61
|
+
|
|
62
|
+
| prop | 값 | 기본값 | 설명 |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| `presentation` | `inline` · `row` | Web `inline`, Native `row` | 기본값이 플랫폼마다 다르다. 같은 설정 화면을 두 표면에서 맞추려면 **항상 명시**한다 |
|
|
65
|
+
| `size` | `small`(44×26) · `medium`(52×32) | `medium` | — |
|
|
66
|
+
| `checked`/`defaultChecked` | `boolean` | `defaultChecked` `false` | `checked`를 생략하면 비제어로 동작한다 |
|
|
67
|
+
| `onCheckedChange` | `(checked: boolean) => void` | — | 다음 상태를 받는다 |
|
|
68
|
+
| `labelVisibility` | `visible` · `hidden` | `visible` | `hidden`은 글자만 화면에서 숨기고 접근성 이름은 남긴다 |
|
|
69
|
+
| `description` | 문구 | — | 이름과 따로 연결한다(Web `aria-describedby`, Native `accessibilityHint` 기본값) |
|
|
70
|
+
| `disabled` | `boolean` | `false` | — |
|
|
71
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | 루트(행) 배치. Web·Native 모두 |
|
|
72
|
+
| `style`(Native) | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle` 또는 `size`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
|
|
73
|
+
|
|
74
|
+
큰 글자(공통 large-text 기준)에서 `row`는 설명 아래로 track을 내린다.
|
|
75
|
+
|
|
76
|
+
## 배치
|
|
77
|
+
|
|
78
|
+
| 항목 | 값 | 근거 |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| 크기 | 트랙 `small` 44×26(thumb 22) · `medium` 52×32(thumb 28), 안쪽 여백 2. iOS Native는 `UISwitch` 고유 크기. 루트 최소 높이 44(`control.minTouchTarget`). 행 높이는 설명이 없으면 44, Native는 설명이 있으면 68(`rowTwoLineMinHeight`, ListRow 두 줄 행과 같다). Web은 68을 고정하지 않고 내용 높이를 따른다 | `switchRecipe.sizes`·`rowMinHeight`·`rowTwoLineMinHeight`, `styles.css` `.hjm-switch`, `react-native/src/inputs.tsx` |
|
|
81
|
+
| 간격 | 이름과 트랙 사이 `spacing.sm` 12, 이름과 설명 사이 `spacing.xxs` 4. 여러 개를 쌓을 때는 [List](list.md)·[ListRow](list-row.md) 행 구분을 따르고 Switch 사이에 여백을 더하지 않는다 | `styles.css` `.hjm-switch`·`.hjm-switch__copy`, `react-native/src/inputs.tsx` |
|
|
82
|
+
| 순서·정렬 | 설정 목록에서는 `presentation="row"`로 행 전체 폭을 쓰고 이름·설명이 앞(왼쪽), 트랙이 끝(오른쪽) | `styles.css` `.hjm-switch[data-presentation="row"]` |
|
|
83
|
+
| 고정·스크롤 | — | — |
|
|
84
|
+
| 좁은 폭·큰 글자 | 글자 배율 ≥ 1.6(`largeTextThreshold`)이면 `row`는 이름·설명 아래로 트랙을 내린다 | `switchRecipe.stackedTextScale`, `styles.css` `.hjm-root[data-large-text="true"]` |
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
presentation="row" 큰 글자(≥1.6)
|
|
88
|
+
┌───────────────────────────────┐ ┌───────────────────────┐
|
|
89
|
+
│ 알림 받기 (●──) │ │ 알림 받기 │
|
|
90
|
+
│ 새 편지가 오면 알려요 │ │ 새 편지가 오면 알려요 │
|
|
91
|
+
└───────────────────────────────┘ │ (●──) │
|
|
92
|
+
↑ 이름·설명 · 간격 12 · 트랙 ↑ └───────────────────────┘
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 꼭 지킬 것
|
|
96
|
+
|
|
97
|
+
- `label`·`description`은 i18n 키로 넣는다. 설정 항목과 문구는 제품 소유, 행 구조·색·접근성은 HJM 소유다.
|
|
98
|
+
- `row`인 Switch를 다른 Pressable·button·ListRow `onPress`로 감싸지 않는다. 행 전체가 이미 하나의 switch다.
|
|
99
|
+
- 기존 [ListRow](list-row.md)의 trailing control로만 둘 때는 `labelVisibility="hidden"`과 같은 `label`을 주고,
|
|
100
|
+
ListRow에는 `onPress`를 두지 않는다.
|
|
101
|
+
- 배치는 `layoutStyle`로 한다. Native `style`은 쓰지 않는다. track·thumb 색은 recipe가 정하며 덮지 않는다.
|
|
102
|
+
- 저장 요청 중에는 `disabled`로 막는다. 비활성 상태도 켜짐/꺼짐이 구분되게 색이 바뀐다.
|
|
103
|
+
|
|
104
|
+
## 플랫폼 차이
|
|
105
|
+
|
|
106
|
+
| 항목 | Web | Native |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| 요소 | `<button role="switch">` | `Pressable`(role `switch`) 안의 RN `Switch` |
|
|
109
|
+
| `label`·`description` 타입 | `ReactNode` | `string` |
|
|
110
|
+
| 이름 재지정 | `aria-label`/`aria-labelledby` | `accessibilityLabel`/`accessibilityHint` |
|
|
111
|
+
| 꺼짐 상태 hairline | 그림(track·thumb 모두 1px inset) | 그리지 않음(RN `Switch`에 테두리 hook이 없음, `*Border` 슬롯은 Web 전용) |
|
|
112
|
+
| iOS 크기 | 해당 없음 | `UISwitch` 고유 크기를 따른다(Android는 recipe 크기) |
|
|
113
|
+
|
|
114
|
+
## 함정
|
|
115
|
+
|
|
116
|
+
- Native는 옛 `value`/`defaultValue`/`onValueChange`를 받으면 실행 중 `TypeError`를 던진다.
|
|
117
|
+
`checked`/`defaultChecked`/`onCheckedChange`를 쓴다.
|
|
118
|
+
- Native 꺼짐 상태에는 테두리가 없으므로, 어두운 카드 위에서 track이 배경과 섞이는지 실제 기기 다크 모드로 확인한다.
|
|
119
|
+
(Web은 이 hairline이 빠져 보이지 않던 일이 있었고 현재 stylesheet가 그린다.)
|
|
120
|
+
- Web은 `type="button"`이 기본이라 폼 안에서도 submit을 일으키지 않는다.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Gooey navigation](../../gooey-navigation.md), `src/component-recipes.ts`(`tabsRecipe`), `src/behaviors.ts`(`tabsBehaviorDefaults`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/탐색/탭`, `배포/컴포넌트/탐색/선택 표시가 이어지는 탭`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
같은 화면 안에서 **서로 다른 패널 여러 개 중 하나를 보여 줄 때** 쓴다. 프로필의 "게시물/좋아요",
|
|
14
|
+
설정의 "일반/알림"처럼 탭마다 패널이 따로 있고 화면 이동 없이 바뀌는 자리다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 같은 목록의 필터·보기 방식만 바꿈(패널이 하나) | [SegmentedControl](segmented-control.md) |
|
|
21
|
+
| 앱의 최상위 화면 이동 | [BottomNavigation](bottom-navigation.md), [Sidebar](sidebar.md) |
|
|
22
|
+
| 여러 개를 동시에 켜고 끔 | [ToggleGroup](toggle-group.md), [Chip](chip.md) |
|
|
23
|
+
| 순서가 있는 단계 진행 | [Steps](steps.md) |
|
|
24
|
+
| 접고 펼치는 여러 섹션 | [Accordion](accordion.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `Tabs` | 기본 | `@hjmds/react`, `/navigation` | `@hjmds/react-native`, `/navigation`, `/top-bar` |
|
|
31
|
+
| `TabPanel` | 동반(패널을 Tabs 밖에 둘 때) | `@hjmds/react`, `/navigation` | `@hjmds/react-native`, `/navigation`, `/top-bar` |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { Tabs } from "@hjmds/react/navigation";
|
|
38
|
+
|
|
39
|
+
<Tabs
|
|
40
|
+
label={t("profile.tabs")}
|
|
41
|
+
value={tab}
|
|
42
|
+
onValueChange={setTab}
|
|
43
|
+
items={[
|
|
44
|
+
{ id: "posts", label: t("profile.posts"), panel: <PostList /> },
|
|
45
|
+
{ id: "likes", label: t("profile.likes"), panel: <LikeList /> },
|
|
46
|
+
]}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Native
|
|
52
|
+
import { Tabs } from "@hjmds/react-native/navigation";
|
|
53
|
+
|
|
54
|
+
<Tabs
|
|
55
|
+
label={t("profile.tabs")}
|
|
56
|
+
defaultValue="posts"
|
|
57
|
+
layout="fitted"
|
|
58
|
+
items={[
|
|
59
|
+
{ id: "posts", label: t("profile.posts"), panel: <PostList /> },
|
|
60
|
+
{ id: "likes", label: t("profile.likes"), badge: "3", badgeAccessibilityLabel: t("profile.likesNew", { count: 3 }), panel: <LikeList /> },
|
|
61
|
+
]}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 축과 기본값
|
|
66
|
+
|
|
67
|
+
| prop | 값 | 기본값 | 설명 |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `value`+`onValueChange` / `defaultValue` | 항목 `id`, `(value: string) => void`(Native는 `Value`) | 첫 활성 항목 | controlled는 둘 다 필수. 비제어에서도 `onValueChange`로 바뀐 값을 받는다 |
|
|
70
|
+
| `items[]` | Web `{ id, label: ReactNode, panel?, renderLeading?, disabled? }`, Native는 더해 `badge?`·`badgeAccessibilityLabel?`·`panelAccessibilityLabel?`(`label`은 `string`) | — (필수) | — |
|
|
71
|
+
| `renderLeading`(항목) | `(state: { selected, disabled, color, size }) => ReactNode` | — | `color`는 Web `"currentColor"`, Native 색 문자열 |
|
|
72
|
+
| `activationMode` | `manual` · `automatic` | `manual` | `manual`은 Web에서 방향키로 포커스만 옮기고 Enter/Space로 선택 |
|
|
73
|
+
| `mountPolicy` | `active` · `visited` · `always` | `active` | `active`는 선택된 패널만 mount |
|
|
74
|
+
| `panelMode` | `keyed` · `dynamic` | `keyed` | `dynamic`은 `mountPolicy="active"`일 때만 허용 |
|
|
75
|
+
| `size` | `medium` · `small` | `medium` | 최소 높이 48 · 44 |
|
|
76
|
+
| `layout` | `content` · `fitted` | `content` | `fitted`는 폭을 나눠 채움 |
|
|
77
|
+
| `overflow` | `scroll` · `clip` | `scroll` | — |
|
|
78
|
+
| `orientation` | `horizontal` · `vertical` | `horizontal` | — |
|
|
79
|
+
| `loop` | `boolean` | `true` | — |
|
|
80
|
+
| `appearance` | `standard` · `gooey` | `standard` | `gooey`는 가로일 때만 선택 표시가 늘어나며 이동, 세로는 standard 유지 |
|
|
81
|
+
| `renderPanels` | `boolean` | `true` | `false`면 패널을 그리지 않는다. 패널을 라우터·스크롤 상태와 함께 따로 둘 때 `TabPanel`에 `tabsId`(Tabs의 `id`와 같게)·`activeValue`·`value`를 넘긴다 |
|
|
82
|
+
| `children`(Native) | `(selectedValue: Value) => ReactNode` | — | 항목 `panel` 대신 선택 값으로 패널을 그린다 |
|
|
83
|
+
| `TabPanel` | `{ tabsId, activeValue, children, mode?: "keyed", value, mountPolicy? }` 또는 `{ mode: "dynamic" }`, Native는 `label` 필수 | — | — |
|
|
84
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | Tabs·TabPanel 루트 배치. Web·Native 모두 |
|
|
85
|
+
| Native `style`·`tabListStyle`, TabPanel `style` | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle` 또는 `size`·`layout`·`overflow`·`appearance`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
|
|
86
|
+
|
|
87
|
+
## 배치
|
|
88
|
+
|
|
89
|
+
| 항목 | 값 | 근거 |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| 크기 | 탭 높이 `medium` 최소 48, `small` 최소 44(`control.minTouchTarget`). 목록 아래 구분선 1(`border` 색), 선택 밑줄 2(`stroke.strong`) | `tabsRecipe.sizes`·`indicatorHeight`, `styles.css` `.hjm-tabs__tab` |
|
|
92
|
+
| 간격 | 좌우 안쪽 여백 `medium` `spacing.md` 16 · `small` `spacing.sm` 12, 탭 사이 `spacing.xs` 8, 아이콘과 라벨 사이 `spacing.xs` 8. Web 패널 위아래 여백 `spacing.md` 16 | `tabsRecipe`, `styles.css` `.hjm-tabs__list`·`.hjm-tabs__leading`·`.hjm-tabs__panel` |
|
|
93
|
+
| 순서·정렬 | 화면 상단(TopBar 아래)이나 섹션 머리에 가로로 놓고 패널이 바로 아래. 탭이 적고(2~4) 폭을 꽉 채워야 하면 `layout="fitted"`, 많거나 라벨 길이가 다르면 `content`. 세로(`orientation="vertical"`)는 Web에서 왼쪽 목록(최소 8rem)과 오른쪽 패널을 `spacing.md` 16 간격으로 나란히 두고 구분선이 목록 오른쪽에 선다 | `styles.css` `.hjm-tabs[data-orientation="vertical"]`, `react-native/src/navigation.tsx` |
|
|
94
|
+
| 고정·스크롤 | 기본 `overflow="scroll"`로 탭 목록이 가로 스크롤한다. 목록은 스크롤 영역 안에서 고정되지 않으며 상단에 붙여야 하면 제품이 sticky 영역에 넣는다 | `tabsRecipe.overflow`, `styles.css` `.hjm-tabs__list` |
|
|
95
|
+
| 좁은 폭·큰 글자 | 라벨은 줄바꿈하지 않는다(`white-space: nowrap`). 좁은 폭·큰 글자에서는 가로 스크롤로 받는다 | `styles.css` `.hjm-tabs__tab` |
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
┌────────────────────────────────────┐
|
|
99
|
+
│ TopBar │
|
|
100
|
+
├────────────────────────────────────┤
|
|
101
|
+
│ [전체] [내 글] [저장] →(스크롤) │ ← 최소 48, 사이 8
|
|
102
|
+
│ ━━━━ │ ← 선택 밑줄 2 / 구분선 1
|
|
103
|
+
├────────────────────────────────────┤
|
|
104
|
+
│ 패널(Web 위아래 16) │
|
|
105
|
+
└────────────────────────────────────┘
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 꼭 지킬 것
|
|
109
|
+
|
|
110
|
+
- `label`(탭 목록 이름)은 필수이고 비어 있으면 `TypeError`다. 항목 라벨과 함께 i18n 키로 넣는다.
|
|
111
|
+
- `items`의 `id`는 비지 않고 겹치지 않아야 하며, 활성 항목이 하나 이상 있어야 한다(위반 시 `TypeError`).
|
|
112
|
+
Web controlled `value`가 비활성·없는 항목이면 `RangeError`다.
|
|
113
|
+
- 아이콘은 `renderLeading`으로 넣고 받은 `color`·`size`를 쓴다. 색을 직접 정하지 않는다.
|
|
114
|
+
- 탭을 바꿔도 화면 이동(뒤로 가기 대상)이 아니다. 주소에 남겨야 하면 제품이 controlled 값으로 동기화한다.
|
|
115
|
+
- 배치는 `layoutStyle`로 한다. Web 루트 `div` 속성(`className`·`style`)과 Native의 deprecated `style`·`tabListStyle`로
|
|
116
|
+
색·높이·인디케이터를 덮지 않는다.
|
|
117
|
+
|
|
118
|
+
## 플랫폼 차이
|
|
119
|
+
|
|
120
|
+
| 항목 | Web | Native |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| 항목 `label` 타입 | `ReactNode` | `string` |
|
|
123
|
+
| 항목 배지 | 없음 | `badge`, `badgeAccessibilityLabel` |
|
|
124
|
+
| 패널 접근 이름 | 탭 라벨로 연결(`aria-labelledby`) | 항목 `panelAccessibilityLabel`, `TabPanel`은 `label` 필수 |
|
|
125
|
+
| 패널 렌더 함수 | 없음 | `children(selectedValue)` |
|
|
126
|
+
| 값 타입 | `string` | 제네릭 `Value extends string` |
|
|
127
|
+
| ref | `forwardRef`(`div`) | 없음 |
|
|
128
|
+
| import 경로 | `/navigation` | `/navigation`, `/top-bar` |
|
|
129
|
+
|
|
130
|
+
## 함정
|
|
131
|
+
|
|
132
|
+
- Native `Tabs`는 예전 `options` prop을 받으면 `TypeError`("options was removed")를 던진다. `items`로 옮긴다.
|
|
133
|
+
- 외부 `TabPanel`을 쓸 때 Tabs에 `id`를 주지 않으면 Web은 생성 id를 써서 `tabsId`를 맞출 수 없다.
|
|
134
|
+
`id`를 명시하고 같은 값을 `TabPanel tabsId`에 넘긴다.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Tag
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Tag contract](../../tag.md), `src/tag.ts`(`tagRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/태그`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
반복해서 나오는 **정적 메타데이터 한 조각**에 쓴다. `좌익수`, `A등급`, `2026 시즌`처럼 누르지도,
|
|
14
|
+
고르지도, 지우지도 않는 짧은 라벨이다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 누르거나 선택·해제하는 조각(필터, 지울 수 있는 태그) | [Chip](chip.md) |
|
|
21
|
+
| 상태(새 글, 실패, 진행 중)를 알림 | [Badge](badge.md) |
|
|
22
|
+
| 아이콘·아바타 위 숫자 | [CounterBadge](counter-badge.md) |
|
|
23
|
+
| 사용자가 여러 값을 입력해 모음 | [TagsInput](tags-input.md) |
|
|
24
|
+
| 경고·위험 안내 | [Notice](notice.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `Tag` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { Tag } from "@hjmds/react/display";
|
|
37
|
+
|
|
38
|
+
<Tag tone="success">{t("player.gradeA")}</Tag>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// Native
|
|
43
|
+
import { Tag } from "@hjmds/react-native/data-display";
|
|
44
|
+
|
|
45
|
+
<Tag layoutStyle={{ marginTop: 4 }}>{t("player.position.leftField")}</Tag>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 축과 기본값
|
|
49
|
+
|
|
50
|
+
| prop | 값 | 기본값 | 설명 |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `children` | 문자열 | — (필수) | 문자열만 받는다. 비어 있으면 `TypeError` |
|
|
53
|
+
| `tone` | `neutral` · `info` · `success` · `attention` · `brand` | `neutral` | `warning`·`danger`는 없다(이유는 계약 문서) |
|
|
54
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | 바깥 여백·`alignSelf` 같은 배치. Web·Native 모두 |
|
|
55
|
+
| `accessibilityLabel`(Native) | `string` | 라벨 | — |
|
|
56
|
+
| `style`·`labelStyle`(Native) | `StyleProp` | — | deprecated — `layoutStyle` 또는 `tone`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
|
|
57
|
+
|
|
58
|
+
모양은 radius `sm` 사각형, 최소 높이 20(큰 글자에서 늘어남), caption 크기 semibold 글자다.
|
|
59
|
+
|
|
60
|
+
## 배치
|
|
61
|
+
|
|
62
|
+
| 항목 | 값 | 근거 |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| 크기 | 최소 높이 20, 테두리 1, radius `sm` 8. 누르는 대상이 아니므로 44 터치 영역을 두지 않는다 | `tagRecipe.size`, `styles.css` `.hjm-tag`, `react-native/src/data-display.tsx` |
|
|
65
|
+
| 간격 | 좌우 여백 `spacing.xxs` 4. 제목 옆이면 간격 `spacing.xs` 8, 여러 개는 가로로 `spacing.xxs` 4~`xs` 8 | `tagRecipe.size` |
|
|
66
|
+
| 순서·정렬 | 제목·행 옆에 붙는 작은 메타 표시다. 제목 옆이면 제목과 세로 가운데 정렬한다 | — |
|
|
67
|
+
| 고정·스크롤 | — | — |
|
|
68
|
+
| 좁은 폭·큰 글자 | 여러 개는 줄바꿈(`wrap`)한다. 큰 글자에서는 높이가 글자에 맞춰 늘어난다(고정 높이 금지) | `tagRecipe.size.minHeight` 주석 |
|
|
69
|
+
|
|
70
|
+
## 꼭 지킬 것
|
|
71
|
+
|
|
72
|
+
- 라벨은 i18n 키로 넣는다. 라벨 문구와 어떤 값에 어떤 tone을 줄지는 제품 소유, 색·모양은 HJM 소유다.
|
|
73
|
+
- 배치는 `layoutStyle`로만 한다. 색·radius·여백을 덮지 않는다.
|
|
74
|
+
- `onClick`/`onPress`를 붙여 누르는 Tag로 만들지 않는다. 누를 수 있어야 하면 Chip을 쓴다.
|
|
75
|
+
- 여러 Tag는 [Stack](stack.md) `axis="inline"`·`wrap`으로 나열한다.
|
|
76
|
+
|
|
77
|
+
## 플랫폼 차이
|
|
78
|
+
|
|
79
|
+
| 항목 | Web | Native |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| 접근 이름 | 보이는 글자 | 기본은 라벨, `accessibilityLabel`로 바꿀 수 있음 |
|
|
82
|
+
| 추가 style prop | `className`·`style`(HTML 속성) | `style`·`labelStyle`(deprecated) |
|
|
83
|
+
| ref | `forwardRef`(`span`) | 없음 |
|
|
84
|
+
| import 경로 | `/display` | `/data-display` |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# TagsInput
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [TagsInput](../../tags-input.md), `src/tags-input.ts`(`tagsInputRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/태그 입력`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
사용자가 **자유 입력으로 여러 값을 모으는 필드**에 쓴다. 해시태그, 초대할 사람, 검색 필터처럼
|
|
14
|
+
목록이 없거나, 후보가 있어도 목록 밖의 값을 만들 수 있는 경우다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 정해진 목록에서만 고름 | [Combobox](combobox.md), [Select](select.md) |
|
|
21
|
+
| 고정된 몇 개 선택지를 여러 개 켬 | [CheckboxGroup](checkbox-group.md), [Chip](chip.md) |
|
|
22
|
+
| 본문 중 `@사람` 언급 | [Mentions](mentions.md) |
|
|
23
|
+
| 값 하나만 입력 | [Field](field.md) |
|
|
24
|
+
| 정적 라벨 표시 | [Tag](tag.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `TagsInput` | 기본 | `@hjmds/react`, `/tags-input` | `@hjmds/react-native`, `/tags-input` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import type { TagsInputRejectionReason } from "@hjmds/design-contracts/components/tags-input";
|
|
37
|
+
import { TagsInput } from "@hjmds/react/tags-input";
|
|
38
|
+
|
|
39
|
+
// 상태 → i18n 키 상수 표. 키를 템플릿 문자열로 만들지 않는다.
|
|
40
|
+
const tagErrorKey: Record<TagsInputRejectionReason, string> = {
|
|
41
|
+
empty: "post.tagError.empty",
|
|
42
|
+
duplicate: "post.tagError.duplicate",
|
|
43
|
+
limit: "post.tagError.limit",
|
|
44
|
+
invalid: "post.tagError.invalid",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
<TagsInput
|
|
48
|
+
label={t("post.tags")}
|
|
49
|
+
tags={tags}
|
|
50
|
+
onTagsChange={setTags}
|
|
51
|
+
policy={{ maxTags: 10 }}
|
|
52
|
+
onReject={(result) => setError(result.reason ? t(tagErrorKey[result.reason]) : undefined)}
|
|
53
|
+
composeRemoveLabel={(tag) => t("post.removeTag", { tag })}
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// Native
|
|
59
|
+
import { TagsInput } from "@hjmds/react-native/tags-input";
|
|
60
|
+
|
|
61
|
+
<TagsInput
|
|
62
|
+
label={t("post.tags")}
|
|
63
|
+
tags={tags}
|
|
64
|
+
onTagsChange={setTags}
|
|
65
|
+
composeRemoveLabel={(tag) => t("post.removeTag", { tag })}
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 축과 기본값
|
|
70
|
+
|
|
71
|
+
| prop | 값 | 기본값 | 설명 |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| `tags` + `onTagsChange` / `defaultTags` | `readonly string[]`, `(tags: readonly string[]) => void` | `defaultTags` `[]` | controlled 또는 uncontrolled. 콜백은 다음 태그 배열 전체를 받는다 |
|
|
74
|
+
| `policy.allowDuplicates` | `boolean` | 막음 | 중복 허용 여부 |
|
|
75
|
+
| `policy.maxTags` · `policy.isValid` | `number` · `(value: string) => boolean` | — | 거절되면 `onReject`가 결과를 받는다 |
|
|
76
|
+
| `onReject` | `(result: { accepted: boolean, value: string, reason?: "empty" \| "duplicate" \| "limit" \| "invalid" }) => void` | — | 사유 문장은 제품이 `reason`으로 고른다 |
|
|
77
|
+
| `onDraftChange` | `(draft: string) => void` | — | 입력 중 문자. 후보 필터링에 쓴다 |
|
|
78
|
+
| `composeRemoveLabel` | `(tag: string) => string` | — (필수) | 태그 삭제 버튼 이름 |
|
|
79
|
+
| `commitKeys` | `Enter` · `Comma` · `Space` · `Blur` | `["Enter"]` | Web만. Native는 Return 하나다 |
|
|
80
|
+
| `suggestions` · `suggestionsLabel` | 제품이 거른 후보(`id`·`label`·`value?`·`disabled?`)와 목록 이름 | — | 거르려면 `onDraftChange`로 입력 중 문자를 받는다 |
|
|
81
|
+
| `placeholder`·`description`·`disabled` | `string`·`string`·`boolean` | — | `disabled`는 라벨과 입력 틀을 `fieldRecipe.disabledOpacity`(0.6)로 흐리고 `description`은 그대로 둔다([Field](field.md)) |
|
|
82
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | 필드 전체 배치. Web·Native 모두 |
|
|
83
|
+
| `style`(Native) | `StyleProp<ViewStyle>` | — | deprecated — `layoutStyle`. 개발 모드에서 한 번 경고하고 다음 major에서 제거된다 |
|
|
84
|
+
|
|
85
|
+
## 배치
|
|
86
|
+
|
|
87
|
+
| 항목 | 값 | 근거 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| 크기 | 입력 틀 최소 높이 44(필드 틀과 같음), 모서리 `radius.md` 12. 입력 칸 최소 Web `8ch`, Native 80. 태그 칩 높이 28, 삭제 버튼 터치 영역 44(`control.minTouchTarget`). 후보 행 최소 높이 44(두 플랫폼, Native는 미게시(1.12.1 이후). 1.12.1 Native는 28) | `tagsInputRecipe`, `.hjm-tags-input__frame`·`__remove::after`·`__suggestion` |
|
|
90
|
+
| 간격 | 라벨과 틀 사이 Web `spacing.xxs` 4, Native `spacing.xs` 8. 틀 안 태그·입력 칸 사이 `spacing.xs` 8. 이웃 필드와의 간격은 폼이 정한다 | `.hjm-tags-input`, `react-native/src/tags-input.tsx` |
|
|
91
|
+
| 순서·정렬 | 라벨 → 입력 틀 → 후보 목록 → 설명. 폼 안에서 다른 [Field](field.md)와 같은 세로 줄에 두고 폭을 꽉 채운다 | `react/src/tags-input.tsx`, `react-native/src/tags-input.tsx` |
|
|
92
|
+
| 고정·스크롤 | 후보 목록은 틀 바로 아래 문서 흐름에 펼쳐진다(떠 있는 층이 아니다). Web은 최대 높이 12rem에서 스크롤 | `.hjm-tags-input__suggestions` |
|
|
93
|
+
| 좁은 폭·큰 글자 | 태그와 입력 칸이 넘치면 줄바꿈해 틀이 세로로 자란다. 태그 글자는 잘리지 않는다(`overflow-wrap: anywhere`) | `.hjm-tags-input__frame`·`__tag` |
|
|
94
|
+
|
|
95
|
+
## 꼭 지킬 것
|
|
96
|
+
|
|
97
|
+
- `label`과 `composeRemoveLabel`은 필수다. 삭제 버튼 이름은 태그 글자를 넣은 i18n 문장으로 만든다.
|
|
98
|
+
- 거절 사유 문장("이미 있어요")은 제품이 `reason`으로 만든다. HJM은 판정만 한다.
|
|
99
|
+
- 한국어처럼 공백이 값의 일부인 도메인에서는 `Space`를 확정 키로 켜지 않는다.
|
|
100
|
+
- 후보 필터링·정렬은 제품이 한다. HJM은 받은 후보를 보여 주고 고른 값을 확정한다.
|
|
101
|
+
- 배치는 `layoutStyle`로 한다(Web `className`도 받는다). Native `style`은 deprecated다.
|
|
102
|
+
|
|
103
|
+
## 플랫폼 차이
|
|
104
|
+
|
|
105
|
+
| 항목 | Web | Native |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| 확정 키 | `commitKeys` | Return만(`commitKeys` 없음), 포커스 이탈로 확정하지 않음 |
|
|
108
|
+
| 빈 입력 Backspace | 첫 번째는 마지막 태그 선택, 두 번째에 삭제 | 해당 키 계약 없음, 태그 옆 삭제 버튼 |
|
|
109
|
+
| 후보 이동 | ArrowUp/Down(끝에서 순환) | 후보를 버튼으로 누름 |
|
|
110
|
+
| ref | `forwardRef`(`input`) | 없음 |
|
|
111
|
+
| 스타일 prop | `className`, `layoutStyle` | `layoutStyle`(`style`은 deprecated) |
|