@hjmds/design-contracts 1.26.1 → 1.27.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/agreement.d.ts +1 -0
- package/dist/agreement.d.ts.map +1 -1
- package/dist/agreement.js +2 -1
- package/dist/agreement.js.map +1 -1
- package/dist/auth-screen.d.ts +1 -1
- package/dist/behaviors.d.ts +1 -1
- package/dist/calendar.d.ts +1 -0
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +4 -1
- package/dist/calendar.js.map +1 -1
- package/dist/catalog.d.ts +78 -30
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +10 -5
- package/dist/catalog.js.map +1 -1
- package/dist/chart.d.ts +2 -0
- package/dist/chart.d.ts.map +1 -1
- package/dist/chart.js +1 -1
- package/dist/chart.js.map +1 -1
- package/dist/component-definitions.d.ts +1 -0
- package/dist/component-definitions.d.ts.map +1 -1
- package/dist/component-definitions.js +1 -0
- package/dist/component-definitions.js.map +1 -1
- package/dist/component-recipes.d.ts +53 -6
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +64 -10
- package/dist/component-recipes.js.map +1 -1
- package/dist/data-table.d.ts +11 -0
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +5 -0
- package/dist/data-table.js.map +1 -1
- package/dist/description-list.d.ts +3 -0
- package/dist/description-list.d.ts.map +1 -1
- package/dist/description-list.js +5 -1
- package/dist/description-list.js.map +1 -1
- package/dist/design-profile-layout.d.ts +28 -0
- package/dist/design-profile-layout.d.ts.map +1 -1
- package/dist/design-profile-layout.js +27 -3
- package/dist/design-profile-layout.js.map +1 -1
- package/dist/design-profile.d.ts.map +1 -1
- package/dist/design-profile.js +40 -8
- package/dist/design-profile.js.map +1 -1
- package/dist/field-group.d.ts +13 -0
- package/dist/field-group.d.ts.map +1 -1
- package/dist/field-group.js +17 -2
- package/dist/field-group.js.map +1 -1
- package/dist/foundations.d.ts +24 -11
- package/dist/foundations.d.ts.map +1 -1
- package/dist/foundations.js +40 -7
- package/dist/foundations.js.map +1 -1
- package/dist/heading.d.ts +5 -5
- package/dist/image-choice.d.ts +23 -0
- package/dist/image-choice.d.ts.map +1 -0
- package/dist/image-choice.js +26 -0
- package/dist/image-choice.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/masonry.d.ts +8 -0
- package/dist/masonry.d.ts.map +1 -1
- package/dist/masonry.js +12 -1
- package/dist/masonry.js.map +1 -1
- package/dist/number-field.d.ts +2 -2
- package/dist/number-field.d.ts.map +1 -1
- package/dist/number-field.js +4 -2
- package/dist/number-field.js.map +1 -1
- package/dist/qr-code-recipe.d.ts +1 -0
- package/dist/qr-code-recipe.d.ts.map +1 -1
- package/dist/qr-code-recipe.js +3 -1
- package/dist/qr-code-recipe.js.map +1 -1
- package/dist/recipes.d.ts +1 -1
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js +1 -1
- package/dist/recipes.js.map +1 -1
- package/dist/result.d.ts +9 -0
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +5 -0
- package/dist/result.js.map +1 -1
- package/dist/screen-patterns.d.ts +139 -2
- package/dist/screen-patterns.d.ts.map +1 -1
- package/dist/screen-patterns.js +140 -1
- package/dist/screen-patterns.js.map +1 -1
- package/dist/sheet.d.ts +15 -1
- package/dist/sheet.d.ts.map +1 -1
- package/dist/sheet.js +20 -1
- package/dist/sheet.js.map +1 -1
- package/dist/showcase.d.ts +27 -27
- package/dist/showcase.js +27 -27
- package/dist/showcase.js.map +1 -1
- package/dist/tag.d.ts +10 -2
- package/dist/tag.d.ts.map +1 -1
- package/dist/tag.js +3 -2
- package/dist/tag.js.map +1 -1
- package/dist/tags-input.d.ts +6 -0
- package/dist/tags-input.d.ts.map +1 -1
- package/dist/tags-input.js +3 -1
- package/dist/tags-input.js.map +1 -1
- package/dist/text-formats.d.ts +2 -2
- package/dist/top.d.ts +2 -2
- package/dist/transfer-list.d.ts +1 -1
- package/dist/transfer-list.d.ts.map +1 -1
- package/dist/transfer-list.js +2 -1
- package/dist/transfer-list.js.map +1 -1
- package/dist/upload-item.d.ts +1 -10
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +1 -2
- 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/activity-chip.md +4 -3
- package/docs/catalog-freeze.json +5 -2
- package/docs/chart.md +1 -1
- package/docs/consumer-policy.md +13 -6
- package/docs/description-list.md +4 -0
- package/docs/design-profile.md +28 -2
- package/docs/font-roles.md +1 -1
- package/docs/generated/component-maturity.md +5 -4
- package/docs/generated/renderer-evidence.json +92 -6
- package/docs/generated/renderer-evidence.md +5 -3
- package/docs/generated/showcase-manifest.json +49 -1
- package/docs/gravity-letters.md +7 -2
- package/docs/masonry.md +4 -2
- package/docs/screen-patterns.md +5 -2
- package/docs/showcase.md +7 -9
- package/docs/stable-promotion.md +13 -1
- package/docs/usage/README.md +40 -10
- package/docs/usage/SELECTION.md +30 -4
- package/docs/usage/STANDARD.md +11 -2
- package/docs/usage/components/account-switcher.md +98 -0
- package/docs/usage/components/activity-chip.md +4 -4
- package/docs/usage/components/badge.md +1 -1
- package/docs/usage/components/bezier-editor.md +99 -0
- package/docs/usage/components/bottom-cta.md +2 -1
- package/docs/usage/components/button.md +26 -12
- package/docs/usage/components/carousel.md +4 -4
- package/docs/usage/components/chart.md +1 -0
- package/docs/usage/components/chat-message.md +18 -12
- package/docs/usage/components/chat-screen.md +6 -3
- package/docs/usage/components/cohort-retention-table.md +83 -0
- package/docs/usage/components/collapsible.md +7 -2
- package/docs/usage/components/comment-thread-row.md +102 -0
- package/docs/usage/components/comment-thread-screen.md +27 -13
- package/docs/usage/components/composition-layout.md +1 -0
- package/docs/usage/components/context-menu.md +32 -15
- package/docs/usage/components/conversation-row.md +11 -4
- package/docs/usage/components/conversion-funnel.md +85 -0
- package/docs/usage/components/correlation-matrix.md +80 -0
- package/docs/usage/components/data-table.md +4 -0
- package/docs/usage/components/description-list.md +2 -1
- package/docs/usage/components/editor-screen.md +9 -5
- package/docs/usage/components/effect-surface.md +4 -2
- package/docs/usage/components/empty-state.md +2 -2
- package/docs/usage/components/event-attendance-card.md +104 -0
- package/docs/usage/components/expandable-search.md +105 -0
- package/docs/usage/components/file-picker.md +5 -0
- package/docs/usage/components/filter-recovery.md +97 -0
- package/docs/usage/components/follow-profile-card.md +100 -0
- package/docs/usage/components/heading.md +6 -2
- package/docs/usage/components/histogram-explorer.md +89 -0
- package/docs/usage/components/icon-button.md +18 -2
- package/docs/usage/components/icon-tile.md +12 -4
- package/docs/usage/components/image-choice-group.md +81 -0
- package/docs/usage/components/import-field-mapping.md +94 -0
- package/docs/usage/components/inline-edit.md +105 -0
- package/docs/usage/components/keyboard-avoidance-context.md +60 -0
- package/docs/usage/components/keyboard-avoiding.md +5 -1
- package/docs/usage/components/list-detail-screen.md +4 -4
- package/docs/usage/components/list.md +1 -1
- package/docs/usage/components/masonry.md +6 -7
- package/docs/usage/components/media-selection-screen.md +7 -5
- package/docs/usage/components/media-transport.md +104 -0
- package/docs/usage/components/message-composer.md +13 -3
- package/docs/usage/components/moderation-screen.md +5 -2
- package/docs/usage/components/notification-inbox-screen.md +3 -3
- package/docs/usage/components/notification-row.md +64 -8
- package/docs/usage/components/onboarding-screen.md +1 -1
- package/docs/usage/components/overview-screen.md +9 -3
- package/docs/usage/components/pagination.md +18 -3
- package/docs/usage/components/password-rules-field.md +90 -0
- package/docs/usage/components/permission-screen.md +1 -1
- package/docs/usage/components/photo-source-sheet.md +1 -1
- package/docs/usage/components/popover.md +6 -3
- package/docs/usage/components/pricing-choice.md +85 -0
- package/docs/usage/components/profile-summary.md +56 -8
- package/docs/usage/components/progress.md +3 -3
- package/docs/usage/components/qr-code.md +5 -9
- package/docs/usage/components/repeatable-field-group.md +92 -0
- package/docs/usage/components/reply-composer.md +83 -0
- package/docs/usage/components/reply-message.md +85 -0
- package/docs/usage/components/result.md +3 -3
- package/docs/usage/components/saved-items-screen.md +7 -5
- package/docs/usage/components/screen-layout.md +21 -10
- package/docs/usage/components/search-field.md +1 -1
- package/docs/usage/components/section.md +2 -2
- package/docs/usage/components/selection-action-bar.md +89 -0
- package/docs/usage/components/settings-screen.md +13 -11
- package/docs/usage/components/sheet.md +19 -7
- package/docs/usage/components/shortcut-recorder.md +77 -0
- package/docs/usage/components/split-action.md +94 -0
- package/docs/usage/components/streaming-answer.md +94 -0
- package/docs/usage/components/switch.md +6 -2
- package/docs/usage/components/table-column-settings.md +88 -0
- package/docs/usage/components/target-summary.md +79 -0
- package/docs/usage/components/text.md +11 -5
- package/docs/usage/components/toggle-feedback-button.md +94 -0
- package/docs/usage/components/top-bar.md +8 -12
- package/docs/usage/components/transfer-task.md +87 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +18 -14
- package/docs/usage/compositions/action-recovery-save.md +18 -14
- package/docs/usage/compositions/action-recovery-undo.md +18 -14
- package/docs/usage/compositions/activity-status.md +1 -1
- package/docs/usage/compositions/adaptive-content.md +2 -1
- package/docs/usage/compositions/collection-detail.md +6 -5
- package/docs/usage/compositions/collection-editing.md +13 -6
- package/docs/usage/compositions/command-records.md +5 -4
- package/docs/usage/compositions/communication.md +7 -5
- package/docs/usage/compositions/compound-controls.md +7 -3
- package/docs/usage/compositions/content-transition-comparison.md +10 -5
- package/docs/usage/compositions/data-layouts.md +12 -16
- package/docs/usage/compositions/data-workflows.md +7 -4
- package/docs/usage/compositions/date-time-selection.md +5 -5
- package/docs/usage/compositions/design-profile-comparison.md +22 -6
- package/docs/usage/compositions/disclosure.md +14 -9
- package/docs/usage/compositions/document-resource.md +1 -1
- package/docs/usage/compositions/environment-matrix.md +16 -12
- package/docs/usage/compositions/expo-interactions.md +9 -8
- package/docs/usage/compositions/family-drawer.md +9 -5
- package/docs/usage/compositions/field-group.md +7 -3
- package/docs/usage/compositions/interaction-adapters.md +11 -9
- package/docs/usage/compositions/interaction-flow-apply.md +17 -15
- package/docs/usage/compositions/interaction-flow-search.md +11 -7
- package/docs/usage/compositions/live-list.md +12 -8
- package/docs/usage/compositions/navigation-bar-collection.md +8 -3
- package/docs/usage/compositions/optional-motion.md +2 -2
- package/docs/usage/compositions/paper-surface.md +11 -6
- package/docs/usage/compositions/purpose-input-comment.md +10 -4
- package/docs/usage/compositions/purpose-input-message.md +10 -4
- package/docs/usage/compositions/reference-first.md +4 -4
- package/docs/usage/compositions/reference-review.md +4 -4
- package/docs/usage/compositions/reference-settings.md +4 -4
- package/docs/usage/compositions/selection-motion.md +2 -1
- package/docs/usage/compositions/social-compositions.md +7 -6
- package/docs/usage/compositions/stea-event-ticket.md +2 -4
- package/docs/usage/compositions/stea-order-progress.md +19 -21
- package/docs/usage/compositions/stea-pixel-empty.md +26 -26
- package/docs/usage/compositions/task-controls.md +7 -2
- package/docs/usage/compositions/texture-comparison.md +5 -5
- package/docs/usage/compositions/toast-layout.md +7 -3
- package/docs/usage/compositions/toggle-feedback.md +1 -1
- package/docs/usage/compositions/visual-foundations.md +10 -5
- package/docs/usage/compositions/web-additions.md +5 -1
- package/docs/usage/screens/common-chat.md +5 -3
- package/docs/usage/screens/common-comments.md +14 -9
- package/docs/usage/screens/common-conversation-list.md +109 -0
- package/docs/usage/screens/common-inbox.md +5 -3
- package/docs/usage/screens/common-login.md +7 -4
- package/docs/usage/screens/common-profile.md +16 -12
- package/docs/usage/screens/common-saved.md +12 -8
- package/docs/usage/screens/common-search.md +16 -8
- package/docs/usage/screens/common-settings.md +12 -11
- package/docs/usage/screens/common-shell.md +12 -10
- package/docs/usage/screens/dashboard.md +59 -56
- package/docs/usage/screens/discovery-gallery.md +18 -9
- package/docs/usage/screens/flow-collection.md +10 -6
- package/docs/usage/screens/flow-editor.md +9 -6
- package/docs/usage/screens/flow-media.md +12 -7
- package/docs/usage/screens/flow-moderation.md +9 -3
- package/docs/usage/screens/flow-onboarding.md +13 -8
- package/docs/usage/screens/flow-permission.md +5 -3
- package/docs/usage/screens/landing.md +20 -14
- package/docs/usage/screens/mockup-studio.md +100 -88
- package/docs/usage/screens/notification-settings.md +9 -7
- package/docs/usage/screens/product-bento.md +22 -10
- package/docs/usage/screens/reference-comparison.md +2 -2
- package/docs/usage/tokens/font-roles.md +18 -2
- package/docs/usage/tokens/typography.md +22 -11
- package/package.json +7 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# AccountSwitcher
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.21.0
|
|
7
|
+
- 검토일: 2026-10-16
|
|
8
|
+
- 근거: `src/task-controls.ts`의 `resolveTaskChoice`, 양 renderer `src/task-controls.tsx`, [선택과 실제 실행 구성](../compositions/task-controls.md)
|
|
9
|
+
- 스토리북: `배포/구성/탐색과 이동/계정 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
확인된 현재 계정을 보여 주고 메뉴에서 다른 계정을 골라 실제 세션 전환을 요청할 때 쓴다.
|
|
14
|
+
선택만 바꾸는 메뉴와 달리 메뉴가 닫힌 뒤 제품의 `onSwitch`가 끝날 때까지 기다리며, 그동안
|
|
15
|
+
`accountId`는 바뀌지 않는다. 2026-10-15 선택과 실행 상태를 혼동하지 않도록 구성으로 도입했다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 선택만 바꾸고 요청이 없는 메뉴 | [Menu](menu.md) |
|
|
22
|
+
| 모드 선택 뒤 주요 행동 실행 | `SplitAction`([선택과 실제 실행](../compositions/task-controls.md)) |
|
|
23
|
+
| 화면 안에서 항목 하나를 고르는 폼 | [RadioGroup](radio-group.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `AccountSwitcher` | 확인된 계정 표시와 전환 요청 | `@hjmds/react/task-controls` | `@hjmds/react-native/task-controls` |
|
|
30
|
+
|
|
31
|
+
계정 항목 타입 `TaskChoice`는 `@hjmds/design-contracts/task-controls`에서 import한다. 루트 barrel에는 없다.
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { AccountSwitcher } from "@hjmds/react/task-controls";
|
|
38
|
+
|
|
39
|
+
<AccountSwitcher label={t("account.switch")} dismissLabel={t("common.close")}
|
|
40
|
+
accounts={accounts} accountId={session.accountId}
|
|
41
|
+
errorLabel={t("account.switchFailed")}
|
|
42
|
+
onSwitch={async id => { const next = await switchSession(id); setSession(next); }} />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// Native
|
|
47
|
+
import { AccountSwitcher } from "@hjmds/react-native/task-controls";
|
|
48
|
+
|
|
49
|
+
<AccountSwitcher label={t("account.switch")} dismissLabel={t("common.close")}
|
|
50
|
+
accounts={accounts} accountId={session.accountId}
|
|
51
|
+
errorLabel={t("account.switchFailed")}
|
|
52
|
+
onSwitch={async id => { const next = await switchSession(id); setSession(next); }} />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 축과 기본값
|
|
56
|
+
|
|
57
|
+
| prop | 값 | 기본값 | 설명 |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| accounts | `readonly TaskChoice[]`(`id`·`label`·`description?`·`disabled?`) | 필수 | 메뉴 항목. id 중복이면 오류 |
|
|
60
|
+
| accountId | string | 필수 | 제품이 확인한 현재 계정. `accounts`에 없으면 오류 |
|
|
61
|
+
| label | string | 필수 | 메뉴 트리거 버튼 문구와 메뉴 이름 |
|
|
62
|
+
| dismissLabel | string | 필수 | Native 메뉴 닫기 문구. Web Menu에는 전달하지 않는다 |
|
|
63
|
+
| errorLabel | string | 필수 | 전환 실패 문구 |
|
|
64
|
+
| onSwitch | `(accountId) => void \| Promise<void>` | 필수 | 실제 세션 전환. reject하면 실패 상태 |
|
|
65
|
+
| disabled | boolean | false | 메뉴 잠금과 요청 무시 |
|
|
66
|
+
|
|
67
|
+
## 배치
|
|
68
|
+
|
|
69
|
+
| 항목 | 값 | 근거 |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| 크기 | 부모 폭 안의 세로 묶음. 트리거는 기본 크기 `Button`(tone `secondary`) | 양 renderer `AccountSwitcher`·`ChoiceMenu` |
|
|
72
|
+
| 간격 | 현재 계정·트리거·실패 문구 사이 `spacing.sm` 12 | `Stack gap="sm"` |
|
|
73
|
+
| 순서·정렬 | 현재 계정 이름(strong) → 계정 메뉴 버튼 → 실패 시 중앙 정렬 오류 문구 | 양 renderer |
|
|
74
|
+
| 고정·스크롤 | 자체 스크롤 없음. 메뉴 표면은 Web 트리거 앵커 portal, Native 화면 가운데 `Modal` | [Menu](menu.md) |
|
|
75
|
+
| 좁은 폭·큰 글자 | 계정 이름과 오류 문구는 줄바꿈된다. 메뉴 폭·높이 상한은 Menu를 따른다 | [Menu](menu.md) |
|
|
76
|
+
|
|
77
|
+
## 꼭 지킬 것
|
|
78
|
+
|
|
79
|
+
- `onSwitch`는 실제 세션 전환이 끝난 뒤 resolve하고, 제품이 확인한 계정으로 `accountId`를 갱신한다. HJM은 낙관적으로 계정을 바꾸지 않는다.
|
|
80
|
+
- 계정 목록·권한·세션 저장은 제품이 소유한다. 현재 계정 이름 줄을 제품 레이아웃으로 다시 그리지 않는다.
|
|
81
|
+
- 모든 계정이 `disabled`이면 메뉴 대신 비활성 버튼만 남는다. 빈 상태 안내는 제품 문구로 주변에 둔다.
|
|
82
|
+
|
|
83
|
+
## 상태와 상호작용
|
|
84
|
+
|
|
85
|
+
| 상태 | 모습 | 동작 |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| 기본 | 현재 계정 + 메뉴 버튼, 메뉴에 현재 계정이 단일 선택으로 표시 | Web `onActionAfterDismiss`, Native `onSelectionAfterDismiss` 뒤 요청 |
|
|
88
|
+
| 진행 중 | 트리거 버튼 로딩, 메뉴 잠금 | 같은 계정·비활성 계정 선택은 무시, 중복 요청 차단 |
|
|
89
|
+
| 실패 | 기존 계정 유지, `errorLabel` 중앙 표시 | Web `role="alert"`, Native `accessibilityRole="alert"`·assertive. 같은 메뉴로 재시도 |
|
|
90
|
+
|
|
91
|
+
언마운트되면 진행 중 응답을 분리해 이전 요청 결과가 새 화면 상태에 반영되지 않는다.
|
|
92
|
+
|
|
93
|
+
## 플랫폼 차이
|
|
94
|
+
|
|
95
|
+
| 항목 | Web | Native |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| 메뉴 | `Menu` 드롭다운, `dismissLabel` 미사용 | `Menu` Modal, `dismissLabel`로 닫기 |
|
|
98
|
+
| 실패 알림 | `role="alert"` | `accessibilityRole="alert"`, `accessibilityLiveRegion="assertive"` |
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
- 단계: 컴포넌트
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
|
-
- 적용:
|
|
7
|
-
- 검토일: 2026-10-
|
|
6
|
+
- 적용: 1.26.1
|
|
7
|
+
- 검토일: 2026-10-17
|
|
8
8
|
- 근거: [ActivityChip 계약](../../activity-chip.md), `src/activity-chip.ts`(`activityChipRecipe`)
|
|
9
9
|
- 스토리북: `배포/컴포넌트/상태와 알림/작업 상태 칩`, `배포/구성/피드백과 복구/백그라운드 작업 상태`
|
|
10
10
|
|
|
@@ -105,5 +105,5 @@ import { ActivityChip } from "@hjmds/react-native/activity-chip";
|
|
|
105
105
|
|
|
106
106
|
## 함정
|
|
107
107
|
|
|
108
|
-
-
|
|
109
|
-
-
|
|
108
|
+
- 공개 API 대응표에서는 supplemental로 분류된다. 기본 Chip의 stable 상태를 상속하지 않으며 지원 범위는 [개별 계약](../../activity-chip.md)을 확인한다.
|
|
109
|
+
- Storybook 배포 분류와 기기 검증은 별개다. 기기 시각·VoiceOver/TalkBack 확인은 아직 하지 않았다.
|
|
@@ -47,7 +47,7 @@ import { Badge } from "@hjmds/react-native/data-display";
|
|
|
47
47
|
|
|
48
48
|
| prop | 값 | 기본값 | 설명 |
|
|
49
49
|
| --- | --- | --- | --- |
|
|
50
|
-
| `tone` | `neutral` · `strong` · `brand` · `info` · `success` · `warning` · `attention` · `danger` | `neutral` | `brand`는 판이 중립색이고 글자만 브랜드색이다. 브랜드 틴트는 "선택됨"을 뜻하므로 정적 표시에 쓰지 않는다. `strong`은 `neutral`보다 한 단계 강한 잉크 판이며 승패처럼 한눈에 갈려야 하는 사실에 쓴다 |
|
|
50
|
+
| `tone` | `neutral` · `strong` · `brand` · `info` · `success` · `warning` · `attention` · `danger` | `neutral` | `neutral`·`brand` 판은 캔버스색이라 `border.default` 테두리로 판을 그린다(2026-10-17, 테두리 없이 일반 글자처럼 보였다). `brand`는 판이 중립색이고 글자만 브랜드색이다. 브랜드 틴트는 "선택됨"을 뜻하므로 정적 표시에 쓰지 않는다. `strong`은 `neutral`보다 한 단계 강한 잉크 판이며 승패처럼 한눈에 갈려야 하는 사실에 쓴다 |
|
|
51
51
|
| `variant` | `filled` · `outline` | `filled` | — |
|
|
52
52
|
| `size` | `small` · `medium` | `medium` | — |
|
|
53
53
|
| `leading` | `ReactNode` | — | 라벨 앞 아이콘 자리이며 보조기기에서 숨겨진다 |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# BezierEditor
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.21.0
|
|
7
|
+
- 검토일: 2026-10-16
|
|
8
|
+
- 근거: `src/authoring-tools.ts`의 `validateBezier`·`cubicBezierPoint`, 양 renderer `src/authoring-tools.tsx`, [제작 도구 구성](../compositions/authoring-tools.md)
|
|
9
|
+
- 스토리북: `배포/구성/직접 조작과 모션/베지어 이징 편집`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
CSS `cubic-bezier` 이징 네 좌표를 슬라이더로 편집하고 곡선과 움직임 미리 보기를 함께 보여 줄 때 쓴다.
|
|
14
|
+
x는 0~1로 제한하고 y는 넘침(overshoot)을 허용한다. 값은 controlled이며 저장·모션 토큰 반영은 제품이 한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 숫자 하나 조정 | [Slider](slider.md) |
|
|
21
|
+
| 정해진 HJM 모션 토큰 선택 | [모션 토큰](../tokens/motion.md) |
|
|
22
|
+
| 키보드 단축키 기록 | `ShortcutRecorder`(Web 전용, [제작 도구 구성](../compositions/authoring-tools.md)) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `BezierEditor` | 베지어 이징 편집과 미리 보기 | `@hjmds/react/authoring-tools` | `@hjmds/react-native/authoring-tools` |
|
|
29
|
+
|
|
30
|
+
좌표 타입 `BezierCoordinates`는 `@hjmds/design-contracts/authoring-tools`에서 import한다.
|
|
31
|
+
Native는 곡선을 `react-native-svg`로 그리므로 optional peer인 `react-native-svg`를 설치해야 한다.
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { BezierEditor } from "@hjmds/react/authoring-tools";
|
|
38
|
+
import type { BezierCoordinates } from "@hjmds/design-contracts/authoring-tools";
|
|
39
|
+
|
|
40
|
+
const [value, setValue] = useState<BezierCoordinates>([0.25, 0.1, 0.25, 1]);
|
|
41
|
+
<BezierEditor value={value} onValueChange={setValue} formatCoordinate={v => v.toFixed(2)}
|
|
42
|
+
labels={{ coordinates: [t("x1"), t("y1"), t("x2"), t("y2")], preview: t("preview"),
|
|
43
|
+
curve: t("curve"), decrement: t("decrease"), increment: t("increase") }} />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Native
|
|
48
|
+
import { BezierEditor } from "@hjmds/react-native/authoring-tools";
|
|
49
|
+
|
|
50
|
+
<BezierEditor value={value} onValueChange={setValue} active={isFocused}
|
|
51
|
+
formatCoordinate={v => v.toFixed(2)}
|
|
52
|
+
labels={{ coordinates: [t("x1"), t("y1"), t("x2"), t("y2")], preview: t("preview"),
|
|
53
|
+
curve: t("curve"), decrement: t("decrease"), increment: t("increase") }} />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 축과 기본값
|
|
57
|
+
|
|
58
|
+
| prop | 값 | 기본값 | 설명 |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| value | `[x1, y1, x2, y2]` | 필수 | 유한수, x는 0~1. 벗어나면 `RangeError` |
|
|
61
|
+
| onValueChange | `(value) => void` | 필수 | 슬라이더 변경마다 새 배열 |
|
|
62
|
+
| yRange | `[min, max]` | `[-2, 2]` | y 슬라이더 범위. y1·y2가 범위 밖이면 오류 |
|
|
63
|
+
| duration | number(ms) | 1000 | 미리 보기 시간. 0 이하면 오류 |
|
|
64
|
+
| active | boolean | 미지정(활성) | `false`면 미리 보기 버튼 비활성·애니메이션 중지 |
|
|
65
|
+
| disabled | boolean | false | 슬라이더와 미리 보기 비활성 |
|
|
66
|
+
| labels | `coordinates`(4개)·`preview`·`curve`·`decrement`·`increment` | 필수 | 슬라이더·버튼·곡선 접근성 이름 |
|
|
67
|
+
| formatCoordinate | `(value) => string` | 필수 | 슬라이더 값 문구 |
|
|
68
|
+
|
|
69
|
+
## 배치
|
|
70
|
+
|
|
71
|
+
| 항목 | 값 | 근거 |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| 크기 | 곡선 영역 폭 100%·높이 180, 미리 보기 트랙 높이 24·지름 20 공, 슬라이더는 폭을 채운다 | 양 renderer `svg`/`Svg`, 공 `View` |
|
|
74
|
+
| 간격 | 곡선·슬라이더 4개·트랙·버튼 사이 `spacing.md` 16, 트랙 좌우 안쪽 4 | `Stack gap="md"`, `paddingInline`/`paddingHorizontal: 4` |
|
|
75
|
+
| 순서·정렬 | 곡선 → x1·y1·x2·y2 슬라이더 → 미리 보기 트랙 → 미리 보기 버튼 | 양 renderer |
|
|
76
|
+
| 고정·스크롤 | 자체 스크롤 없음. viewBox는 두 끝점과 제어점 y를 모두 담도록 맞춘다 | `curveMin`·`curveMax` |
|
|
77
|
+
| 좁은 폭·큰 글자 | 곡선은 폭에 맞춰 축소된다. 슬라이더 라벨은 줄바꿈되고 터치 높이 44는 유지 | [Slider](slider.md) |
|
|
78
|
+
|
|
79
|
+
## 꼭 지킬 것
|
|
80
|
+
|
|
81
|
+
- 값은 controlled다. 저장·되돌리기·모션 토큰 등록은 제품이 하며 HJM은 초안만 보여 준다.
|
|
82
|
+
- 축소 모션 설정이면 미리 보기를 실행하지 않는다. 화면을 떠날 때는 `active={false}`로 애니메이션을 멈춘다.
|
|
83
|
+
- 곡선 색·공 색을 제품 CSS로 덮지 않는다. 곡선은 brand 콘텐츠 색을 쓴다.
|
|
84
|
+
|
|
85
|
+
## 상태와 상호작용
|
|
86
|
+
|
|
87
|
+
값·시간·`active`·`disabled`·축소 모션이 바뀌면 진행 중 미리 보기를 취소한다. Web은 문서가 숨겨질 때
|
|
88
|
+
(`visibilitychange`), Native는 앱이 비활성일 때(`AppState`) 애니메이션을 멈춘다. 미리 보기는 공을 120만큼
|
|
89
|
+
가로 이동한다. 미리 보기 트랙은 장식이라 접근성 트리에서 숨긴다.
|
|
90
|
+
|
|
91
|
+
## 플랫폼 차이
|
|
92
|
+
|
|
93
|
+
| 항목 | Web | Native |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| 애니메이션 | Web Animations API `cubic-bezier(...)` | `Animated.timing` + `Easing.bezier`, native driver |
|
|
96
|
+
| 곡선 색 | 보조선 `currentColor`, 곡선 `--hjm-color-content-brand` | 보조선 `textMuted`, 곡선·제어점·공 `contentBrand` |
|
|
97
|
+
| 증감 버튼 이름 | `decrement`·`increment`를 Slider에 넘기지 않는다 | Slider `decrementLabel`·`incrementLabel`로 전달 |
|
|
98
|
+
| 추가 export | 없음 | `authoringToolsCapabilities`(`bezierEditor: true`, `shortcutRecorder: false`) |
|
|
99
|
+
| 의존성 | 없음 | optional peer `react-native-svg` |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.12.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-17
|
|
8
8
|
- 근거: [화면 제목과 마지막 행동](../../screen-chrome.md), recipe `bottomCtaRecipe`(`src/component-recipes.ts`)
|
|
9
9
|
- 스토리북: `배포/컴포넌트/동작/하단 실행 버튼`
|
|
10
10
|
|
|
@@ -79,6 +79,7 @@ const insets = useSafeAreaInsets();
|
|
|
79
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
80
|
| 순서·정렬 | 위→아래 [설명 한 줄(caption, muted)] → 행동 줄. 행동 줄은 [보조][주] 순서로 같은 폭을 나눈다. 주 행동은 하나다 | `react/src/bottom-cta.tsx`, `react-native/src/actions.tsx` |
|
|
81
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
|
+
| `ScreenLayout` `footer` 안 | footer가 위 경계·`screenPatternRecipe.padding` 16·바탕·safe area를 맡으므로 BottomCTA는 바(경계·여백·바탕·그림자·최소 높이)를 그리지 않고 설명과 행동 줄만 그린다. 직접 넘긴 `safeAreaBottom`만 아래 여백으로 남긴다. 2026-10-17 화면 리뷰: footer 안의 BottomCTA가 경계를 두 겹 긋고 버튼을 한 번 더 들였으며 Native는 footer 안에 그림자 있는 흰 상자를 하나 더 그렸다 | 내부 `ScreenFooterContext`, Web `.hjm-bottom-cta[data-placement="screen-footer"]`, Native `BottomCTA` |
|
|
82
83
|
| 좁은 폭·큰 글자 | Web은 행동 칸 기준 폭 132(`control.minTouchTarget` × 3)보다 좁으면 줄바꿈한다. 큰 글자에서는 세로로 쌓고 주 행동이 위에 온다 | `.hjm-bottom-cta__actions > div`, `.hjm-bottom-cta[data-large-text="true"]` |
|
|
83
84
|
|
|
84
85
|
```text
|
|
@@ -4,23 +4,27 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.12.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-17
|
|
8
8
|
- 근거: [버튼 라벨 줄바꿈](../../button-label.md), `src/base-recipes.ts`(`buttonRecipe`), `src/foundations.ts`(`control.buttonHeight`)
|
|
9
9
|
- 스토리북: `배포/컴포넌트/동작/버튼`, `배포/컴포넌트/동작/버튼 안에서 확인`, `배포/컴포넌트/동작/반응 선택`
|
|
10
10
|
|
|
11
11
|
## 언제 쓰나
|
|
12
12
|
|
|
13
13
|
사용자가 누르면 무언가가 일어나는 텍스트 행동에 쓴다. 저장·확인·다음 같은 화면의 주 행동,
|
|
14
|
-
보조 행동, 삭제처럼 되돌리기 어려운
|
|
14
|
+
보조 행동, 삭제처럼 되돌리기 어려운 행동이 여기에 속한다. `selected`는 항목 저장·북마크, 켜 둔
|
|
15
|
+
도구 막대 도구처럼 **행동 자체가 눌림 상태를 갖는 토글**에만 쓴다. 여러 선택지 중 하나·여럿을 고르거나
|
|
16
|
+
설정을 켜고 끄는 데는 쓰지 않는다([선택 조립 규칙](../SELECTION.md#조립-규칙필수)).
|
|
15
17
|
|
|
16
18
|
## 쓰지 않을 때
|
|
17
19
|
|
|
18
20
|
| 상황 | 대신 쓸 것 |
|
|
19
21
|
| --- | --- |
|
|
20
|
-
| 아이콘만 있는 행동 | [IconButton](icon-button.md) |
|
|
22
|
+
| 아이콘만 있는 행동, 도구성 행동(새로고침·추가·편집·삭제·공유·검색·필터·더보기) | [IconButton](icon-button.md) — 글자는 `label`로 |
|
|
21
23
|
| 화면 하단에 고정된 주 행동 | [BottomCTA](bottom-cta.md) |
|
|
22
24
|
| 다른 페이지·URL로 이동 | [Link](link.md) (`tone="link"` 버튼은 같은 화면 안의 행동용) |
|
|
23
|
-
| 여러 선택지 중 하나를 고름 | [SegmentedControl](segmented-control.md), [ToggleGroup](toggle-group.md) |
|
|
25
|
+
| 여러 선택지 중 하나를 고름 | [SegmentedControl](segmented-control.md), [RadioGroup](radio-group.md), [ToggleGroup](toggle-group.md), `Chip`(`selectionMode="single"`) |
|
|
26
|
+
| 여러 개를 고름 | `Chip`(`selectionMode="multiple"`), [CheckboxGroup](checkbox-group.md) |
|
|
27
|
+
| 설정 켜고 끄기 | [Switch](switch.md)(설정 목록은 [ListRow](list-row.md) + Switch) |
|
|
24
28
|
| 소셜 로그인 | [AuthProviderButton](auth-provider-button.md) |
|
|
25
29
|
| 복사 | `ClipboardButton`(Web, 아래 표) |
|
|
26
30
|
| 떠 있는 주 행동 | [FloatingActionButton](floating-action-button.md) |
|
|
@@ -62,7 +66,7 @@ import { Button } from "@hjmds/react-native/actions";
|
|
|
62
66
|
| `size` | `small` · `medium` · `large` | `medium` | 높이 36 · 44 · 52 |
|
|
63
67
|
| `shape` | `rounded` · `pill` | `rounded` | `radius.md` · `full` |
|
|
64
68
|
| `align` | `center` · `leading` | `center` | `leading`은 꽉 찬 폭의 행 행동 |
|
|
65
|
-
| `selected` | `boolean` | — |
|
|
69
|
+
| `selected` | `boolean` | — | 눌림 상태를 갖는 행동 토글(저장·북마크, 켜 둔 도구). Web `aria-pressed`, Native 접근성 state. 선택지 고르기·설정 켜고 끄기에는 쓰지 않는다 |
|
|
66
70
|
| `loading` | `boolean` | `false` | 기존 문구·아이콘을 시각적으로 숨기고 중앙 스피너 하나만 표시한다. 같은 children을 유지해 크기를 보존하고 누름을 막는다. 접근성 이름·포커스는 유지한다(Web `aria-disabled`, Native는 `disableWhileLoading`으로만 옛 disabled 동작) |
|
|
67
71
|
| Web `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | `disabled`·`loading`·`aria-disabled`이면 부르지 않는다 |
|
|
68
72
|
| Native `onPress` | `(event: GestureResponderEvent) => void` | — | 같은 조건에서 부르지 않는다 |
|
|
@@ -80,7 +84,7 @@ import { Button } from "@hjmds/react-native/actions";
|
|
|
80
84
|
| 항목 | 값 | 근거 |
|
|
81
85
|
| --- | --- | --- |
|
|
82
86
|
| 크기 | 높이 `small` 36 · `medium` 44 · `large` 52. `small`만 hitSlop 4로 터치 영역 44 | `control.buttonHeight`, `control.buttonHitSlop` |
|
|
83
|
-
| 간격 |
|
|
87
|
+
| 간격 | 한 묶음의 버튼 사이: **세로로 쌓으면 `spacing.xs` 8, 가로로 나란하면 `spacing.sm` 12**. 예제·제품의 `Stack`도 같은 값을 쓴다. Dialog·Sheet 행동 영역 좌우·아래 여백 `spacing.lg` 20 | `actionGroupRecipe`, `.hjm-dialog__footer`, Native Sheet footer |
|
|
84
88
|
| 순서·정렬 | 가로 행동 줄은 **보조 → 주**, 끝 정렬. 세로 순서는 배치 소유자의 계약을 따른다: Dialog는 보조 → 주, Native Sheet는 주 → 보조, 좁은 AlertDialog는 주 → 보조. 본문의 짧은 선택·확정 구성은 해당 구성의 순서다. 목록 행 끝은 `small` + `ghost`/`secondary`, `primary` 금지 | `renderAction` 순서(Native overlays), `.hjm-dialog__footer` |
|
|
85
89
|
| 고정·스크롤 | 화면 맨 아래 고정 주 행동은 Button 대신 [BottomCTA](bottom-cta.md)(안전 영역·키보드 처리) | — |
|
|
86
90
|
| 좁은 폭·큰 글자 | 폭 < 600(`breakpoint.medium`)이면 AlertDialog 행동을 세로로 쌓고 주 행동이 위(DOM은 [취소][확인] 유지). 라벨은 두 줄까지, 큰 글자에서는 상한 해제 | `.hjm-alert-dialog__actions`, [버튼 라벨](../../button-label.md) |
|
|
@@ -95,8 +99,7 @@ import { Button } from "@hjmds/react-native/actions";
|
|
|
95
99
|
|
|
96
100
|
## 꼭 지킬 것
|
|
97
101
|
|
|
98
|
-
-
|
|
99
|
-
기존 [실행과 복구](../../action-session.md)와 비교했다. Button은 callback의 Promise를 자동으로
|
|
102
|
+
- Button은 callback의 Promise를 자동으로
|
|
100
103
|
기다려 성공을 판정하는 엔진이 아니다. 제품 mutation 또는 기존 action-session의 pending을
|
|
101
104
|
`loading`에 연결하고 실제 확정 뒤 성공 문구를 보여 준다. 실패하면 초안을 보존하고 재시도를
|
|
102
105
|
제공한다. Promise가 resolve됐다는 이유만으로 도메인 응답의 실패를 성공으로 표시하지 않는다.
|
|
@@ -105,8 +108,10 @@ import { Button } from "@hjmds/react-native/actions";
|
|
|
105
108
|
- 배치는 `layoutStyle`로만 한다. 색·radius·높이를 `style`/`className`으로 덮지 않는다.
|
|
106
109
|
Native에서 `style`·`labelStyle`을 넘기면 실행 중 `TypeError`가 난다.
|
|
107
110
|
- 색은 tone과 제품 테마 토큰으로 바꾼다. 버튼마다 브랜드 색을 하드코딩하지 않는다.
|
|
108
|
-
- 진행 중 상태는 `loading`으로 표시하고 같은 자리에 별도 Spinner를 겹치지 않는다. children을 빈 문자열이나 다른 길이의 진행 문구로 바꾸지 않는다. 렌더러가 문구·아이콘을 숨긴 자리에 중앙 스피너만 표시하고 기존 크기·접근성 이름을 유지한다.
|
|
109
|
-
- "한 화면 `primary` 하나"의 예외는 다음과 같다. `selected`를 준 버튼은 tone과 관계없이 `buttonRecipe.states.selected`(배경 `bg`, 글자 `contentBrand`, 선택 테두리 없음)로 칠해져 primary 채움이 아니므로 세지 않고(Web `.hjm-button[data-selected="true"]`, Native `internal/recipe-button.tsx`,
|
|
111
|
+
- 진행 중 상태는 `loading`으로 표시하고 같은 자리에 별도 Spinner를 겹치지 않는다. children을 빈 문자열이나 다른 길이의 진행 문구로 바꾸지 않는다. 렌더러가 문구·아이콘을 숨긴 자리에 중앙 스피너만 표시하고 기존 크기·접근성 이름을 유지한다.
|
|
112
|
+
- "한 화면 `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`, 작품 탐색·랜딩 스토리의 시트).
|
|
113
|
+
|
|
114
|
+
- `selected` 버튼 여러 개를 나란히 두어 하나 고르기·여러 개 고르기·켜고 끄기를 흉내 내지 않는다. 그 의도는 [선택 조립 규칙](../SELECTION.md#조립-규칙필수)의 SegmentedControl·RadioGroup·Chip·Switch를 쓴다.
|
|
110
115
|
|
|
111
116
|
- 긴 소개 화면에서 위·아래 CTA가 **동일한 행동**으로 이어지고 서로 다른 스크롤 구간에 있으면 둘 다 primary를 허용한다. 큰 글자에서 첫 CTA가 멀어지는 문제를 보완하기 위한 예외이며, 서로 다른 가입·구매 행동을 동시에 강조하는 근거로 쓰지 않는다.
|
|
112
117
|
|
|
@@ -121,9 +126,9 @@ import { Button } from "@hjmds/react-native/actions";
|
|
|
121
126
|
| 기본 `type` | `"button"`(폼 submit은 `type="submit"`을 명시) | 해당 없음 |
|
|
122
127
|
|
|
123
128
|
|
|
124
|
-
-
|
|
129
|
+
- footer 순서를 모든 본문 행동에 강제하지 않는다. 시간 선택처럼 주 행동 다음에 초기화가 오는 구성과 AlertDialog의 좁은 폭 순서는 각각 명시된 구성 계약을 따른다.
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
`selected`에는 선택 테두리를 추가하지 않는다. `selected` 접근성 상태와
|
|
127
132
|
contentBrand 글자·아이콘 색은 유지하며 키보드 focus outline은 선택과 별도로 유지한다.
|
|
128
133
|
|
|
129
134
|
## 저장 확정 라벨과 프레임 예약 (npm 1.21.0 · 2026-10-15)
|
|
@@ -164,3 +169,12 @@ Button의 loading은 표시와 UI 재실행을 막는 기능이다. 같은 프
|
|
|
164
169
|
무효화는 기존 action-session 또는 제품 mutation이 맡는다. 저장 실패 뒤에는 입력과
|
|
165
170
|
마지막 확정 값을 유지한다. [버튼 완료 피드백](../compositions/action-feedback.md)의
|
|
166
171
|
`resolveSaveFeedback` 조합을 사용한다.
|
|
172
|
+
|
|
173
|
+
## 근거
|
|
174
|
+
|
|
175
|
+
- 버튼 묶음 간격(세로 8·가로 12): 2026-10-17 사용자 지시 — 세로로 쌓은 하단 버튼의 12 간격이 벌어져 보였다.
|
|
176
|
+
- Button은 Promise로 성공을 판정하지 않음: 2026-10-07 [Stateful Button 대조](../../../../../docs/plans/aceternity-interaction-adoption-2026-10-07.md)를 기존 [실행과 복구](../../action-session.md)와 비교했다.
|
|
177
|
+
- `loading` 중앙 스피너·크기 유지: 2026-10-06 독립 검증에서 이 사용자 요구가 사용 지침에 빠져 있음을 확인해 명시했다.
|
|
178
|
+
- `selected` 버튼 나열 금지: 2026-10-16 사용자 지적 — 예제의 선택 버튼 줄이 테두리 없는 글자처럼 보이고 폭이 제각각이었다.
|
|
179
|
+
- footer 순서 범위: 2026-10-06 독립 지침 재구현에서 footer 순서를 모든 본문 행동에 강제하는 것으로 읽혔다.
|
|
180
|
+
- 선택 테두리 없음: 2026-10-10 사용자 요청으로 선택 시 추가되는 테두리를 제거했다.
|
|
@@ -82,7 +82,7 @@ import { Carousel } from "@hjmds/react-native/carousel";
|
|
|
82
82
|
| `currentKey` + `onCurrentKeyChange` · `defaultCurrentKey` | 슬라이드 `id`, `(key: string) => void` | 첫 슬라이드 | 제어(둘 다 필수)·비제어 중 하나만 타입이 허용한다. 값은 인덱스가 아니라 `id`다 |
|
|
83
83
|
| `autoplay` | `{ intervalMs: number }`(0보다 큼) | 없음 | 주면 일시정지/재개 버튼이 생긴다. 마지막 슬라이드, reduced motion, 사용자 조작 뒤에는 멈추고 명시적인 재개 전까지 다시 돌지 않는다 |
|
|
84
84
|
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`은 제거된 속성 — `layoutStyle`로 배치하고 내부 표현은 recipe를 따른다 |
|
|
85
|
-
| `CarouselMotion` 탐색 | `navigation?: "buttons" \| "counter"` | `buttons` | `counter`는 이전 아이콘·현재/전체·다음 아이콘을 한 줄에 둔다. 끝에서는 해당 버튼을 비활성화한다. Web/Native 동일, 미게시 |
|
|
85
|
+
| `CarouselMotion` 탐색 | `navigation?: "buttons" \| "counter"` | `buttons` | `buttons`는 이전·다음 아이콘 버튼(`previousLabel`·`nextLabel`이 이름)을 슬라이드 아래 끝 정렬 한 줄에 `spacing.sm` 간격으로 둔다. `counter`는 이전 아이콘·현재/전체·다음 아이콘을 한 줄에 둔다. 아이콘은 읽기 방향을 따라 좌우가 바뀐다. 끝에서는 해당 버튼을 비활성화한다. Web/Native 동일, 미게시 |
|
|
86
86
|
| `CarouselMotion` 선택 | `currentKey: string`, `onCurrentKeyChange(key: string): void` | 필수(항상 제어) | autoplay가 없다. `slides`는 `{ id, label, disabled? }` |
|
|
87
87
|
| `CarouselMotion` 라벨 | `label`·`previousLabel`·`nextLabel`, 선택 `composeAccessibleName` | — | `composeAccessibleName`이 없으면 슬라이드 `label`을 쓴다 |
|
|
88
88
|
| `CarouselMotion` 크기(Native) | `width`·`height` 측정값 | 필수 | 양수가 아니면 `TypeError`. Web `CarouselMotion`은 `layoutStyle`을 받고 Native는 받지 않는다 |
|
|
@@ -91,11 +91,11 @@ import { Carousel } from "@hjmds/react-native/carousel";
|
|
|
91
91
|
|
|
92
92
|
| 항목 | 값 | 근거 |
|
|
93
93
|
| --- | --- | --- |
|
|
94
|
-
| 크기 | 부모 폭을 채우고 슬라이드 높이는 내용이 정한다(현재 슬라이드만 보인다). 이전·다음은 ghost
|
|
94
|
+
| 크기 | 부모 폭을 채우고 슬라이드 높이는 내용이 정한다(현재 슬라이드만 보인다). 이전·다음은 문구를 접근성 이름으로 받는 ghost IconButton(chevronStart/chevronEnd, `carouselRecipe.sizes.medium.controlIcon`), 점은 지름 8에 터치 영역 44(`control.minTouchTarget`), 색은 `carouselRecipe.dot.color`. 두 플랫폼 동일 | `carouselRecipe.dot`, `.hjm-carousel__dot` |
|
|
95
95
|
| 간격 | 영역 사이(재생 버튼·슬라이드·조작 줄) `spacing.sm` 12, 조작 줄 안 `spacing.xs` 8 | `carouselRecipe.sizes.medium.gap`, `.hjm-carousel__controls` |
|
|
96
|
-
| 순서·정렬 | 위→아래 [일시정지/재개(autoplay일 때, 시작 쪽)] → [슬라이드] → [이전][점 · · ·][다음] 가운데 정렬. Native
|
|
96
|
+
| 순서·정렬 | 위→아래 [일시정지/재개(autoplay일 때, 시작 쪽)] → [슬라이드] → [이전][점 · · ·][다음] 가운데 정렬. Native 점 줄은 하나의 adjustable 요소(플랫폼 page control과 같은 방식)라 화면 낭독기는 위·아래 쓸기로 넘긴다. 위치 문구는 화면에 쓰지 않고 접근성 값으로만 전한다(2026-10-17) | `react/src/carousel.tsx`, `react-native/src/carousel.tsx` |
|
|
97
97
|
| 고정·스크롤 | 고정 영역이 없다. 슬라이드 넘김은 버튼·점(또는 `CarouselMotion` 스와이프)으로 하고 가로 스크롤 영역을 만들지 않는다 | `.hjm-carousel__slide[hidden]` |
|
|
98
|
-
| 좁은 폭·큰 글자 |
|
|
98
|
+
| 좁은 폭·큰 글자 | [이전][점][다음]은 한 줄을 유지하고 점 묶음만 줄바꿈된다. 아이콘 버튼이라 큰 글자에서도 폭이 늘지 않는다 | `.hjm-carousel__controls`, `react-native/src/carousel.tsx` |
|
|
99
99
|
|
|
100
100
|
## 꼭 지킬 것
|
|
101
101
|
|
|
@@ -104,6 +104,7 @@ import { BarChart, Sparkline } from "@hjmds/react-native/chart";
|
|
|
104
104
|
| `missingLabel` | `string` | — (필수) | 텍스트 대안에서 빈 값을 읽는 문구 |
|
|
105
105
|
| `height` | `number` | `chartRecipe.heights.regular` 180 | 가로는 부모 폭을 채운다 |
|
|
106
106
|
| `curve` | `linear` · `monotone` · `step` | `linear` | `monotone`은 데이터 밖으로 넘지 않는 곡선 |
|
|
107
|
+
| `missingConnection` | `none` · `dotted` | `none` | 빈 구간 양쪽 관측값에서 각자의 시즌/기간 x 위치를 유지한 채 plot 하단까지 수직 점선을 내린다. 양쪽 점선은 서로 연결하지 않는다. 하단은 결측 표시이며 0이 아니다. 값·포커스·영역 채움은 만들지 않으며 앞/뒤 결측을 외삽하지 않는다. Web·Native 미게시 |
|
|
107
108
|
| `area` | `boolean` | 계열이 하나면 `true` | 선 아래 그라데이션(0.32 → 0) |
|
|
108
109
|
| `stacked` | `boolean` | `false` | 쌓은 영역. 0을 항상 포함 |
|
|
109
110
|
| `invert` | `boolean` | `false` | 작을수록 위(순위·평균자책점) |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.17.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-16
|
|
8
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
9
|
- 스토리북: `배포/구성/정보 표시/대화 메시지`, `배포/화면/소통/채팅`
|
|
10
10
|
|
|
@@ -66,21 +66,27 @@ import { Text } from "@hjmds/react-native/primitives";
|
|
|
66
66
|
</ChatMessage>
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
2026-10-16 사용자 결정으로 대화 타임라인의 공통 기준을 카카오톡 iOS 채팅방 실측(@3x 캡처)으로 바꿨다.
|
|
70
|
+
묶음 단위 표시·촘촘한 말풍선·시각 위치를 흡수했고 노란 말풍선 같은 브랜드 색은 가져오지 않았다(색은 semantic token).
|
|
71
|
+
|
|
69
72
|
## 축과 기본값
|
|
70
73
|
|
|
71
74
|
| prop | 값 | 기본값 | 설명 |
|
|
72
75
|
| --- | --- | --- | --- |
|
|
73
76
|
| `direction` | `"incoming"` · `"outgoing"` | 필수 | 수신은 시작 쪽, 발신은 끝 쪽에 붙는다 |
|
|
74
|
-
| `author` | `string` | 필수 |
|
|
75
|
-
| `
|
|
77
|
+
| `author` | `string` | 필수 | 수신 묶음의 첫 말풍선에서만 그린다. 발신(내 메시지)은 그리지 않는다(Web은 행 이름으로만 쓴다). 1:1 대화처럼 작성자를 숨길 때는 빈 문자열 |
|
|
78
|
+
| `groupPosition` | `"single"` · `"first"` · `"middle"` · `"last"` | `"single"` | 미게시. `resolveChatMessageGroups(items)`가 같은 작성자·같은 표시 시각의 연속 메시지를 묶어 돌려준다. 첫 말풍선에만 작성자·아바타·꼬리, 끝 말풍선에만 시각을 그리고 뒤 말풍선은 아바타 폭을 비워 둔다 |
|
|
79
|
+
| `timestamp` | `string` | 필수 | 제품이 포맷한 시각. 묶음 판정에도 같은 문자열을 쓴다. 말풍선 아래 끝 옆(수신 오른쪽·발신 왼쪽)에 그린다 |
|
|
76
80
|
| `timestampPresentation` | `"always"` · `"swipe"` | `"always"` | `swipe`는 수신은 오른쪽·발신은 왼쪽으로 끄는 동안 반대편 여백에 시각을 보여 주고 놓으면 숨긴다. 이 모드에서는 같은 제스처로 답장을 실행하지 않는다. 시각은 보조기술에서도 읽을 수 있다. |
|
|
77
|
-
| `bubbleTail` | `boolean` | `false` |
|
|
81
|
+
| `bubbleTail` | `boolean` | `true`(미게시 변경, 이전 `false`) | 묶음 첫 말풍선의 위쪽 모서리(수신 시작 쪽·발신 끝 쪽)에 윗변이 이어지는 삼각 꼬리(`tailWidth` 7 × `tailHeight` 10)를 같은 표면으로 그리고, 그 모서리 radius를 `tailCornerRadius` 4로 줄인다. 행 양쪽 `spacing.xs` 8 여백으로 잘림을 막는다 |
|
|
78
82
|
| `deliveryLabel` | `string` | 없음 | 시각 뒤에 ` · `로 붙는 전송 상태 |
|
|
79
|
-
| `avatar` | `ReactNode` | 없음 | 버블 옆
|
|
80
|
-
| `reply` | `ReactNode` | 없음 | 인용 노드. `replyLink`가 없으면 버블
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
83
|
+
| `avatar` | `ReactNode` | 없음 | 버블 옆 아바타. `chatMessageRecipe.avatarSize` 36 크기로 넘긴다. 묶음 뒤 말풍선에서는 같은 폭을 비워 정렬을 유지한다 |
|
|
84
|
+
| `reply` | `ReactNode` | 없음 | 인용 노드. `replyLink`가 없으면 버블 맨 위에 그리고 아래 구분선(1)으로 답장 본문과 나눈다 |
|
|
85
|
+
| `replyQuote` | `{ title, excerpt }` | 없음 | 미게시. 말풍선 맨 위에 HJM이 인용을 그린다: 제목(예: "서연에게 답장", 본문 글자) → 원문 한 줄(흐린 글자, 넘치면 말줄임) → 구분선 1 → 답장 본문. 자유 노드 `reply`보다 우선 사용한다 |
|
|
86
|
+
| `replyLink` | `{ label, onPress() }` | 없음 | 인용과 같이 주면 말풍선 안 인용을 눌러 원문으로 이동한다(Native는 말풍선의 "원문으로 이동" 접근성 동작도 제공). 이전에는 버블 밖 ghost 버튼이었다 |
|
|
87
|
+
| `media` | `{ id, label, preview, aspectRatio?, onPress? }[]` | 없음 | 미게시. 보낸 사진. 폭 `chatMessageRecipe.mediaWidth` 210, 한 장은 `aspectRatio`(기본 1), 여러 장은 정사각 칸(`resolveChatMediaColumns`: 2·4장 2열, 그 외 3열, 간격 2), 바깥 radius 14. 글 없이 사진만 보내면 말풍선 표면·꼬리를 그리지 않는다. `onPress`가 있으면 사진마다 버튼(크게 보기는 제품) |
|
|
88
|
+
| `replyAction` | `{ label, onPress(), disabled?, icon? }` | 없음 | `replySwipeDistance` 60px 이상이고 세로 이동의 2배를 넘는 가로 스와이프로 실행(`isReplySwipe`). `reactions`가 있으면 길게 누른 메뉴의 첫 항목으로도 보인다(미게시) |
|
|
89
|
+
| `reactions` | `ReactionPickerProps & { closeLabel, menuActions?, reportAction?, menuAction? }` | 없음 | `reactionHoldMs` 450ms 길게 누르면 메뉴가 열린다: 반응 줄(이모지 `typography.title` 18/26, `more`면 +) → 메시지 미리보기 → 카드(맨 위 `timestamp` 정확한 시각 → `replyAction` → `menuActions` → `menuAction` → `reportAction` 빨간 글자). 항목은 높이 44, 아이콘 24 칸. 배치는 `holdMenuRecipe`(인스타그램 DM 실측): 화면 좌우 10 안쪽 전체 폭 반응 줄(높이 60, 48 원형 칸, 이모지 28, 넘칠 때만 가로 스크롤) → 12 → 누른 말풍선 그대로 미리보기(제 쪽 정렬) → 12 → 폭 200·radius 18 동작 카드(행 56, 아이콘 24, `submenu` 행은 ›)를 말풍선 쪽에 붙인다. 보이는 ×는 없고 배경 탭·Escape·VoiceOver 닫기로 닫는다(보조기술 첫 초점용 닫기 대상만 둔다). 같은 반응 재선택은 `null`. `label`은 Web에서 말풍선 이름·Popover 제목, Native에서 접근성 힌트·동작 이름. 시각·동작 카드와 `reportAction`·`menuActions`는 미게시(2026-10-16 인스타그램 DM 길게 누르기 실측 반영) |
|
|
84
90
|
| `interactiveContent` | `boolean` | `false` | 사진 앨범·링크처럼 자식이 버튼을 가지면 `true`. 안쪽 버튼·링크의 누름과 Web Enter/Space는 그 컨트롤이 받는다 |
|
|
85
91
|
| `actions` | `ReactNode` | 없음 | 시각 옆 슬롯. 반응 집계 배지·재시도는 제품이 넣는다 |
|
|
86
92
|
| `children` | `ReactNode` | 필수 | 메시지 본문. Native는 말풍선이 이 텍스트로 읽힌다 |
|
|
@@ -90,9 +96,9 @@ import { Text } from "@hjmds/react-native/primitives";
|
|
|
90
96
|
|
|
91
97
|
| 항목 | 값 | 근거 |
|
|
92
98
|
| --- | --- | --- |
|
|
93
|
-
| 크기 | 말풍선 열 최대 폭 `screenPatternRecipe.messageMaxWidth` 84%;
|
|
94
|
-
| 간격 |
|
|
95
|
-
| 순서·정렬 | incoming 시작 쪽, outgoing 끝 쪽; 작성자 → (replyLink 인용) → 버블
|
|
99
|
+
| 크기 | 말풍선 열 최대 폭 `screenPatternRecipe.messageMaxWidth` 84%; 말풍선 radius `chatMessageRecipe.bubbleRadius` 14, 안쪽 padding 세로 6·가로 10, 테두리 없음(수신 `surface`, 발신 `surfaceAccent`); 아바타 36; 꼬리 10 | `chatMessageRecipe`, Web `.hjm-chat-message*`, Native `ChatMessage` |
|
|
100
|
+
| 간격 | 묶음 사이 `groupGap` 12, 같은 묶음 말풍선 사이 `runGap` 6(메시지 행이 위쪽 여백으로 소유하므로 타임라인 목록에는 gap을 주지 않는다); 아바타–버블 `avatarGap` 8; 작성자–버블 `authorGap` 4; 버블–시각 `metaGap` 4; 인용 아래 `spacing.xs` 8 | `chatMessageRecipe`, `resolveChatMessageGroupDisplay` |
|
|
101
|
+
| 순서·정렬 | incoming 시작 쪽, outgoing 끝 쪽; (첫 말풍선) 작성자 → (replyLink 인용) → 버블 + 아래 끝 옆 시각·전송 상태·actions | 렌더 순서 |
|
|
96
102
|
| 고정·스크롤 | 자체 고정 없음, 타임라인(ChatScreen 본문)이 스크롤; 반응 메뉴는 Web Popover · Native 전체 화면 `Modal`; 답장 스와이프 중 가로 이동은 ±72로 제한 | Web·Native `ChatMessage` |
|
|
97
103
|
| 좁은 폭·큰 글자 | 버블은 84% 안에서 줄바꿈되고 본문을 자르지 않는다(Web `overflow-wrap: anywhere`, `white-space: pre-wrap`); 반대쪽 16% 여백은 작성자 구분을 위해 남긴다 | Web `.hjm-chat-message__bubble`, Native `maxWidth`·`flexShrink: 1` |
|
|
98
104
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.17.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-17
|
|
8
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
9
|
- 스토리북: `배포/화면/소통/채팅`
|
|
10
10
|
|
|
@@ -67,17 +67,20 @@ import { ChatScreen, MessageComposer } from "@hjmds/react-native/screens";
|
|
|
67
67
|
| 슬롯·prop | 내용 | 비고 |
|
|
68
68
|
| --- | --- | --- |
|
|
69
69
|
| `title`(필수) | 지역화한 대화 제목 | `header`를 주면 Web은 `aria-label`로만 쓴다 |
|
|
70
|
-
| `header` / `
|
|
70
|
+
| `header` / `exit` / `actions` / `description` | 기존 navigation 헤더, 또는 헤더 시작 쪽 화면 나가기 아이콘(`exit`: 뒤로·닫기)·도구 슬롯 | `header`를 주면 기본 헤더를 그리지 않는다 |
|
|
71
71
|
| `children` | 타임라인(가상화 목록 + `ChatMessage`) | 자동 스크롤·새 메시지 배지·이전 메시지 위치는 제품 |
|
|
72
72
|
| `composer`(필수) | `MessageComposer` 등 작성창 | `ready`와 `empty`에서 보인다(빈 대화의 첫 메시지 입력) |
|
|
73
73
|
| `state` / `stateAction` | `loading`·`empty`·`error`·`restricted` + 지역화 `title` | ready 외에는 본문을 교체한다 |
|
|
74
74
|
| `notice` | 새로고침 실패 같은 비차단 안내 | 초안 유지가 필요하면 `state` 대신 이것 |
|
|
75
|
+
| `avatar` | 상대·방 사진 노드 | 미게시. 가운데 정렬 제목 앞에 놓는다. 단톡방은 인원 수를 `titleBadge`(흐린 글자)로 넘긴다 |
|
|
76
|
+
| `jumpToLatest` | `{ label, icon, onPress() }` | 미게시. 독자가 최신 메시지에서 벗어났을 때만 넘긴다. 작성창 위 끝 쪽에 원형 `IconButton size="small"`(secondary)로 떠 있고, 스크롤 위치 판단은 제품이 한다 |
|
|
75
77
|
|
|
76
78
|
## 축과 기본값
|
|
77
79
|
|
|
78
80
|
| prop | 값 | 기본값 | 설명 |
|
|
79
81
|
| --- | --- | --- | --- |
|
|
80
82
|
| `composer` | `ReactNode` | 필수 | footer에 고정되는 작성창. `ready`와 `empty`에서 보인다(빈 대화의 첫 메시지 입력) |
|
|
83
|
+
| `headerAlignment` | `"center"` · `"start"` | `"center"` | 뒤로·도구 사이에 상대 이름을 가운데 둔다(카카오톡·인스타그램 실측) |
|
|
81
84
|
| `scroll` | `"content"` · `"screen"` | `"content"` | 가상화 목록이 스크롤을 소유하므로 작은 예제 외에는 바꾸지 않는다 |
|
|
82
85
|
| `contentInset` | `"default"` · `"none"` | `"default"` | 이미 gutter를 주는 route 안에서는 `"none"`으로 이중 여백을 막는다 |
|
|
83
86
|
| `state` | `ScreenContentState`(`{ kind: "ready" }` 또는 `{ kind, title, description? }`) | `{ kind: "ready" }` | ready 외에는 본문을 교체한다. composer는 loading·error·restricted에서 숨긴다 |
|
|
@@ -89,7 +92,7 @@ import { ChatScreen, MessageComposer } from "@hjmds/react-native/screens";
|
|
|
89
92
|
| 항목 | 값 | 근거 |
|
|
90
93
|
| --- | --- | --- |
|
|
91
94
|
| 크기 | 폭 최대 `layout.readingMaxWidth` 720, 가운데 정렬; 높이는 host가 준 남은 높이(Web `block-size: 100%`, Native `flex: 1`) | `screenPatternRecipe.maxWidth`, Web `.hjm-screen`, Native `ScreenLayout` |
|
|
92
|
-
| 간격 |
|
|
95
|
+
| 간격 | 헤더·footer padding `spacing.md` 16, 타임라인 본문 좌우 `chatMessageRecipe.timelineInset` 10(미게시, 위아래는 16); 헤더 안 간격 Web 16 / Native `spacing.sm` 12; 메시지 사이 간격은 `ChatMessage`의 `groupPosition`이 소유(타임라인 목록 gap 0) | `screenPatternRecipe.padding`·`itemGap`, Web `.hjm-screen__header` |
|
|
93
96
|
| 순서·정렬 | 헤더 → notice → 타임라인 → composer(footer) | `ScreenLayout` 렌더 순서 |
|
|
94
97
|
| 고정·스크롤 | 헤더·composer 고정, 타임라인이 content 스크롤; footer 위 테두리 1, Web은 하단 safe area만큼 padding을 늘린다 | Web `.hjm-screen__footer`, Native footer `borderTopWidth` |
|
|
95
98
|
| 좁은 폭·큰 글자 | 제목 열 최소 폭 `headerMinWidth` 120 × 글자 배율, 모자라면 actions가 다음 줄로 내려간다; Native safe area·키보드는 host | `screenPatternRecipe.headerMinWidth` |
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# CohortRetentionTable
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.21.0
|
|
7
|
+
- 검토일: 2026-10-16
|
|
8
|
+
- 근거: `src/data-workflows.ts`의 `resolveCohorts`, 양 renderer `src/data-workflows.tsx`, [데이터 해석과 가져오기 구성](../compositions/data-workflows.md)
|
|
9
|
+
- 스토리북: `배포/구성/정보 표시/코호트 잔존율`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
가입 시점(코호트)별로 기간마다 남은 인원 비율을 직사각형 표와 heat 색으로 비교할 때 쓴다. 각 행의 `size`가
|
|
14
|
+
분모이고 `retained[i]`가 기간 i의 관측 인원이다. 아직 관측하지 않은 기간은 `null`(또는 생략)로 두면
|
|
15
|
+
`missingLabel`이 표시된다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 순차 단계 전환율 | [ConversionFunnel](conversion-funnel.md) |
|
|
22
|
+
| 변수끼리 대칭 상관 | [CorrelationMatrix](correlation-matrix.md) |
|
|
23
|
+
| 정렬·선택이 필요한 일반 기록 표 | [DataTable](data-table.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `CohortRetentionTable` | 코호트 잔존율 heat 표 | `@hjmds/react/data-workflows` | `@hjmds/react-native/data-workflows` |
|
|
30
|
+
|
|
31
|
+
행 타입 `Cohort`와 계산 helper `resolveCohorts`는 `@hjmds/design-contracts/data-workflows`에서 import한다.
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { CohortRetentionTable } from "@hjmds/react/data-workflows";
|
|
38
|
+
|
|
39
|
+
<CohortRetentionTable label={t("retention.title")} cohortColumnLabel={t("retention.cohort")}
|
|
40
|
+
periods={periodLabels} cohorts={cohorts} missingLabel={t("retention.unobserved")}
|
|
41
|
+
formatRate={(rate, retained, size) => formatPercent(rate)} />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Native
|
|
46
|
+
import { CohortRetentionTable } from "@hjmds/react-native/data-workflows";
|
|
47
|
+
|
|
48
|
+
<CohortRetentionTable label={t("retention.title")} cohortColumnLabel={t("retention.cohort")}
|
|
49
|
+
periods={periodLabels} cohorts={cohorts} missingLabel={t("retention.unobserved")}
|
|
50
|
+
formatRate={(rate, retained, size) => formatPercent(rate)} />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 축과 기본값
|
|
54
|
+
|
|
55
|
+
| prop | 값 | 기본값 | 설명 |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| label | string | 필수 | 표 접근성 이름(화면에 제목을 그리지 않는다) |
|
|
58
|
+
| cohortColumnLabel | string | 필수 | 첫 열 머리글 |
|
|
59
|
+
| periods | `readonly string[]` | 필수 | 기간 열 머리글. 길이 1 이상이어야 한다 |
|
|
60
|
+
| cohorts | `readonly Cohort[]`(`id`·`label`·`size`·`retained`) | 필수 | `retained`는 `periods`보다 길 수 없고 각 값은 0~`size` 정수 또는 `null` |
|
|
61
|
+
| formatRate | `(rate, retained, size) => string` | 필수 | 관측 칸 문구 |
|
|
62
|
+
| missingLabel | string | 필수 | 미관측 칸·`size` 0 칸 문구 |
|
|
63
|
+
|
|
64
|
+
## 배치
|
|
65
|
+
|
|
66
|
+
| 항목 | 값 | 근거 |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| 크기 | 본문 폭을 채우는 DataTable. 열 = 코호트 이름 + 기간 수 | [DataTable](data-table.md) |
|
|
69
|
+
| 간격 | DataTable 셀 여백(`regular` 세로 12·가로 16) | `dataTableRecipe.density` |
|
|
70
|
+
| 순서·정렬 | 코호트 이름 열(`pinned: "start"`) → 기간 열 순서대로. 행은 `cohorts` 순서 | 양 renderer |
|
|
71
|
+
| 고정·스크롤 | 이름 열을 시작 쪽에 고정. Native는 나머지 열을 가로 `ScrollView`에 넣는다 | [DataTable](data-table.md) |
|
|
72
|
+
| 좁은 폭·큰 글자 | 열을 빼지 않고 이름 열 고정 + 가로 스크롤로 비교를 보존한다 | [DataTable](data-table.md) |
|
|
73
|
+
|
|
74
|
+
## 꼭 지킬 것
|
|
75
|
+
|
|
76
|
+
- heat 색은 비율이 0보다 클 때만 칠하고 강도는 0.35·0.7 경계의 세 단계다. 제품 CSS로 칸 색을 다시 칠하지 않는다.
|
|
77
|
+
- 미관측과 0명을 구분한다. 0명은 `retained`에 0, 미관측은 `null`로 넣는다.
|
|
78
|
+
- 정렬·행 선택은 제공하지 않는 읽기 전용 표다. 정렬이 필요하면 제품이 `cohorts` 순서를 바꾼다.
|
|
79
|
+
|
|
80
|
+
## 상태와 상호작용
|
|
81
|
+
|
|
82
|
+
읽기 전용이다. heat 색은 테마의 dataviz 계열 0번 색(high)을 쓰며 테마 전환을 따른다. 잘못된 분모·기간 수는
|
|
83
|
+
렌더 시 `RangeError`로 막는다.
|