@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,125 @@
|
|
|
1
|
+
# BottomCTA
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [화면 제목과 마지막 행동](../../screen-chrome.md), recipe `bottomCtaRecipe`(`src/component-recipes.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/동작/하단 실행 버튼`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
화면의 결론 행동(저장·다음·결제·가입)을 본문 아래 하단 영역에 둘 때 쓴다. 주 행동 하나,
|
|
14
|
+
선택적인 보조 행동 하나, 그 위의 짧은 설명 한 줄을 담는다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 본문 중간의 일반 행동 | [Button](button.md) |
|
|
21
|
+
| 목록 위에 떠 있는 "새 항목" 행동 | [FloatingActionButton](floating-action-button.md) |
|
|
22
|
+
| 주 행동 아래 붙는 약관·수수료 같은 상시 조건 | [BottomInfo](bottom-info.md) |
|
|
23
|
+
| 최상위 화면 사이 이동 | [BottomNavigation](bottom-navigation.md) |
|
|
24
|
+
| 지금 생긴 오류·성공 알림 | [Notice](notice.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `BottomCTA` | 기본 | `@hjmds/react`, `/bottom-cta` | `@hjmds/react-native`, `/actions`, `/bottom-cta` |
|
|
31
|
+
|
|
32
|
+
Native의 `/bottom-cta`는 `/actions` 모듈의 alias다(번들 감소 아님).
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { BottomCTA } from "@hjmds/react/bottom-cta";
|
|
39
|
+
|
|
40
|
+
<BottomCTA
|
|
41
|
+
position="sticky"
|
|
42
|
+
description={t("checkout.feeNotice")}
|
|
43
|
+
primaryAction={{ label: t("checkout.pay"), onClick: pay, loading: paying }}
|
|
44
|
+
secondaryAction={{ label: t("common.cancel"), onClick: cancel }}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Native
|
|
50
|
+
import { BottomCTA } from "@hjmds/react-native/bottom-cta";
|
|
51
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
52
|
+
|
|
53
|
+
const insets = useSafeAreaInsets();
|
|
54
|
+
<BottomCTA
|
|
55
|
+
safeAreaBottom={insets.bottom}
|
|
56
|
+
primaryAction={{ label: t("checkout.pay"), onPress: pay, loading: paying }}
|
|
57
|
+
secondaryAction={{ label: t("common.cancel"), onPress: cancel }}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 축과 기본값
|
|
62
|
+
|
|
63
|
+
| prop | 값 | 기본값 | 설명 |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| `primaryAction` | Web `{ label, onClick, accessibilityLabel?, disabled?, loading?, loadingLabel?, size?, tone? }` · Native `{ label, onPress, accessibilityLabel?, accessibilityHint?, disabled?, loading?, loadingLabel?, size?, tone? }` | 필수, tone `primary`, size `medium` | Web `onClick: (event: MouseEvent<HTMLButtonElement>) => void`, Native `onPress: (event: GestureResponderEvent) => void` |
|
|
66
|
+
| `secondaryAction` | 같은 action 객체 또는 제품이 만든 `ReactNode` | tone `secondary` | 객체면 HJM Button으로 그린다. Web은 `label`과 `onClick`이 있어야 객체로 본다 |
|
|
67
|
+
| `description` | `string` | — | 행동 위 caption 한 줄 |
|
|
68
|
+
| `accessibilityLabel` | `string` | — | 바 전체(Web `role="group"`, Native `toolbar`)의 이름 |
|
|
69
|
+
| `safeAreaBottom` | 0 이상의 유한수 | `0` | 음수·무한대는 `RangeError` |
|
|
70
|
+
| Web `position` | `flow` · `sticky` | `flow` | fixed는 제공하지 않는다 |
|
|
71
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`은 deprecated — layoutStyle 또는 tone/토큰 |
|
|
72
|
+
| 큰 글자 | — | — | 두 행동을 세로로 쌓고 **주 행동을 위**에 둔다(`column-reverse`, Native `textScale >= 1.6`, Web `isLargeTextScale`) |
|
|
73
|
+
|
|
74
|
+
## 배치
|
|
75
|
+
|
|
76
|
+
| 항목 | 값 | 근거 |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 크기 | 화면 폭을 꽉 채우는 하단 바. 최소 높이 64(Native는 64 + `safeAreaBottom`). 행동 버튼은 칸을 꽉 채우고 높이는 Button `size`를 따른다(기본 medium 44) | `bottomCtaRecipe.minHeight`, `.hjm-bottom-cta__actions .hjm-button`, `react-native/src/actions.tsx` |
|
|
79
|
+
| 간격 | 좌우 `layout.pagePadding.regular` 20, 위 `spacing.sm` 12, 아래 `spacing.sm` 12와 하단 inset 중 큰 값(Web은 `env(safe-area-inset-bottom)`·`safeAreaBottom`도 비교). 설명↔행동, 행동 사이 `spacing.sm` 12. 위 경계는 `border.default` 1px(`stroke.default`), Native는 위로 드리우는 그림자(opacity 0.08, radius 8) | `bottomCtaRecipe`, `.hjm-bottom-cta` |
|
|
80
|
+
| 순서·정렬 | 위→아래 [설명 한 줄(caption, muted)] → 행동 줄. 행동 줄은 [보조][주] 순서로 같은 폭을 나눈다. 주 행동은 하나다 | `react/src/bottom-cta.tsx`, `react-native/src/actions.tsx` |
|
|
81
|
+
| 고정·스크롤 | 본문 스크롤 영역 아래에 붙는다. Web은 `flow`(문서 흐름 끝) 또는 `sticky`(아래 0에 붙음, z-index `layer.sticky` 100). Native는 제품 화면 레이아웃이 스크롤 영역 밖 아래에 둔다. 키보드가 열리는 폼은 `KeyboardAvoiding` 또는 `KeyboardDock`으로 감싼다 | `.hjm-bottom-cta[data-position="sticky"]`, `react-native/src/keyboard-controller.tsx` |
|
|
82
|
+
| 좁은 폭·큰 글자 | Web은 행동 칸 기준 폭 132(`control.minTouchTarget` × 3)보다 좁으면 줄바꿈한다. 큰 글자에서는 세로로 쌓고 주 행동이 위에 온다 | `.hjm-bottom-cta__actions > div`, `.hjm-bottom-cta[data-large-text="true"]` |
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
┌──────────────────────────────┐
|
|
86
|
+
│ 스크롤 본문 │
|
|
87
|
+
│ … │
|
|
88
|
+
├──────────────────────────────┤ ← border 1px (Native 그림자)
|
|
89
|
+
│ 설명 한 줄(caption) │
|
|
90
|
+
│ [ 보조 ] [ 주 행동 ] │ ← 같은 폭, 사이 spacing.sm 12
|
|
91
|
+
│ ░░ 하단 안전 영역 ░░ │ ← max(spacing.sm, inset)
|
|
92
|
+
└──────────────────────────────┘
|
|
93
|
+
큰 글자: [ 주 행동 ] / [ 보조 ] (주 행동 위)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 꼭 지킬 것
|
|
97
|
+
|
|
98
|
+
- action `label`은 i18n 키로 넣고 비우지 않는다. Web은 빈 label에 `TypeError`를 던진다.
|
|
99
|
+
- 진행 중은 action의 `loading`으로 표시한다. 버튼 위에 Spinner를 따로 겹치지 않는다.
|
|
100
|
+
- 주 행동은 하나다. 세 번째 행동이 필요하면 화면 구조를 다시 본다.
|
|
101
|
+
- 폼 제출을 BottomCTA로 옮기면 [Form](form.md)의 제출 버튼과 중복하지 않는다. Web은 Form `actions`에 submit Button을 넣지 않고,
|
|
102
|
+
Native Form은 내장 제출 버튼(`submitLabel` 필수)을 항상 그리므로 Form 대신 필드 + contracts `createFormSubmitSession`
|
|
103
|
+
(`@hjmds/design-contracts/components/form`)으로 제출 세션을 잡고 그 상태를 `primaryAction.loading`에 연결한다.
|
|
104
|
+
- 배치는 `layoutStyle`로만 한다. Native `style`은 deprecated(개발 모드 1회 경고, 다음 major 제거)다.
|
|
105
|
+
- 하단 inset을 넘긴다. Native는 자동으로 읽지 않으므로 `safeAreaBottom`이 없으면 홈 인디케이터에 붙는다.
|
|
106
|
+
- 키보드가 열리는 폼 화면(Native)은 `KeyboardAvoiding`(`/keyboard`) 또는 `KeyboardDock`(`/keyboard-controller`,
|
|
107
|
+
optional peer `react-native-keyboard-controller` 설치 필요)로 감싼다.
|
|
108
|
+
|
|
109
|
+
## 플랫폼 차이
|
|
110
|
+
|
|
111
|
+
| 항목 | Web | Native |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| 이벤트 | `onClick` | `onPress` |
|
|
114
|
+
| 위치 | `position="sticky"`로 문서 흐름 안 고정 | 제품의 화면 레이아웃이 배치 |
|
|
115
|
+
| 하단 inset | `env(safe-area-inset-bottom)`과 `safeAreaBottom` 중 큰 값 | `safeAreaBottom`(과 recipe padding 중 큰 값)만 |
|
|
116
|
+
| `loadingLabel` 타입 | `string`(접근성 이름) | `ReactNode` |
|
|
117
|
+
| `accessibilityHint` | 없음 | 있음 |
|
|
118
|
+
| root 역할 | `role="group"` | `accessibilityRole="toolbar"` |
|
|
119
|
+
| 그 밖 | HTML 속성·`className`·`style` 전달 | `testID`(`style`은 deprecated) |
|
|
120
|
+
| 빈 `label` | `TypeError` | 검사 없음(Button이 빈 이름으로 그려진다) |
|
|
121
|
+
|
|
122
|
+
## 함정
|
|
123
|
+
|
|
124
|
+
- Web `BottomCTA`의 `style`은 recipe CSS 변수 뒤에 펼쳐지므로 `--hjm-bottom-cta-*` 변수를 덮을 수 있다. 외형은 recipe 소유이므로 `style`로 변수를 바꾸지 않는다.
|
|
125
|
+
- Native는 하단 inset을 스스로 읽지 않는다. `safeAreaBottom`을 빠뜨려도 오류가 없고 홈 인디케이터에 붙어 보인다.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# BottomInfo
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [BottomInfo](../../bottom-info.md), recipe `bottomInfoRecipe`(`src/bottom-info.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/상태와 알림/하단 안내`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
주 행동 아래에 늘 붙어 있는 작은 조건 문장에 쓴다. "가입하면 약관에 동의하는 것으로 봅니다",
|
|
14
|
+
"수수료는 결제 시점에 확정됩니다" 같은 문장이다. 한 줄은 문장으로, 두 줄 이상은 목록으로 그린다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 지금 생긴 오류·성공·경고 | [Notice](notice.md) |
|
|
21
|
+
| 사용자가 직접 체크해야 하는 동의 | [Agreement](agreement.md) |
|
|
22
|
+
| 입력 하나에 딸린 설명·오류 | [Field](field.md) |
|
|
23
|
+
| 하단 행동 영역 자체 | [BottomCTA](bottom-cta.md) (`description`은 행동 위 한 줄) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `BottomInfo` | 기본 | `@hjmds/react`, `/bottom-info` | `@hjmds/react-native`, `/bottom-info` |
|
|
30
|
+
|
|
31
|
+
## 최소 사용 예
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Web
|
|
35
|
+
import { BottomInfo } from "@hjmds/react/bottom-info";
|
|
36
|
+
|
|
37
|
+
<BottomInfo
|
|
38
|
+
items={[t("signup.termsNotice"), t("signup.ageNotice")]}
|
|
39
|
+
renderItem={(item, index) => index === 0 ? <TermsSentence text={item} /> : item}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// Native
|
|
45
|
+
import { BottomInfo } from "@hjmds/react-native/bottom-info";
|
|
46
|
+
|
|
47
|
+
<BottomInfo items={[t("checkout.feeNotice")]} tone="emphasis" />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 축과 기본값
|
|
51
|
+
|
|
52
|
+
| prop | 값 | 기본값 | 설명 |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| `items` | 비지 않은 문자열 배열 | 필수 | 빈 배열·빈 문자열은 거부한다. 2개 이상이면 목록 표식이 붙는다(`listMarkerFrom: 2`) |
|
|
55
|
+
| `tone` | `muted` · `emphasis` | `muted` | 법적 고지처럼 놓치면 안 되는 문장만 `emphasis`. danger·success tone은 없다 |
|
|
56
|
+
| `renderItem` | `(item: string, index: number) => ReactNode` | — | 한 줄을 rich copy로 바꾼다(문장 안 약관 링크 등). `null`·`undefined`를 돌려주면 원래 문장을 그린다 |
|
|
57
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`은 deprecated — layoutStyle 또는 tone/토큰 |
|
|
58
|
+
|
|
59
|
+
상태가 없는 표시 컴포넌트다. 이벤트 콜백이 없다.
|
|
60
|
+
|
|
61
|
+
## 배치
|
|
62
|
+
|
|
63
|
+
| 항목 | 값 | 근거 |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| 크기 | 폭을 채우는 caption 글자 블록(`typography.caption` 11/16). 높이는 줄 수가 정한다. 누르는 요소가 아니다(문장 안 링크는 `renderItem`이 소유) | `bottomInfoRecipe.textVariant`, `.hjm-bottom-info` |
|
|
66
|
+
| 간격 | 위 `spacing.sm` 12(주 행동과의 간격), 줄 사이 `spacing.xxs` 4, 목록일 때 시작 쪽 들여쓰기 `spacing.md` 16(Web) | `bottomInfoRecipe.paddingTop`·`gap`, `.hjm-bottom-info__list` |
|
|
67
|
+
| 순서·정렬 | 주 행동 **바로 아래**. 주 행동과의 간격은 BottomInfo 자신의 위 여백(`spacing.sm` 12)이므로 둘을 감싸는 Stack에 간격을 더하지 않는다. 한 줄은 문장, 두 줄 이상은 목록 | `bottomInfoRecipe.paddingTop`·`listMarkerFrom` |
|
|
68
|
+
| 고정·스크롤 | 자체 고정이 없다. 하단에 고정한 [BottomCTA](bottom-cta.md) 아래에 둘 때는 그 바와 같은 영역(스크롤 밖)에 넣는다. BottomCTA `description`은 행동 **위** 한 줄로 자리가 다르다 | `../../bottom-info.md` |
|
|
69
|
+
| 좁은 폭·큰 글자 | 줄바꿈되고 자르지 않는다(`overflow-wrap: anywhere`) | `.hjm-bottom-info__item` |
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
┌──────────────────────────────┐
|
|
73
|
+
│ [ 가입하고 시작하기 ] │ ← 주 행동
|
|
74
|
+
│ ↕ spacing.sm 12 │
|
|
75
|
+
│ 가입하면 약관에 동의…(caption)│ ← BottomInfo 한 줄
|
|
76
|
+
│ • 수수료는 결제 시점에 확정 │ ← 2줄 이상은 목록
|
|
77
|
+
│ • 환불은 7일 이내 │
|
|
78
|
+
└──────────────────────────────┘
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 꼭 지킬 것
|
|
82
|
+
|
|
83
|
+
- 문장은 i18n 키로 넣는다. 자르지 않는다(법적 고지가 많다).
|
|
84
|
+
- 문장 안 링크의 주소·라우팅은 제품 소유다. `renderItem`으로 그 줄만 바꾼다.
|
|
85
|
+
- `items`가 React key로 쓰인다. 같은 문장을 두 번 넣지 않는다.
|
|
86
|
+
- 상태 알림 용도로 쓰지 않는다. `role="status"`가 없어 낭독되지 않는다.
|
|
87
|
+
|
|
88
|
+
## 플랫폼 차이
|
|
89
|
+
|
|
90
|
+
| 항목 | Web | Native |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| root | `<aside>`, 여러 줄이면 `<ul>/<li>` | `View` + 줄마다 `·` 표식 텍스트 |
|
|
93
|
+
| 전달 가능 속성 | HTML 속성·`className`·ref·`layoutStyle` | `layoutStyle`(`style`은 deprecated) |
|
|
94
|
+
| `renderItem` 결과 위치 | `<li>`/`<p>` 안 | `Text` 안. 인라인 텍스트(문자열·`Text`·인라인 링크)만 돌려준다 |
|
|
95
|
+
|
|
96
|
+
## 함정
|
|
97
|
+
|
|
98
|
+
- Native는 `renderItem` 결과를 `Text` 안에 넣는다. `View`를 돌려주면 Text 안 View가 되어 플랫폼마다 배치가 깨진다.
|
|
99
|
+
- 같은 문장이 두 번 들어가면 React key가 겹친다(두 플랫폼 모두 `items` 문자열이 key).
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# BottomNavigation
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [BottomNavigation](../../bottom-navigation.md), recipe `bottomNavigationRecipe`
|
|
9
|
+
- 스토리북: `배포/컴포넌트/탐색/하단 탐색`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
앱의 안정된 최상위 route(홈·검색·메시지·내 정보) 2~6개 사이를 이동하는 하단 막대에 쓴다.
|
|
14
|
+
선택 상태는 제품 router가 소유하고, 컴포넌트는 이동 의도만 알린다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 같은 화면 안에서 패널만 바꿈 | [Tabs](tabs.md) |
|
|
21
|
+
| 화면의 결론 행동 | [BottomCTA](bottom-cta.md) |
|
|
22
|
+
| 넓은 화면의 측면 탐색 | [Sidebar](sidebar.md) |
|
|
23
|
+
| 화면 제목과 뒤로 가기 | [TopBar](top-bar.md) |
|
|
24
|
+
| 몇 개 중 하나를 고르는 값 | [SegmentedControl](segmented-control.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `BottomNavigation` | 기본 | `@hjmds/react`, `/navigation` | `@hjmds/react-native`, `/navigation`, `/top-bar` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web (Next.js)
|
|
36
|
+
import { BottomNavigation } from "@hjmds/react/navigation";
|
|
37
|
+
|
|
38
|
+
<BottomNavigation
|
|
39
|
+
descriptor={{
|
|
40
|
+
accessibilityLabel: t("nav.main"),
|
|
41
|
+
selectedKey: currentRoute,
|
|
42
|
+
items: [
|
|
43
|
+
{ id: "home", label: t("nav.home"), icon: { name: "home" } },
|
|
44
|
+
{ id: "inbox", label: t("nav.inbox"), icon: { name: "notifications" },
|
|
45
|
+
badge: { count: unread, accessibilityLabel: t("nav.unread", { count: unread }) } },
|
|
46
|
+
],
|
|
47
|
+
}}
|
|
48
|
+
getHref={(item) => routes[item.id]}
|
|
49
|
+
renderLink={(props) => <Link {...props} />}
|
|
50
|
+
renderIcon={({ name, size, strokeWidth }) => <AppIcon name={name} size={size} strokeWidth={strokeWidth} />}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Native
|
|
56
|
+
import { BottomNavigation } from "@hjmds/react-native/navigation";
|
|
57
|
+
|
|
58
|
+
<BottomNavigation
|
|
59
|
+
descriptor={descriptor}
|
|
60
|
+
safeAreaBottom={insets.bottom}
|
|
61
|
+
onActivate={({ key, reason }) => reason === "reselect" ? scrollToTop(key) : navigation.navigate(key)}
|
|
62
|
+
renderIcon={({ name, color, size, strokeWidth }) => <AppIcon name={name} color={color} size={size} strokeWidth={strokeWidth} />}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 축과 기본값
|
|
67
|
+
|
|
68
|
+
| prop | 값 | 기본값 | 설명 |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `descriptor` | `{ accessibilityLabel: string; selectedKey: Key; items: { id: Key; label: string; accessibilityLabel?: string; icon: { name: IconName }; badge?: { count: number; max?: number; accessibilityLabel: string }; disabled?: boolean }[] }` | 필수 | 항목 2~6개. `selectedKey`는 router가 확정한 값 |
|
|
71
|
+
| `configuration.presentation` | `bar` · `floating` · `capsule` | `bar` | — |
|
|
72
|
+
| `configuration.distribution` | `equal` · `center-gap` | `equal` | `center-gap`은 짝수 개 항목만, `capsule`과 함께 쓰면 오류다 |
|
|
73
|
+
| `configuration.density` | `regular` · `compact` | `regular` | — |
|
|
74
|
+
| `configuration.direction` | `ltr` · `rtl` | Provider 환경(없으면 `ltr`) | — |
|
|
75
|
+
| `configuration.keyboardBehavior` | `hide` · `remain` | `hide` | 기본은 소프트 키보드가 열리면 막대를 숨긴다 |
|
|
76
|
+
| `primaryAction` | `ReactNode`(Button/IconButton) | — | `configuration` 밖의 별도 prop. 생성 같은 비목적지 행동이며 `center-gap` 또는 `capsule`과 짝짓는다 |
|
|
77
|
+
| 항목 `badge` | `{ count, max?, accessibilityLabel }` | `max` 99 | 숫자만. 점 배지는 없다. `count`가 0이면 그리지 않는다 |
|
|
78
|
+
| `onActivate` | `(activation: { key: Key; reason: "navigate" \| "reselect" }) => void` | Web 선택 · Native 필수 | 이동 의도만 알린다. 선택은 바꾸지 않는다. `reselect`는 이미 선택된 항목을 다시 누른 경우 |
|
|
79
|
+
| Native `onLongActivate` | 같은 시그니처 | — | 길게 누름(tabLongPress 등) |
|
|
80
|
+
| `renderIcon` | `(props: { item; name: IconName; selected: boolean; color: string; size: number; strokeWidth: number }) => ReactNode` | 필수 | Web `color`는 `"currentColor"`이고 `scale: number`가 더 있다 |
|
|
81
|
+
| Web `getHref` | `(item: ResolvedBottomNavigationItemDescriptor) => string` | 필수 | 빈 문자열은 `TypeError` |
|
|
82
|
+
| Web `renderLink` | `(props: AnchorHTMLAttributes & { href: string; children: ReactNode; "data-state": "idle" \| "selected" }) => ReactElement` | 일반 `<a>` | 라우터 Link를 연결한다. props를 그대로 펼친다 |
|
|
83
|
+
| Native `renderBadge` | `(props: { item; badge: { visibleLabel: string; hiddenFromAccessibility: true }; count: number; max?: number; selected: boolean }) => ReactNode` | 기본 배지 | 결과 subtree는 보조기기에서 숨겨진다 |
|
|
84
|
+
| Native `getItemTestID` | `(item) => string \| undefined` | — | — |
|
|
85
|
+
| Native `safeAreaBottom` | 0 이상 숫자 | 0 | recipe 최소 하단 padding에 더한다 |
|
|
86
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`·`surfaceStyle`·`listStyle`·`primaryActionStyle`은 deprecated — layoutStyle 또는 `configuration` |
|
|
87
|
+
|
|
88
|
+
## 배치
|
|
89
|
+
|
|
90
|
+
| 항목 | 값 | 근거 |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| 크기 | 항목 최소 `regular` 56×64 · `compact` 52×52. 표면 최대 폭 `bar` 제한 없음(화면 폭) · `floating` 384 · `capsule` 480. 모서리 `floating` `radius.xl` 24 · `capsule` `radius.full`. 아이콘·배지 자리 40×28 | `bottomNavigationRecipe.density`·`presentations`·`indicator` |
|
|
93
|
+
| 간격 | 항목 안쪽 `regular` `spacing.xs` 8 · `compact` `spacing.xxs` 4, 아이콘↔라벨 `regular` `spacing.xxs` 4 · `compact` 2. `floating`·`capsule`은 바깥 좌우 `spacing.md` 16 · 위 `spacing.xs` 8. 하단은 안전 영역 + `spacing.xs` 8(Native는 `safeAreaBottom`을 최소 padding에 더함). `center-gap` 가운데 빈칸 `control.buttonHeight.large` + `spacing.md` = 68 | `presentations`, `distributions`, `safeArea`, `.hjm-bottom-navigation` |
|
|
94
|
+
| 순서·정렬 | 항목은 논리 순서(RTL이면 뒤집힘)로 같은 폭을 나눈다. 라벨은 아이콘 아래 가운데. `primaryAction`은 막대 정가운데에 겹친다(`center-gap` 빈칸 또는 `capsule`). 배지는 아이콘 끝·위 모서리(`blockStart` −4, `inlineEnd` −8) | `.hjm-bottom-navigation__list`·`__primary-action`, `bottomNavigationRecipe.badge`·`direction` |
|
|
95
|
+
| 고정·스크롤 | Web은 화면 아래 `position: fixed`(z-index `layer.sticky` 100). 본문 마지막 내용이 막대 아래로 들어가므로 제품 레이아웃이 하단 여백을 둔다. Native는 navigator 탭 막대 자리에 제품 레이아웃이 둔다. 키보드가 열리면 기본으로 숨는다 | `.hjm-bottom-navigation`, `bottomNavigationRecipe.adaptive`·`keyboard` |
|
|
96
|
+
| 좁은 폭·큰 글자 | 라벨은 줄바꿈되고 항목 높이가 늘어난다(고정 높이 없음). 글자 배율은 최대 1.4배까지만 커진다 | `bottomNavigationRecipe.largeText`·`label.wrap` |
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
bar(기본) center-gap + primaryAction
|
|
100
|
+
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
|
101
|
+
│ 스크롤 본문(하단 여백 필요) │ │ 스크롤 본문 │
|
|
102
|
+
├──────────────────────────────┤ ├──────────────────────────────┤
|
|
103
|
+
│ (홈) (검색) (알림) (나) │ │ (홈) (검색) [+] (알림) (나)│
|
|
104
|
+
│ 홈 검색 알림 나 │ │ 홈 검색 ↑68↑ 알림 나 │
|
|
105
|
+
│ ░ 안전 영역 + spacing.xs 8 ░ │ │ ░ 안전 영역 + spacing.xs 8 ░ │
|
|
106
|
+
└──────────────────────────────┘ └──────────────────────────────┘
|
|
107
|
+
floating·capsule: 바깥 좌우 16 · 위 8 띄운 둥근 표면, 최대 폭 384 · 480
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 꼭 지킬 것
|
|
111
|
+
|
|
112
|
+
- `selectedKey`는 router가 확정한 값으로 넘긴다. `onActivate`는 의도일 뿐이며 컴포넌트가 선택을 바꾸지 않는다.
|
|
113
|
+
- 선택된 항목은 `disabled`일 수 없다. id 중복, 빈·앞뒤 공백 label, 2~6개 밖은 거부한다.
|
|
114
|
+
- icon은 `name`만 넘긴다. 크기·색·굵기는 `renderIcon`이 받은 값을 그대로 적용한다(덮어쓰기 금지).
|
|
115
|
+
- label과 badge `accessibilityLabel`은 i18n 키로 만든다. icon 이름·아이콘 세트는 제품 소유다.
|
|
116
|
+
- 생성 버튼을 항목으로 넣지 않는다. `primaryAction`으로 둔다.
|
|
117
|
+
- 배치는 `layoutStyle`로만 한다. Native의 `style`·`surfaceStyle`·`listStyle`·`primaryActionStyle`은 deprecated(개발 모드 1회 경고, 다음 major 제거)이며 외형은 `configuration`이 소유한다.
|
|
118
|
+
|
|
119
|
+
## 플랫폼 차이
|
|
120
|
+
|
|
121
|
+
| 항목 | Web | Native |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| 항목 | `aria-current` 링크(`getHref` 필수, `renderLink`로 라우터 연결) | tab(iOS는 button role) + `onActivate` 필수 |
|
|
124
|
+
| `onActivate` | 선택, 수정키 없는 왼쪽 클릭에만 | 필수, 추가로 `onLongActivate` |
|
|
125
|
+
| 배치 | CSS `position: fixed` 하단 | 제품 레이아웃이 배치 |
|
|
126
|
+
| 하단 inset | `env(safe-area-inset-bottom)` 자동 | `safeAreaBottom`(recipe 최소 padding에 더함) |
|
|
127
|
+
| 키보드 감지 | `visualViewport` 높이 | `Keyboard` 이벤트 |
|
|
128
|
+
| `renderIcon`의 `color` | `"currentColor"` | 해석된 색 문자열 |
|
|
129
|
+
| 그 밖 | `className`·`style`·HTML 속성·`layoutStyle` | `renderBadge`, `getItemTestID`, `layoutStyle`(`style`·`surfaceStyle`·`listStyle`·`primaryActionStyle`은 deprecated) |
|
|
130
|
+
|
|
131
|
+
## 함정
|
|
132
|
+
|
|
133
|
+
- Web root가 `position: fixed`라 본문 마지막 내용이 막대 아래로 들어간다. 제품 레이아웃이 하단 여백을 둔다.
|
|
134
|
+
- `renderLink` 없이 쓰면 일반 `<a>`로 그려 SPA 전환이 일어나지 않는다.
|
|
135
|
+
- Web `onActivate`는 링크 기본 이동을 막지 않는다. `renderLink`의 라우터 Link가 이동하고 `onActivate`는 계측·맨 위로 스크롤 같은 부수 동작에만 쓴다. 수정키·가운데 클릭에는 불리지 않는다.
|
|
136
|
+
- 현재 Native 내비게이션 연구 스토리(`showcase/native/src/reference-navigation-bars.tsx`)는 deprecated `style`(`paddingHorizontal: 0`)·`listStyle`(`borderRadius`)로 recipe 여백·모서리를 덮고, 제목을 `Text variant="heading"`으로 그린다. 규칙은 `configuration`·`layoutStyle`과 [Heading](heading.md)이다(스토리 수정 후보).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Breadcrumb
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Breadcrumb](../../breadcrumb.md), recipe `breadcrumbRecipe`(`src/breadcrumb.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/탐색/이동 경로`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
Web의 깊은 계층 화면에서 현재 위치까지의 경로를 보여 주고 상위 계층으로 바로 돌아가게 할 때 쓴다.
|
|
14
|
+
`구단 목록 › LG 트윈스 › 선수단`처럼 순서가 곧 계층이다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| Native 화면 | 플랫폼 back + [TopBar](top-bar.md) 제목 (Native Breadcrumb는 없다) |
|
|
21
|
+
| 같은 계층의 형제 화면 전환 | [Tabs](tabs.md) |
|
|
22
|
+
| 최상위 목적지 이동 | [BottomNavigation](bottom-navigation.md), [Sidebar](sidebar.md) |
|
|
23
|
+
| 문장 안 링크 하나 | [Link](link.md) |
|
|
24
|
+
| 단계 진행 표시 | [Steps](steps.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `Breadcrumb` | 기본 | `@hjmds/react`, `/breadcrumb`, `/navigation` | 없음 |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { Breadcrumb } from "@hjmds/react/breadcrumb";
|
|
37
|
+
|
|
38
|
+
<Breadcrumb
|
|
39
|
+
label={t("nav.breadcrumb")}
|
|
40
|
+
items={[
|
|
41
|
+
{ id: "teams", label: t("teams.title"), destination: { kind: "internal", href: "/teams" } },
|
|
42
|
+
{ id: "lg", label: team.name, destination: { kind: "internal", href: `/teams/${team.id}` } },
|
|
43
|
+
{ id: "squad", label: t("teams.squad") },
|
|
44
|
+
]}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Native renderer는 없다.
|
|
49
|
+
|
|
50
|
+
## 축과 기본값
|
|
51
|
+
|
|
52
|
+
| prop | 값 | 기본값 | 설명 |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| `label` | 문자열 | 필수 | navigation landmark 이름. 비우면 `TypeError` |
|
|
55
|
+
| `items` | `readonly { id: Id; label: string; destination?: { kind: "internal" \| "external"; href: string } }[]` | 필수 | 마지막 항목만 현재 위치이며 `destination`이 없어야 한다. 그 앞 항목은 `destination`이 필수다. 빈 trail·중복 id·빈 label도 거부한다. 항목 하나(현재 화면만)는 유효하다 |
|
|
56
|
+
| 항목 `destination` | Link의 `LinkDestination`(`internal` · `external`) | — | href 규칙은 [Link](link.md)를 따른다 |
|
|
57
|
+
| `separator` | 노드 · `null` | `›`(RTL에서 미러링) | 직접 넘긴 구분자는 뒤집지 않고, `null`이면 숨긴다. 구분자는 항상 `aria-hidden`이다 |
|
|
58
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. `style`도 받지만 외형을 덮지 않는다 |
|
|
59
|
+
|
|
60
|
+
콜백이 없다. 조상 항목은 일반 `<a href>`이고 이동은 브라우저가 한다.
|
|
61
|
+
|
|
62
|
+
## 배치
|
|
63
|
+
|
|
64
|
+
| 항목 | 값 | 근거 |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| 크기 | 내용 폭만 차지하는 한 줄 경로. 글자는 `label` 변형(12/18). 링크 높이가 44에 못 미치므로 터치 중심 화면의 주 탐색으로 쓰지 않는다 | `breadcrumbRecipe.link`·`current` |
|
|
67
|
+
| 간격 | 항목·구분자 사이 `spacing.xxs` 4. 바깥 여백이 없으므로 아래 제목·본문과의 간격은 화면의 세로 [Stack](stack.md)이 정한다 | `breadcrumbRecipe.gap`, `.hjm-breadcrumb` |
|
|
68
|
+
| 순서·정렬 | 페이지 제목 바로 위, 본문 시작 쪽 정렬. 상위 → 현재 순서이고 현재 위치(마지막)는 링크가 아니며 `semibold`로 표시한다 | `.hjm-breadcrumb__current` |
|
|
69
|
+
| 고정·스크롤 | 고정되지 않고 본문과 함께 스크롤한다 | `.hjm-breadcrumb__list` |
|
|
70
|
+
| 좁은 폭·큰 글자 | 줄바꿈된다(`flex-wrap: wrap`). 항목 label도 줄바꿈되고 구분자는 줄어들지 않는다 | `.hjm-breadcrumb__list`, `.hjm-breadcrumb__separator { flex-shrink: 0 }` |
|
|
71
|
+
|
|
72
|
+
## 꼭 지킬 것
|
|
73
|
+
|
|
74
|
+
- `label`과 항목 label은 i18n 키 또는 제품 데이터로 넣는다.
|
|
75
|
+
- 긴 경로를 `...`로 접지 않는다. 축약 축이 없고 renderer는 전체 trail을 줄바꿈해 그린다.
|
|
76
|
+
- 마지막 항목은 링크가 아니다(`aria-current="page"` 텍스트). 현재 화면에 href를 주지 않는다.
|
|
77
|
+
|
|
78
|
+
## 함정
|
|
79
|
+
|
|
80
|
+
- 조상 항목은 일반 `<a href>`로 그린다. 라우터 adapter prop(`renderLink` 등)이 없으므로
|
|
81
|
+
Next.js `Link` 같은 클라이언트 전환을 기대하지 않는다.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [버튼 라벨 줄바꿈](../../button-label.md), `src/base-recipes.ts`(`buttonRecipe`), `src/foundations.ts`(`control.buttonHeight`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/동작/버튼`, `배포/컴포넌트/동작/버튼 안에서 확인`, `배포/컴포넌트/동작/반응 선택`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
사용자가 누르면 무언가가 일어나는 텍스트 행동에 쓴다. 저장·확인·다음 같은 화면의 주 행동,
|
|
14
|
+
보조 행동, 삭제처럼 되돌리기 어려운 행동, 켜고 끄는 토글 버튼(`selected`)이 여기에 속한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 아이콘만 있는 행동 | [IconButton](icon-button.md) |
|
|
21
|
+
| 화면 하단에 고정된 주 행동 | [BottomCTA](bottom-cta.md) |
|
|
22
|
+
| 다른 페이지·URL로 이동 | [Link](link.md) (`tone="link"` 버튼은 같은 화면 안의 행동용) |
|
|
23
|
+
| 여러 선택지 중 하나를 고름 | [SegmentedControl](segmented-control.md), [ToggleGroup](toggle-group.md) |
|
|
24
|
+
| 소셜 로그인 | [AuthProviderButton](auth-provider-button.md) |
|
|
25
|
+
| 복사 | `ClipboardButton`(Web, 아래 표) |
|
|
26
|
+
| 떠 있는 주 행동 | [FloatingActionButton](floating-action-button.md) |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `Button` | 기본 | `@hjmds/react`, `/actions` | `@hjmds/react-native`, `/actions` |
|
|
33
|
+
| `ClipboardButton` | 동반(복사) | `/clipboard` | — |
|
|
34
|
+
| `InlineConfirm` | 확장(버튼 자리에서 한 번 더 확인) | `/inline-confirm` | `/inline-confirm` |
|
|
35
|
+
| `ReactionPicker` | 확장(반응 고르기) | `/reaction-picker` | `/reaction-picker` |
|
|
36
|
+
|
|
37
|
+
## 최소 사용 예
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// Web
|
|
41
|
+
import { Button } from "@hjmds/react/actions";
|
|
42
|
+
|
|
43
|
+
<Button tone="primary" loading={saving} onClick={save}>
|
|
44
|
+
{t("profile.save")}
|
|
45
|
+
</Button>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Native
|
|
50
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
51
|
+
|
|
52
|
+
<Button tone="primary" loading={saving} onPress={save} fullWidth>
|
|
53
|
+
{t("profile.save")}
|
|
54
|
+
</Button>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 축과 기본값
|
|
58
|
+
|
|
59
|
+
| prop | 값 | 기본값 | 설명 |
|
|
60
|
+
| --- | --- | --- | --- |
|
|
61
|
+
| `tone` | `primary` · `secondary` · `ghost` · `danger` · `link` | `primary` | 한 화면의 `primary`는 하나(예외는 아래 꼭 지킬 것) |
|
|
62
|
+
| `size` | `small` · `medium` · `large` | `medium` | 높이 36 · 44 · 52 |
|
|
63
|
+
| `shape` | `rounded` · `pill` | `rounded` | `radius.md` · `full` |
|
|
64
|
+
| `align` | `center` · `leading` | `center` | `leading`은 꽉 찬 폭의 행 행동 |
|
|
65
|
+
| `selected` | `boolean` | — | 주면 토글 버튼. Web `aria-pressed`, Native 접근성 state |
|
|
66
|
+
| `loading` | `boolean` | `false` | 기존 문구·아이콘을 시각적으로 숨기고 중앙 스피너 하나만 표시한다. 같은 children을 유지해 크기를 보존하고 누름을 막는다. 접근성 이름·포커스는 유지한다(Web `aria-disabled`, Native는 `disableWhileLoading`으로만 옛 disabled 동작) |
|
|
67
|
+
| Web `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | `disabled`·`loading`·`aria-disabled`이면 부르지 않는다 |
|
|
68
|
+
| Native `onPress` | `(event: GestureResponderEvent) => void` | — | 같은 조건에서 부르지 않는다 |
|
|
69
|
+
| `leading` · `trailing` | `ReactNode` | — | 라벨 앞뒤 아이콘 |
|
|
70
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치. Web 꽉 찬 폭은 `layoutStyle={{ width: "100%" }}` |
|
|
71
|
+
| Native `fullWidth` | `boolean` | `false` | 부모 폭을 채운다 |
|
|
72
|
+
| Native `loadingLabel` · `renderLoadingIndicator` | `ReactNode` · `(props: { color: string; size: "small" }) => ReactNode` | — | 로딩 중 읽기 문구·스피너 교체 |
|
|
73
|
+
| Native `growWithContent` | `boolean` | `false` | 이미지 등 사용자 콘텐츠가 recipe 높이를 넘어 늘어나게 한다. 문자열·숫자 라벨은 이 옵션 없이도 줄 수에 맞춰 늘어난다 |
|
|
74
|
+
| `ClipboardButton` | `value: string`, `labels: { idle; copied }`, `onCopy?: (value: string) => void`, `onCopyError?: (error: unknown) => void`, `feedbackDuration?`(ms, 2000) | — | Button props를 물려받는다(`tone` 기본 `secondary`, 미게시(1.12.1 이후). 1.12.1은 `primary`). 쓰는 법은 [CodeBlock](code-block.md) |
|
|
75
|
+
| `InlineConfirm` | `label`·`prompt`·`confirmLabel`·`cancelLabel`·`pendingLabel`·`successLabel`·`errorLabel`(모두 `string`), `onConfirm: () => void \| Promise<void>` | — | danger 버튼 → 같은 자리 확인 묶음. Promise가 거부되면 `errorLabel`을 alert로 보인다. Web만 `layoutStyle` |
|
|
76
|
+
| `ReactionPicker` | `label: string`, `options: readonly { id; emoji; label; count?; disabled? }[]`, `value: string \| null`, `onValueChange: (value: string \| null) => void`, `layout?: "wrap" \| "strip"`, `more?: { label; options }` | `layout` `wrap` | 제어 전용. 같은 반응을 다시 누르면 `null`. 이모지는 `strip`(대화 반응 줄)에서만 `typography.title` 18/26으로 커지고 `wrap`은 버튼 글자 크기 그대로다. `more`를 주면 끝에 `+` 토글이 생겨 카탈로그를 펼치고, 카탈로그에만 있는 이모지를 고르면 접히면서 Web 키보드 포커스가 `+` 버튼으로 간다. Web만 `layoutStyle`(루트 기본 `minWidth`보다 우선). `layout`·`more`·`layoutStyle`은 미게시(1.12.1 이후) |
|
|
77
|
+
|
|
78
|
+
## 배치
|
|
79
|
+
|
|
80
|
+
| 항목 | 값 | 근거 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| 크기 | 높이 `small` 36 · `medium` 44 · `large` 52. `small`만 hitSlop 4로 터치 영역 44 | `control.buttonHeight`, `control.buttonHitSlop` |
|
|
83
|
+
| 간격 | 나란한 버튼 사이 `spacing.sm` 12. Dialog·Sheet 행동 영역 좌우·아래 여백 `spacing.lg` 20 | `.hjm-dialog__footer` |
|
|
84
|
+
| 순서·정렬 | 가로 행동 줄은 **보조 → 주**, 끝 정렬. 세로 순서는 배치 소유자의 계약을 따른다: Dialog는 보조 → 주, Native Sheet는 주 → 보조, 좁은 AlertDialog는 주 → 보조. 본문의 짧은 선택·확정 구성은 해당 구성의 순서다. 목록 행 끝은 `small` + `ghost`/`secondary`, `primary` 금지 | `renderAction` 순서(Native overlays), `.hjm-dialog__footer` |
|
|
85
|
+
| 고정·스크롤 | 화면 맨 아래 고정 주 행동은 Button 대신 [BottomCTA](bottom-cta.md)(안전 영역·키보드 처리) | — |
|
|
86
|
+
| 좁은 폭·큰 글자 | 폭 < 600(`breakpoint.medium`)이면 AlertDialog 행동을 세로로 쌓고 주 행동이 위(DOM은 [취소][확인] 유지). 라벨은 두 줄까지, 큰 글자에서는 상한 해제 | `.hjm-alert-dialog__actions`, [버튼 라벨](../../button-label.md) |
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
대화상자·시트 하단 폭 < 600인 AlertDialog
|
|
90
|
+
┌──────────────────────────────┐ ┌──────────────────────┐
|
|
91
|
+
│ [취소] [저장] │ │ [ 삭제 ] │ ← primary(danger)
|
|
92
|
+
│ secondary ─┘ primary ─┘ │ │ [ 취소 ] │
|
|
93
|
+
└──────────────────────────────┘ └──────────────────────┘
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 꼭 지킬 것
|
|
97
|
+
|
|
98
|
+
- 라벨은 i18n 키로 넣는다. 자르지 말고 두 줄을 넘으면 카피를 고친다([라벨 정책](../../button-label.md)).
|
|
99
|
+
- 배치는 `layoutStyle`로만 한다. 색·radius·높이를 `style`/`className`으로 덮지 않는다.
|
|
100
|
+
Native에서 `style`·`labelStyle`을 넘기면 실행 중 `TypeError`가 난다.
|
|
101
|
+
- 색은 tone과 제품 테마 토큰으로 바꾼다. 버튼마다 브랜드 색을 하드코딩하지 않는다.
|
|
102
|
+
- 진행 중 상태는 `loading`으로 표시하고 같은 자리에 별도 Spinner를 겹치지 않는다. children을 빈 문자열이나 다른 길이의 진행 문구로 바꾸지 않는다. 렌더러가 문구·아이콘을 숨긴 자리에 중앙 스피너만 표시하고 기존 크기·접근성 이름을 유지한다. 2026-10-06 독립 검증에서 이 사용자 요구가 사용 지침에는 빠져 있음을 확인해 명시했다.
|
|
103
|
+
- "한 화면 `primary` 하나"의 예외는 다음과 같다. `selected`를 준 버튼은 tone과 관계없이 `buttonRecipe.states.selected`(배경 `bg`, 글자·테두리 `contentBrand`)로 칠해져 primary 채움이 아니므로 세지 않고(Web `.hjm-button[data-selected="true"]`, Native `internal/recipe-button.tsx`, 검색·작품 탐색 스토리의 필터 줄), Sheet·Dialog 안의 행동은 그 표면 안에서 primary 하나를 센다([Sheet](sheet.md) `footer`, 작품 탐색·랜딩 스토리의 시트).
|
|
104
|
+
|
|
105
|
+
- 긴 소개 화면에서 위·아래 CTA가 **동일한 행동**으로 이어지고 서로 다른 스크롤 구간에 있으면 둘 다 primary를 허용한다. 큰 글자에서 첫 CTA가 멀어지는 문제를 보완하기 위한 예외이며, 서로 다른 가입·구매 행동을 동시에 강조하는 근거로 쓰지 않는다.
|
|
106
|
+
|
|
107
|
+
## 플랫폼 차이
|
|
108
|
+
|
|
109
|
+
| 항목 | Web | Native |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| 이벤트 | `onClick` | `onPress` |
|
|
112
|
+
| 꽉 찬 폭 | `layoutStyle={{ width: "100%" }}` | `fullWidth` |
|
|
113
|
+
| 로딩 문구·스피너 교체 | 없음 | `loadingLabel`, `renderLoadingIndicator` |
|
|
114
|
+
| 내용에 맞춰 높이 증가 | CSS가 처리 | 문자열·숫자 라벨은 자동. 사용자 콘텐츠는 `growWithContent` |
|
|
115
|
+
| 기본 `type` | `"button"`(폼 submit은 `type="submit"`을 명시) | 해당 없음 |
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
- 2026-10-06 독립 지침 재구현에서 footer 순서를 모든 본문 행동에 강제하는 것으로 읽혔다. 시간 선택처럼 주 행동 다음에 초기화가 오는 구성과 AlertDialog의 좁은 폭 순서는 각각 명시된 구성 계약을 따른다.
|