@hjmds/design-contracts 1.12.1 → 1.13.1
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 +26 -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 +31 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +20 -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 +378 -0
- package/docs/sheet.md +21 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +13 -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 +107 -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 +221 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +146 -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 +157 -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 +279 -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,190 @@
|
|
|
1
|
+
# 끌기·밀기·화면 전환
|
|
2
|
+
|
|
3
|
+
- 단계: 구성
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Optional interaction adapters](../../../../../docs/interaction-adapters.md), `src/interaction-adapters.ts`, `showcase/web/src/patterns/InteractionAdapters.stories.tsx`, `showcase/native/src/InteractionAdapters.stories.tsx`. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/드래그·스와이프·모션`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
순서 바꾸기·행 작업·내용 전환·카드 넘기기·달성 축하·카드 확대 화면 전환 같은 선택형 상호작용 어댑터를 한 화면에서 함께 쓸 때, 각 어댑터를 어디에 놓고 무엇으로 감싸야 하는지 확인하는 구성이다.
|
|
14
|
+
|
|
15
|
+
스토리는 2026-10-06 사용자 승인으로 스토리북 배포됐다. 어댑터 API 자체의 성숙도는 따로다: `sortable`·`swipe-actions`·`content-transition`·`carousel-motion`·`celebration`은
|
|
16
|
+
Web·Native stable, Native `screen-transition`은 experimental이다. 각 어댑터는 root에서 재노출되지 않고 subpath로만 import하며, 제품에 해당 peer를 설치해야 한다.
|
|
17
|
+
|
|
18
|
+
## 구성 요소
|
|
19
|
+
|
|
20
|
+
| 컴포넌트 | 역할 | 지침 |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `SortableCollection` | 짧은 한 열 목록의 순서 바꾸기. 드래그 + 앞/뒤 버튼 경로 | [SortableCollection](../components/sortable-collection.md) |
|
|
23
|
+
| `SwipeActions` | 행 작업(보관·삭제). Native는 스와이프로 버튼을 드러내기만 하고 실행은 버튼으로 | [SwipeActions](../components/swipe-actions.md) |
|
|
24
|
+
| `ContentTransition`·`TextTransition` | 상태가 바뀔 때 내용·문장 전체를 페이드 | [ContentTransition](../components/content-transition.md), [TextTransition](../components/text-transition.md) |
|
|
25
|
+
| `CarouselMotion` | 카드 넘기기. 이전/다음 버튼, 반복·자동 재생 없음 | [Carousel](../components/carousel.md) |
|
|
26
|
+
| `Celebration` | 제품이 확인한 성공 뒤 입자 효과(장식) | [Celebration](../components/celebration.md) |
|
|
27
|
+
| `createHjmTransitionStack`·`SharedTransitionElement`·`SharedTransitionScreen`·`useSharedTransitionOptions` | Native 전용: 목록 카드 → 상세 화면 확대 전환 | [SharedTransitionScreen](../components/shared-transition-screen.md), [SharedTransitionElement](../components/shared-transition-element.md) |
|
|
28
|
+
| `Button`·`Text` | 상태 변경 트리거, 결과 문구 | [Button](../components/button.md), [Text](../components/text.md) |
|
|
29
|
+
|
|
30
|
+
## 배치
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
Native(전체 화면 host) Web(문서 흐름)
|
|
34
|
+
┌ GestureHandlerRootView ─────────────┐ ┌ Container size="reading" ────┐
|
|
35
|
+
│ ░ 상단 안전 영역(insets.top) ░ │ │ Section 즐겨찾기 순서 │
|
|
36
|
+
├─────────────────────────────────────┤ │ ⠿ 숲길 [앞][뒤] │
|
|
37
|
+
│ ScrollView 위아래 spacing.md 16 │ │ ⠿ 바닷가 [앞][뒤] │
|
|
38
|
+
│ └ Container gutter 16/20 │ │ ↕ sectionGap 24 │
|
|
39
|
+
│ Section 즐겨찾기 순서 │ │ Section 목록 작업 │
|
|
40
|
+
│ ⠿ 숲길 [앞][뒤] 행 높이 ≥44│ │ ListRow [보관][삭제] │ ← 버튼 상시 노출
|
|
41
|
+
│ ⠿ 바닷가 [앞][뒤] │ │ 결과 문구(status) │
|
|
42
|
+
│ ↕ sectionGap 24 │ │ Section 내용 전환 │
|
|
43
|
+
│ Section 목록 작업 │ │ [다음 상태] / 전환되는 제목 │
|
|
44
|
+
│ ┌ ListRow ← 스와이프 ──[보관][삭제]┐│ │ Section 카드 탐색 │
|
|
45
|
+
│ └ [작업 보기] 상시 버튼 ─────────┘│ │ [ 카드 ] [이전][다음] │
|
|
46
|
+
│ 결과 문구(live region) │ │ Section 목표 달성 │
|
|
47
|
+
│ Section 내용 전환 / 카드 / 축하 │ │ [기록 달성 축하] + 결과 문구│
|
|
48
|
+
│ ░ 하단 안전 영역(insets.bottom) ░ │ │ (canvas 입자) │
|
|
49
|
+
├─────────────────────────────────────┤ └──────────────────────────────┘
|
|
50
|
+
│ Celebration: 스크롤 밖 화면 오버레이│ ← 터치·접근성 트리에 안 들어감
|
|
51
|
+
└─────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| 바깥 틀 | Native: `GestureHandlerRootView` → `ScrollView` → `Container` → `Stack gap="xl"`. Web: `Container size="reading"` → `Stack gap="xl"` | 화면 루트. Native host가 상·하 안전 영역을 padding으로 받고, 스크롤은 `ScrollView` 하나. 텍스트 입력이 없어 키보드 처리는 없다 | Native `ScrollView` 위아래 `spacing.md` 16, 좌우는 `Container` `gutter`(폭 600 미만 `compact` 16, 이상 `regular` 20). Web 최대 폭 `layout.readingMaxWidth` 720. 구획 사이 `layout.sectionGap` 24(`Stack gap="xl"`) |
|
|
57
|
+
| 구획 제목 | [Section](../components/section.md) `title` | 각 어댑터 묶음 위 | 제목과 본문 사이 `spacing.xs` 8(`sectionRecipe`) |
|
|
58
|
+
| 순서 목록 | `SortableCollection` | 본문, 스크롤 | Native 행 padding·간격 `spacing.xs` 8, 손잡이 줄 최소 높이 44. Web 손잡이 44×44, 행 위아래 `spacing.xs` 8 |
|
|
59
|
+
| 행 작업 | `SwipeActions` + [ListRow](../components/list-row.md) | 목록 행 | 행 높이·여백은 `ListRow`(한 줄 56), 작업 버튼 높이 44·간격 `spacing.xs` 8 |
|
|
60
|
+
| 결과 문구 | `Text`(Web `role="status"`, Native `accessibilityLiveRegion`) | 작업·축하 바로 아래 | 구획 안 간격은 Section이 정한다 |
|
|
61
|
+
| 내용 전환 | `ContentTransition`·`TextTransition` | 바뀌는 내용 자리 | 크기 변화 없음(전환은 투명도·짧은 이동만) |
|
|
62
|
+
| 카드 | `CarouselMotion` | 본문 | Native는 `onLayout`으로 잰 폭 × 제품이 정한 높이, Web은 부모 폭 |
|
|
63
|
+
| 축하 | `Celebration` | Native: 스크롤 **밖** 화면 오버레이(host의 형제), Web: 자체 canvas | 입자 32/64개, 1.6/2.4초(`celebrationRecipe`) |
|
|
64
|
+
|
|
65
|
+
- Native `Celebration`을 스크롤 내용 안에 두면 화면 밖에서 시작해 보이지 않는다(기기 QA 확인). 항상 화면 host의 형제로 둔다.
|
|
66
|
+
- `SwipeActions`는 Web에서 버튼을 바로 보이고, Native에서 스와이프·상시 "작업 보기" 버튼·동작 줄이기 모두 같은 버튼을 드러낸다. 목록 전체가 `openRowId` 하나를 공유한다.
|
|
67
|
+
- 카드 높이·색·이미지는 제품 소유다. 카드 안 제목은 [Heading](../components/heading.md)으로 둔다.
|
|
68
|
+
|
|
69
|
+
근거: `packages/react-native/src/sortable.tsx`, `packages/react/src/sortable.tsx`, `packages/react-native/src/swipe-actions.tsx`, `src/interaction-adapters.ts`(`celebrationRecipe`), `src/foundations.ts`(`layout`)
|
|
70
|
+
|
|
71
|
+
## 흐름과 상태
|
|
72
|
+
|
|
73
|
+
1. 순서: 손잡이를 길게 눌러 끌거나(Native)·스페이스로 잡아 화살표로 옮기거나(Web)·앞/뒤 버튼을 누른다. `onCommit(intent)`의 `orderedIds`를 제품이 먼저 화면에 반영하고 저장한다. 저장·되돌리기는 제품 소유다(`onCommit`은 `void`를 돌려받는다).
|
|
74
|
+
2. 행 작업: Native는 스와이프 또는 "작업 보기"로 버튼을 드러내고 버튼을 눌러 실행한다. 스와이프만으로 삭제하지 않는다. `onAction`이 던지거나 reject하면 `onError`로 받는다.
|
|
75
|
+
3. 내용 전환: 상태 키가 바뀌면 이전 내용은 바로 사라지고 새 내용이 들어온다. 접근성 트리에는 하나만 있다.
|
|
76
|
+
4. 카드: 이전/다음 버튼이나 스와이프로 옮기고, `onCurrentKeyChange`를 제품 state에 반영한다.
|
|
77
|
+
5. 축하: 제품이 성공을 확인한 뒤 새 `eventId`로 한 번 재생한다. 의미는 결과 문구가 전한다.
|
|
78
|
+
6. 화면 전환(Native): 목록 카드와 상세의 `SharedTransitionElement id`를 같게 두고 이동한다. back·제스처 취소는 navigation이 처리한다.
|
|
79
|
+
|
|
80
|
+
| 상태 | 모습 | 포커스·알림 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| 기본 | 순서 목록·행·카드가 정지 상태로 보이고, 결과 문구는 안내 문구(`note.hint`) | — |
|
|
83
|
+
| 진행 중 | 순서 이동 중에는 잡은 행이 따라 움직인다. 순서 저장 중에는 `disabled`, 행 작업 중에는 `busy`로 같은 작업의 중복 실행을 막는다(`onAction`이 끝날 때까지 두 번째 행동도 받지 않는다) | `labels.dragStart`·`position`·`dragCancel` 알림(제품 문구), Web 그룹 `aria-busy` |
|
|
84
|
+
| 실패 | 순서 저장 실패: 제품이 이전 `items`로 되돌리고 실패 문구. 행 작업 실패: `onError` → 실패 문구(`note.actionFailed`), 행은 그대로 남는다. 다시 누르면 같은 작업을 재요청하고, 재요청도 실패하면 같은 문구를 다시 알린다 | 결과 문구 알림(Web `role="status"`, Native live region) |
|
|
85
|
+
| 동작 줄이기 | Native 드래그 끔(앞/뒤 버튼 유지), 스와이프 끔(버튼 노출), 전환·카드는 즉시 이동, 입자 없음 | 의미는 문구로 유지 |
|
|
86
|
+
| 앱 백그라운드 | Native 드래그 취소, 전환·입자 정지 | — |
|
|
87
|
+
|
|
88
|
+
상태→문구 키는 상수 표로 둔다(`const noteStatusKey = { idle: "note.hint", failed: "note.actionFailed", archived: "note.archived" } as const`). 템플릿 문자열 키는 키 추출·누락 검사가 찾지 못한다.
|
|
89
|
+
|
|
90
|
+
## 코드 골격
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Web
|
|
94
|
+
import { Container, Section, Stack, Text } from "@hjmds/react/layout";
|
|
95
|
+
import { ListRow } from "@hjmds/react/display";
|
|
96
|
+
import { Heading } from "@hjmds/react/heading";
|
|
97
|
+
import { SortableCollection } from "@hjmds/react/sortable";
|
|
98
|
+
import { SwipeActions } from "@hjmds/react/swipe-actions";
|
|
99
|
+
import { ContentTransition } from "@hjmds/react/content-transition";
|
|
100
|
+
import { Celebration } from "@hjmds/react/celebration";
|
|
101
|
+
|
|
102
|
+
const noteStatusKey = { idle: "note.hint", failed: "note.actionFailed", archived: "note.archived" } as const;
|
|
103
|
+
const recordKey = { start: "record.start", ready: "record.ready" } as const;
|
|
104
|
+
|
|
105
|
+
<Container size="reading">
|
|
106
|
+
<Stack gap="xl">
|
|
107
|
+
<Section title={t("fav.title")}>
|
|
108
|
+
<SortableCollection items={items} label={t("fav.label")} labels={sortLabels} disabled={savingOrder}
|
|
109
|
+
renderItem={(item) => <strong>{item.label}</strong>} onCommit={(intent) => void commitOrder(intent.orderedIds)} />
|
|
110
|
+
</Section>
|
|
111
|
+
<Section title={t("note.title")}>
|
|
112
|
+
<SwipeActions label={t("note.actions")} actions={[{ id: "archive", label: t("note.archive") }]} busy={busy}
|
|
113
|
+
onAction={runAction} onError={() => setNoteStatus("failed")}>
|
|
114
|
+
<ListRow title={note.title} />
|
|
115
|
+
</SwipeActions>
|
|
116
|
+
<Text as="p" role="status">{t(noteStatusKey[noteStatus])}</Text>
|
|
117
|
+
</Section>
|
|
118
|
+
<ContentTransition stateKey={record}>
|
|
119
|
+
<Heading level="level4" semanticLevel={3}>{t(recordKey[record])}</Heading>
|
|
120
|
+
</ContentTransition>
|
|
121
|
+
</Stack>
|
|
122
|
+
{eventId ? <Celebration eventId={eventId} /> : null}
|
|
123
|
+
</Container>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// Native
|
|
128
|
+
import { ScrollView, View, useWindowDimensions } from "react-native";
|
|
129
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
130
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
131
|
+
import { Container, Section, Stack, Text } from "@hjmds/react-native/primitives";
|
|
132
|
+
import { ListRow } from "@hjmds/react-native/data-display";
|
|
133
|
+
import { useHjmNativeTheme } from "@hjmds/react-native/provider";
|
|
134
|
+
import { SortableCollection } from "@hjmds/react-native/sortable";
|
|
135
|
+
import { SwipeActions } from "@hjmds/react-native/swipe-actions";
|
|
136
|
+
import { CarouselMotion } from "@hjmds/react-native/carousel-motion";
|
|
137
|
+
import { Celebration } from "@hjmds/react-native/celebration";
|
|
138
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
139
|
+
|
|
140
|
+
const { spacing } = useHjmNativeTheme().tokens;
|
|
141
|
+
const insets = useSafeAreaInsets();
|
|
142
|
+
const gutter = resolveWindowClass(useWindowDimensions().width) === "compact" ? "compact" : "regular";
|
|
143
|
+
|
|
144
|
+
<GestureHandlerRootView style={{ flex: 1, paddingTop: insets.top, paddingBottom: insets.bottom }}>
|
|
145
|
+
<ScrollView contentContainerStyle={{ paddingVertical: spacing.md }}>
|
|
146
|
+
<Container gutter={gutter}>
|
|
147
|
+
<Stack gap="xl">
|
|
148
|
+
<Section title={t("fav.title")}>
|
|
149
|
+
<SortableCollection items={items} label={t("fav.label")} labels={sortLabels} disabled={savingOrder}
|
|
150
|
+
renderItem={() => null} onCommit={(intent) => void commitOrder(intent.orderedIds)} />
|
|
151
|
+
</Section>
|
|
152
|
+
<Section title={t("note.title")}>
|
|
153
|
+
<SwipeActions rowId={note.id} label={note.title} actionsLabel={t("note.showActions")}
|
|
154
|
+
openRowId={openRow} onOpenRowChange={setOpenRow} busy={busy}
|
|
155
|
+
actions={[{ id: "archive", label: t("note.archive") }]} onAction={runAction} onError={() => setNoteStatus("failed")}>
|
|
156
|
+
<ListRow title={note.title} />
|
|
157
|
+
</SwipeActions>
|
|
158
|
+
<Text accessibilityLiveRegion="polite">{t(noteStatusKey[noteStatus])}</Text>
|
|
159
|
+
</Section>
|
|
160
|
+
<View onLayout={(e) => setCardWidth(e.nativeEvent.layout.width)}>
|
|
161
|
+
<CarouselMotion width={cardWidth} height={cardHeight} slides={slides} currentKey={slideKey} onCurrentKeyChange={setSlideKey}
|
|
162
|
+
label={t("places.label")} previousLabel={t("places.previous")} nextLabel={t("places.next")} renderSlide={renderCard} />
|
|
163
|
+
</View>
|
|
164
|
+
</Stack>
|
|
165
|
+
</Container>
|
|
166
|
+
</ScrollView>
|
|
167
|
+
{eventId ? <Celebration eventId={eventId} /> : null}{/* 스크롤 밖 */}
|
|
168
|
+
</GestureHandlerRootView>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`sortLabels`·`commitOrder`·`runAction`·카드 내용·높이·색은 제품 소유다. 스토리의 Unsplash 이미지·장소 이름은 예시다.
|
|
172
|
+
|
|
173
|
+
## 플랫폼 차이
|
|
174
|
+
|
|
175
|
+
| 항목 | Web | Native |
|
|
176
|
+
| --- | --- | --- |
|
|
177
|
+
| 순서 이동 엔진 | `@dnd-kit/react` 0.5.0, 키보드(스페이스·화살표·Escape) | `react-native-sortables` 1.10.1, 길게 눌러 끌기 + 조절 접근성 행동 |
|
|
178
|
+
| 행 작업 | 버튼 상시 노출 | 스와이프로 드러냄 + 상시 "작업 보기", `rowId`·`openRowId`·`actionsLabel` 필수 |
|
|
179
|
+
| 카드 크기 | 부모 폭 | 측정한 `width`·`height` 필수 |
|
|
180
|
+
| 축하 위치 | 자체 canvas | 스크롤 밖 화면 오버레이, Skia 필요 |
|
|
181
|
+
| 화면 전환 | 없음(제품 라우터) | `screen-transition`(experimental, React Navigation 7.4.1 + exports patch) |
|
|
182
|
+
| 제스처 host | — | 화면 루트 `GestureHandlerRootView` |
|
|
183
|
+
|
|
184
|
+
## 함정
|
|
185
|
+
|
|
186
|
+
- Native `SortableCollection`은 손잡이 줄에 `item.label`을 이미 그린다. `renderItem`은 `() => null`로 두거나 보조 정보만 그린다.
|
|
187
|
+
- Native 화면 전환은 현재 포트폴리오 Expo 앱(expo-router, React Navigation 미사용)에서 바로 쓸 수 없다. exports patch를 제품 패키지 관리자에 등록해야 한다.
|
|
188
|
+
- 어댑터 peer 없이 subpath를 import하면 자동 대체 없이 기기 Metro 번들이 실패한다.
|
|
189
|
+
- 예제는 Heading·Container·Stack·ListRow를 사용한다. 플랫폼별 드래그/스와이프 host는 유지하고 동일한 배치 역할을 재사용한다.
|
|
190
|
+
- Native 결과 문구는 iOS 알림을 포함한 `PatternStatus` 예제 helper로 표시한다. 제품은 showcase 파일을 import하지 말고 제품 알림 계층에 연결한다.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# 선택 후 적용·취소
|
|
2
|
+
|
|
3
|
+
- 단계: 구성
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [제품 상호작용 품질](../../../../../docs/INTERACTION_QUALITY.md), [Sheet 계약](../../sheet.md), `src/component-recipes.ts`(`sheetRecipe`), `showcase/web/src/patterns/interaction-flow-previews.tsx`, `showcase/native/src/interaction-flow-previews.tsx`. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/상호작용 예제/선택 후 적용·취소`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/구성/선택과 필터/선택 후 적용·취소`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
표시 방식·정렬·필터처럼 시트에서 여러 번 바꿔 본 뒤 적용을 눌러야 화면에 반영되고, 취소하거나 닫으면 기존 선택을 유지해야 할 때 쓴다.
|
|
14
|
+
|
|
15
|
+
누르는 즉시 반영해도 되는 단일 설정은 시트 없이 [SegmentedControl](../components/segmented-control.md)·[ToggleGroup](../components/toggle-group.md)을 쓴다.
|
|
16
|
+
|
|
17
|
+
같은 `상호작용 예제` 묶음: [닫았다 열고 초안 이어쓰기](interaction-flow-draft.md) · [늦은 응답보다 최신 검색 유지](interaction-flow-search.md) ·
|
|
18
|
+
[대표 항목과 묶음 전체 선택](selection-scope.md). 시트를 쓰는 두 예제의 공통 규칙은 이 문서의 [공통: 시트 흐름](#공통-시트-흐름)에,
|
|
19
|
+
상태 문구 알림은 [저장과 재시도의 공통 절](action-recovery-save.md#공통-세션과-상태-알림)에 있다.
|
|
20
|
+
|
|
21
|
+
## 구성 요소
|
|
22
|
+
|
|
23
|
+
| 컴포넌트 | 역할 | 지침 |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `Container`·`Stack` | 바깥 틀. 구획 사이 `gap="xl"`, 묶음 안 `gap="md"` | [Container](../components/container.md), [Stack](../components/stack.md) |
|
|
26
|
+
| `Heading` | 묶음 제목 | [Heading](../components/heading.md) |
|
|
27
|
+
| `Text` 상태 | 현재 적용된 값 `t("display.current", { value })` | [Text](../components/text.md) |
|
|
28
|
+
| `Button` 트리거 | 시트를 연다. 열 때 임시 선택을 적용 값으로 초기화. 화면의 primary 하나 | [Button](../components/button.md) |
|
|
29
|
+
| `Sheet` | 선택 영역. `title`·`closeLabel` 필수, `returnFocusRef`로 트리거에 포커스 복귀 | [Sheet](../components/sheet.md) |
|
|
30
|
+
| `Button selected`(secondary) × N | 선택지. 임시 선택(draft)만 바꾼다 | [Button](../components/button.md) |
|
|
31
|
+
| `Button` 적용 / `Button` 취소(secondary) | 시트 `footer`. 적용만 확정 값을 바꾼다 | [Button](../components/button.md) |
|
|
32
|
+
|
|
33
|
+
## 배치
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
┌ 화면 ────────────────────────────────┐
|
|
37
|
+
│ ░ 상단 안전 영역(화면 셸) ░ │
|
|
38
|
+
│ Native ScrollView 위아래 16 │
|
|
39
|
+
│ └ Container gutter 16/20 │
|
|
40
|
+
│ Heading 제목 │
|
|
41
|
+
│ 현재 표시: 간결하게 (status) │ Stack gap spacing.md 16
|
|
42
|
+
│ [ 표시 방식 선택 ] ← 트리거(primary)│
|
|
43
|
+
└──────────────────────────────────────┘
|
|
44
|
+
▼ 열림(하단 시트, 모달)
|
|
45
|
+
┌ Sheet ───────────────────────────────┐
|
|
46
|
+
│ 표시 방식 선택 [×] │ ← header 고정, 닫기 = 취소와 같음
|
|
47
|
+
├──────────────────────────────────────┤
|
|
48
|
+
│ [ 간결하게 ✓ ] (selected) │ ↑ body 스크롤
|
|
49
|
+
│ [ 자세하게 ] │ │ 선택지 간격 spacing.md 16
|
|
50
|
+
│ [ 크게 보기 ] │ │
|
|
51
|
+
│ 안내: 적용해야 바뀐다 │ ↓
|
|
52
|
+
├──────────────────────────────────────┤
|
|
53
|
+
│ Web: [선택 취소] [선택 적용] │ ← footer 고정, 오른쪽 정렬, gap 12
|
|
54
|
+
│ Native: [ 선택 적용 ] / [ 선택 취소 ] │ ← 세로, 주 행동 먼저
|
|
55
|
+
│ ░░ 하단 안전 영역(시트가 더함) ░░ │
|
|
56
|
+
└──────────────────────────────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
60
|
+
| --- | --- | --- | --- |
|
|
61
|
+
| 바깥 틀 | Native: `ScrollView` → `Container` → `Stack gap="xl"`. Web: `Container` → `Stack gap="xl"` | 화면 본문. 안전 영역은 화면 셸(제품 navigator)이, 시트의 아래 안전 영역은 Sheet가 더한다. 화면에 입력이 없어 키보드 처리는 없다 | Native `ScrollView` 위아래 `spacing.md` 16, 좌우 `Container` `gutter`(폭 600 미만 `compact` 16, 이상 `regular` 20). 구획 사이 `layout.sectionGap` 24 |
|
|
62
|
+
| 제목 | `Heading level="level3"` | 묶음 맨 위 | 24/32(`heading.level3`), 묶음 안 간격 `layout.contentGap` 16(`Stack gap="md"`) |
|
|
63
|
+
| 현재 값 | `Text` | 제목 아래, 트리거 위 | `spacing.md` 16 |
|
|
64
|
+
| 트리거 | `Button`(primary) | 현재 값 아래 | `medium` 44(`control.buttonHeight`) |
|
|
65
|
+
| 시트 머리 | `Sheet` header | 시트 위 고정 | Native 최소 높이 `control.minTouchTarget` 44, 제목–닫기 `spacing.sm` 12; Web 여백 `spacing.lg` 20, 제목–닫기 `spacing.md` 16 |
|
|
66
|
+
| 선택지 | `Button selected` × N | 시트 body, 스크롤 | 높이 44, 간격 `spacing.md` 16. 두 플랫폼 본문이 `sheetRecipe.body.gap`으로 준다(Web은 미게시(1.12.1 이후). 1.12.1 Web 본문은 gap이 없어 `Stack gap="md"`로 감싼다) |
|
|
67
|
+
| 행동 | `Button` 적용·취소 | 시트 footer, 스크롤 밖 고정 | 간격 `spacing.sm` 12(`sheetRecipe.footer.gap`), Native 위 여백 `spacing.sm` 12 |
|
|
68
|
+
| 시트 틀 | `Sheet` | 하단, 모달 | Web 최대 폭 640(`sheetRecipe.web.maxWidth`)·최대 높이 90%, Native 가로 여백 `spacing.lg` 20·최대 높이 90%(`maxHeightRatio` 0.9), 하단 안전 영역 추가 |
|
|
69
|
+
|
|
70
|
+
- footer 순서는 [Sheet](../components/sheet.md) 배치 규칙을 따른다. Web footer는 오른쪽 정렬 가로 줄 **[취소][적용]**(보조 → 주 행동), Native footer는 세로 열이라
|
|
71
|
+
**적용(primary) 먼저, 취소(secondary) 아래**다. 두 renderer의 footer가 이미 `gap` 12를 주므로 버튼을 `Stack`으로 다시 감싸지 않는다.
|
|
72
|
+
- primary 개수: 화면에서는 트리거 하나, 시트 안에서는 적용 하나를 센다. 선택지는 `selected`를 준 secondary라 세지 않는다([Button](../components/button.md) 꼭 지킬 것).
|
|
73
|
+
- 선택지가 단일 선택이면 Button 묶음 대신 [RadioGroup](../components/radio-group.md)도 쓸 수 있다. 의미(임시 선택 → 적용)는 같다.
|
|
74
|
+
|
|
75
|
+
근거: `src/component-recipes.ts`(`sheetRecipe`), `packages/react/src/styles.css`(`.hjm-sheet__footer`, `.hjm-sheet__body`), `packages/react-native/src/overlays.tsx`(Sheet footer `View`), `src/foundations.ts`(`layout`, `heading`)
|
|
76
|
+
|
|
77
|
+
## 흐름과 상태
|
|
78
|
+
|
|
79
|
+
1. 트리거를 누르면 임시 선택을 현재 적용 값으로 초기화하고 시트를 연다.
|
|
80
|
+
2. 선택지를 누르면 임시 선택만 바뀐다(`selected` 표시). 화면 본문의 현재 값은 그대로다.
|
|
81
|
+
3. 적용을 누르면 확정 값을 임시 선택으로 바꾸고 시트를 닫는다.
|
|
82
|
+
4. 취소·닫기(×)·바깥 누름·Escape·Android back은 확정 값을 바꾸지 않고 닫는다.
|
|
83
|
+
5. 닫히면 포커스가 트리거로 돌아온다.
|
|
84
|
+
|
|
85
|
+
| 상태 | 모습 | 포커스·알림 |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| 기본 | 시트 닫힘, 현재 값 + 트리거 | — |
|
|
88
|
+
| 열림 | 임시 선택이 표시된 선택지, footer 두 버튼 | 시트로 포커스 이동(Web focus trap) |
|
|
89
|
+
| 진행 중 | 이 예제는 로컬 상태만 바꿔 진행 중이 없다. 적용을 서버에 저장하면 적용 버튼 `loading`, Sheet `busy`로 닫기를 막는다 | 시트 안 포커스 유지 |
|
|
90
|
+
| 실패 | 서버 저장이 실패하면(네트워크·서버 오류) 시트를 열어 둔 채 임시 선택을 유지하고 body 아래에 실패 문구(`display.applyFailed`)를 보인다. 다시 적용을 누르면 재요청하고, 재요청도 실패하면 같은 문구를 다시 알린다. 확정 값은 성공 전까지 바꾸지 않는다 | 실패 문구 알림(Web `role="status"`, Native live region) |
|
|
91
|
+
| 적용 | 시트 닫힘, 현재 값 갱신 | 트리거로 복귀, 현재 값 문구 알림 |
|
|
92
|
+
| 취소·닫기 | 시트 닫힘, 현재 값 유지 | 트리거로 복귀 |
|
|
93
|
+
|
|
94
|
+
선택지 id→문구 키는 상수 표로 둔다(`displayKey`). `` t(`display.${id}`) ``처럼 템플릿 문자열로 만들면 키 추출·누락 검사가 찾지 못한다.
|
|
95
|
+
서버 저장이 붙는 흐름은 [저장과 재시도](action-recovery-save.md)의 세션을 적용 버튼에 연결한다.
|
|
96
|
+
|
|
97
|
+
## 코드 골격
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// Web
|
|
101
|
+
import { useRef, useState } from "react";
|
|
102
|
+
import { Button } from "@hjmds/react/actions";
|
|
103
|
+
import { Heading } from "@hjmds/react/heading";
|
|
104
|
+
import { Container, Stack, Text } from "@hjmds/react/layout";
|
|
105
|
+
import { Sheet } from "@hjmds/react/overlays";
|
|
106
|
+
|
|
107
|
+
const displayKey = { compact: "display.compact", detailed: "display.detailed", large: "display.large" } as const;
|
|
108
|
+
type DisplayId = keyof typeof displayKey;
|
|
109
|
+
const options = Object.keys(displayKey) as DisplayId[];
|
|
110
|
+
|
|
111
|
+
const [open, setOpen] = useState(false);
|
|
112
|
+
const [applied, setApplied] = useState<DisplayId>("compact");
|
|
113
|
+
const [draft, setDraft] = useState<DisplayId>(applied);
|
|
114
|
+
const trigger = useRef<HTMLButtonElement>(null);
|
|
115
|
+
|
|
116
|
+
<Container gutter={gutter}>
|
|
117
|
+
<Stack gap="xl">
|
|
118
|
+
<Stack gap="md">
|
|
119
|
+
<Heading level="level3" semanticLevel={2}>{t("display.title")}</Heading>
|
|
120
|
+
<Text role="status">{t("display.current", { value: t(displayKey[applied]) })}</Text>
|
|
121
|
+
<Button ref={trigger} onClick={() => { setDraft(applied); setOpen(true); }}>{t("display.choose")}</Button>
|
|
122
|
+
</Stack>
|
|
123
|
+
</Stack>
|
|
124
|
+
<Sheet open={open} onOpenChange={setOpen} title={t("display.choose")} closeLabel={t("common.close")} returnFocusRef={trigger}
|
|
125
|
+
footer={<>
|
|
126
|
+
<Button tone="secondary" onClick={() => setOpen(false)}>{t("display.cancel")}</Button>
|
|
127
|
+
<Button onClick={() => { setApplied(draft); setOpen(false); }}>{t("display.apply")}</Button>
|
|
128
|
+
</>}>
|
|
129
|
+
<Stack gap="md">{/* Web 시트 body에는 gap이 없다 */}
|
|
130
|
+
{options.map((id) => (
|
|
131
|
+
<Button key={id} tone="secondary" selected={draft === id} onClick={() => setDraft(id)}>{t(displayKey[id])}</Button>
|
|
132
|
+
))}
|
|
133
|
+
<Text>{t("display.applyHint")}</Text>
|
|
134
|
+
</Stack>
|
|
135
|
+
</Sheet>
|
|
136
|
+
</Container>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// Native
|
|
141
|
+
import { useRef, useState } from "react";
|
|
142
|
+
import { ScrollView, View, useWindowDimensions } from "react-native";
|
|
143
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
144
|
+
import { Heading } from "@hjmds/react-native/heading";
|
|
145
|
+
import { Sheet } from "@hjmds/react-native/overlays";
|
|
146
|
+
import { Container, Stack } from "@hjmds/react-native/primitives";
|
|
147
|
+
import { useHjmNativeTheme } from "@hjmds/react-native/provider";
|
|
148
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
149
|
+
|
|
150
|
+
const { spacing } = useHjmNativeTheme().tokens;
|
|
151
|
+
const gutter = resolveWindowClass(useWindowDimensions().width) === "compact" ? "compact" : "regular";
|
|
152
|
+
const trigger = useRef<View>(null);
|
|
153
|
+
|
|
154
|
+
<ScrollView contentContainerStyle={{ paddingVertical: spacing.md }}>
|
|
155
|
+
<Container gutter={gutter}>
|
|
156
|
+
<Stack gap="xl">
|
|
157
|
+
<Stack gap="md">
|
|
158
|
+
<Heading level="level3" semanticLevel={2}>{t("display.title")}</Heading>
|
|
159
|
+
<StatusText>{t("display.current", { value: t(displayKey[applied]) })}</StatusText>
|
|
160
|
+
<View ref={trigger} collapsable={false}>
|
|
161
|
+
<Button onPress={() => { setDraft(applied); setOpen(true); }}>{t("display.choose")}</Button>
|
|
162
|
+
</View>
|
|
163
|
+
</Stack>
|
|
164
|
+
</Stack>
|
|
165
|
+
</Container>
|
|
166
|
+
</ScrollView>
|
|
167
|
+
<Sheet open={open} onOpenChange={setOpen} title={t("display.choose")} closeLabel={t("common.close")} returnFocusRef={trigger}
|
|
168
|
+
footer={<>
|
|
169
|
+
<Button onPress={() => { setApplied(draft); setOpen(false); }}>{t("display.apply")}</Button>
|
|
170
|
+
<Button tone="secondary" onPress={() => setOpen(false)}>{t("display.cancel")}</Button>
|
|
171
|
+
</>}>
|
|
172
|
+
{options.map((id) => (
|
|
173
|
+
<Button key={id} tone="secondary" selected={draft === id} onPress={() => setDraft(id)}>{t(displayKey[id])}</Button>
|
|
174
|
+
))}
|
|
175
|
+
</Sheet>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`options`·`displayKey`·문구는 제품 소유다. `StatusText`는 [저장과 재시도의 공통 절](action-recovery-save.md#공통-세션과-상태-알림)의 Native 상태 알림 helper다.
|
|
179
|
+
Web `gutter`는 [화면 여백과 너비](../tokens/layout.md)처럼 폭 구간으로 고른다.
|
|
180
|
+
|
|
181
|
+
### 공통: 시트 흐름
|
|
182
|
+
|
|
183
|
+
[닫았다 열고 초안 이어쓰기](interaction-flow-draft.md)도 이 규칙을 따른다.
|
|
184
|
+
|
|
185
|
+
- 시트 안에서 바뀌는 값(임시 선택·초안)은 **시트 밖**, 시트를 여는 화면의 state에 둔다.
|
|
186
|
+
- 확정은 footer의 주 행동 하나만 한다. 닫기·바깥 누름·back은 확정하지 않는다.
|
|
187
|
+
- footer 버튼은 Web [보조][주], Native 주 → 보조 순서로 바로 넘긴다. renderer footer가 간격 12를 준다.
|
|
188
|
+
- `returnFocusRef`로 닫힌 뒤 포커스를 트리거에 돌려준다. Native는 Button을 `View ref collapsable={false}`로 감싸 ref를 넘긴다.
|
|
189
|
+
- 서버에 저장하는 확정이면 저장 중 Sheet `busy`로 닫기를 막고, 실패하면 시트와 시트 밖 값(임시 선택·초안)을 그대로 둔다.
|
|
190
|
+
- 시트가 닫힌 뒤 다른 모달을 열거나 화면을 옮기면 `onDismissComplete`에서 한다.
|
|
191
|
+
|
|
192
|
+
## 플랫폼 차이
|
|
193
|
+
|
|
194
|
+
| 항목 | Web | Native |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| 트리거 ref | `Button ref` | `View ref collapsable={false}`로 감쌈 |
|
|
197
|
+
| 시트 머리 | 여백 `spacing.lg` 20, 제목–닫기 `spacing.md` 16 | 최소 높이 44, 제목–닫기 `spacing.sm` 12 |
|
|
198
|
+
| footer | 가로 flex(wrap, 끝 정렬), [취소][적용] | 세로 View, 위 여백 `spacing.sm` 12, [적용] 위 [취소] 아래 |
|
|
199
|
+
| 초점 | focus trap + `returnFocusRef` | `returnFocusRef` |
|
|
200
|
+
| 상태 알림 | `role="status"` | live region(Android) + iOS 알림 호출 |
|
|
201
|
+
| 바깥 틀 | `Container` | `ScrollView`(세로 여백) 안 `Container` |
|
|
202
|
+
|
|
203
|
+
## 함정
|
|
204
|
+
|
|
205
|
+
- 구획 제목은 Heading으로 표시한다. 이전 Text heading 예제는 2026-10-06 제목 의미 구조를 맞추면서 수정했다.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# 닫았다 열고 초안 이어쓰기
|
|
2
|
+
|
|
3
|
+
- 단계: 구성
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [제품 상호작용 품질](../../../../../docs/INTERACTION_QUALITY.md), [공통 실행과 실패 복구 · 초안과 실행 취소](../../action-session.md#초안과-실행-취소), [Sheet 계약](../../sheet.md), `showcase/web/src/patterns/interaction-flow-previews.tsx`, `showcase/native/src/interaction-flow-previews.tsx`. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/상호작용 예제/닫았다 열고 초안 이어쓰기`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/구성/입력과 작성/닫았다 열고 초안 이어쓰기`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
메모·댓글처럼 시트에서 쓰던 글을 저장하지 않고 닫았다가 다시 열었을 때, 쓰던 초안을 그대로 이어 쓰게 할 때 쓴다.
|
|
14
|
+
|
|
15
|
+
초안을 버리는 것은 명시적인 "초안 버리기" 행동으로만 한다. 이 예제는 화면이 열린 동안만 초안을 보존한다. 앱 종료 뒤 복구·로그아웃 정리·
|
|
16
|
+
암호화는 제품 저장소가 결정한다.
|
|
17
|
+
|
|
18
|
+
같은 `상호작용 예제` 묶음: [선택 후 적용·취소](interaction-flow-apply.md) · [늦은 응답보다 최신 검색 유지](interaction-flow-search.md) ·
|
|
19
|
+
[대표 항목과 묶음 전체 선택](selection-scope.md). 시트 흐름의 공통 규칙은 [선택 후 적용·취소의 공통 절](interaction-flow-apply.md#공통-시트-흐름)을 따른다.
|
|
20
|
+
|
|
21
|
+
## 구성 요소
|
|
22
|
+
|
|
23
|
+
| 컴포넌트 | 역할 | 지침 |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `Container`·`Stack` | 바깥 틀. 구획 사이 `gap="xl"`, 묶음 안 `gap="md"` | [Container](../components/container.md), [Stack](../components/stack.md) |
|
|
26
|
+
| `Heading` | 묶음 제목 | [Heading](../components/heading.md) |
|
|
27
|
+
| `Button` 트리거 | 작성 시트를 연다. 화면의 primary 하나 | [Button](../components/button.md) |
|
|
28
|
+
| `Text` 상태 | 초안 유무 `t("memo.hasDraft")`·`t("memo.noDraft")`, 저장된 메모 | [Text](../components/text.md) |
|
|
29
|
+
| `Sheet` | 작성 영역. `closeLabel`이 "초안을 남기고 닫기"라는 의미를 말한다. Native는 `keyboardAvoidance`·`scrollable`, 저장 중 `busy` | [Sheet](../components/sheet.md) |
|
|
30
|
+
| `TextField` | 초안 입력. 값은 시트 밖 state. Web·Native 모두 `onValueChange` | [Field](../components/field.md) |
|
|
31
|
+
| `Button` 저장 / `Button` 초안 버리기(secondary) | 시트 `footer`. 저장은 빈 초안이면 `disabled`, 저장 중 `loading` | [Button](../components/button.md) |
|
|
32
|
+
|
|
33
|
+
## 배치
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
┌ 화면 ────────────────────────────────┐
|
|
37
|
+
│ Native ScrollView 위아래 16 │
|
|
38
|
+
│ └ Container gutter 16/20 │
|
|
39
|
+
│ Heading 제목 │
|
|
40
|
+
│ [ 메모 작성 ] ← 트리거(primary) │ Stack gap spacing.md 16
|
|
41
|
+
│ 작성 중인 초안이 있어요 (status) │
|
|
42
|
+
│ 저장된 메모 (제품 소유) │
|
|
43
|
+
└──────────────────────────────────────┘
|
|
44
|
+
▼ 열림(하단 시트, 모달)
|
|
45
|
+
┌ Sheet ───────────────────────────────┐
|
|
46
|
+
│ 메모 작성 [× 초안 남기고 닫기]│ ← header 고정
|
|
47
|
+
├──────────────────────────────────────┤
|
|
48
|
+
│ 메모 내용 │ ↑ body 스크롤(Native scrollable)
|
|
49
|
+
│ [ 입력 ] │ │ TextField 높이 44
|
|
50
|
+
│ 안내 문구 / 저장 실패 문구 │ ↓ 간격 spacing.md 16
|
|
51
|
+
├──────────────────────────────────────┤
|
|
52
|
+
│ Web: [초안 버리기] [메모 저장] │ ← footer 고정, 오른쪽 정렬, gap 12
|
|
53
|
+
│ Native: [ 메모 저장 ] / [ 초안 버리기 ]│ ← 세로, 주 행동 먼저
|
|
54
|
+
│ ░░ 하단 안전 영역 / 키보드 위 ░░ │
|
|
55
|
+
└──────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| 바깥 틀 | Native: `ScrollView` → `Container` → `Stack gap="xl"`. Web: `Container` → `Stack gap="xl"` | 화면 본문. 안전 영역은 화면 셸이, 시트 아래 안전 영역은 Sheet가 더한다. 키보드는 입력이 있는 시트가 처리한다(Native `keyboardAvoidance`) | Native `ScrollView` 위아래 `spacing.md` 16, 좌우 `Container` `gutter`(폭 600 미만 `compact` 16, 이상 `regular` 20). 구획 사이 `layout.sectionGap` 24 |
|
|
61
|
+
| 제목 | `Heading level="level3"` | 묶음 맨 위 | 24/32(`heading.level3`), 묶음 안 `layout.contentGap` 16 |
|
|
62
|
+
| 트리거·상태 | `Button`, `Text` | 제목 아래 | 버튼 `medium` 44, `spacing.md` 16 |
|
|
63
|
+
| 시트 머리 | `Sheet` header | 시트 위 고정 | Native 최소 높이 `control.minTouchTarget` 44 |
|
|
64
|
+
| 입력 | `TextField` | 시트 body, 스크롤 | 높이 `control.fieldHeight` 44, 간격 `spacing.md` 16(Native `sheetRecipe.body.gap`, Web은 `Stack gap="md"`) |
|
|
65
|
+
| 행동 | `Button` 저장·버리기 | 시트 footer, 스크롤 밖 고정 | 간격 `spacing.sm` 12(`sheetRecipe.footer.gap`) |
|
|
66
|
+
| 키보드 | `Sheet keyboardAvoidance`(Native) | 시트 전체가 키보드 위로 | 제품 키보드 listener·maxHeight를 두지 않는다 |
|
|
67
|
+
|
|
68
|
+
- footer 순서는 [Sheet](../components/sheet.md) 배치 규칙을 따른다. Web은 가로 줄 **[버리기][저장]**, Native는 세로 열 **저장(primary) 먼저, 버리기(secondary) 아래**다.
|
|
69
|
+
renderer footer가 간격 12를 주므로 `Stack`으로 다시 감싸지 않는다.
|
|
70
|
+
- 버리기가 되돌릴 수 없는 손실이면 `tone="danger"`와 [InlineConfirm](../components/button.md)으로 한 번 더 확인하는 것을 검토한다.
|
|
71
|
+
|
|
72
|
+
근거: `src/component-recipes.ts`(`sheetRecipe`), `src/foundations.ts`(`control`, `layout`, `heading`), `packages/react/src/styles.css`(`.hjm-sheet__footer`), [Sheet 지침](../components/sheet.md)
|
|
73
|
+
|
|
74
|
+
## 흐름과 상태
|
|
75
|
+
|
|
76
|
+
1. 트리거를 누르면 시트가 열리고, 남아 있던 초안이 입력에 그대로 보인다.
|
|
77
|
+
2. 입력한다. 초안은 시트 밖 state에 쌓인다.
|
|
78
|
+
3. 닫기(×)·바깥 누름·back으로 닫으면 초안은 남고 화면 상태 문구가 "초안 있음"이 된다.
|
|
79
|
+
4. 다시 열면 이어 쓴다.
|
|
80
|
+
5. 저장하면 저장 값을 갱신하고 초안을 비운 뒤 닫는다. 버리기는 초안을 비우고 닫는다.
|
|
81
|
+
|
|
82
|
+
| 상태 | 모습 | 포커스·알림 |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| 기본 | 시트 닫힘, 상태 문구 `t("memo.noDraft")` 또는 `t("memo.hasDraft")` | — |
|
|
85
|
+
| 작성 중(열림) | 입력 + footer, 빈 입력이면 저장 `disabled` | 시트로 포커스 이동 |
|
|
86
|
+
| 진행 중 | 서버 저장 중: 저장 `loading`, Sheet `busy`로 닫기를 막는다. 입력은 그대로 보인다. 이 예제(로컬 저장)에는 없다 | 시트 안 포커스 유지 |
|
|
87
|
+
| 실패 | 저장 실패(네트워크·서버 오류): 시트를 열어 둔 채 초안을 지우지 않고 body 아래 실패 문구(`memo.saveFailed`). 다시 저장을 누르면 재요청하고, 재요청도 실패하면 같은 문구를 다시 알린다 | 실패 문구 알림(Web `role="status"`, Native live region) |
|
|
88
|
+
| 초안 남기고 닫힘 | 상태 문구 `t("memo.hasDraft")` | 트리거로 복귀, 상태 문구 알림 |
|
|
89
|
+
| 저장 | 저장된 메모 갱신, 초안 비움 | 트리거로 복귀 |
|
|
90
|
+
|
|
91
|
+
서버 저장이 붙으면 저장 버튼에 [저장과 재시도](action-recovery-save.md)를 합친다. 현재보다 오래된 성공 응답으로 사용자가 새로 쓴 초안을 지우지 않는다.
|
|
92
|
+
상태→문구 키는 상수 표(`draftStatusKey`)로 둔다. 템플릿 문자열 키는 키 추출·누락 검사가 찾지 못한다.
|
|
93
|
+
|
|
94
|
+
## 코드 골격
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// Web
|
|
98
|
+
import { useRef, useState } from "react";
|
|
99
|
+
import { Button } from "@hjmds/react/actions";
|
|
100
|
+
import { TextField } from "@hjmds/react/forms";
|
|
101
|
+
import { Heading } from "@hjmds/react/heading";
|
|
102
|
+
import { Container, Stack, Text } from "@hjmds/react/layout";
|
|
103
|
+
import { Sheet } from "@hjmds/react/overlays";
|
|
104
|
+
|
|
105
|
+
const draftStatusKey = { empty: "memo.noDraft", kept: "memo.hasDraft", failed: "memo.saveFailed" } as const;
|
|
106
|
+
|
|
107
|
+
const [open, setOpen] = useState(false);
|
|
108
|
+
const [draft, setDraft] = useState(""); // 시트 밖에 둬서 닫아도 남는다
|
|
109
|
+
const trigger = useRef<HTMLButtonElement>(null);
|
|
110
|
+
|
|
111
|
+
<Container gutter={gutter}>
|
|
112
|
+
<Stack gap="xl">
|
|
113
|
+
<Stack gap="md">
|
|
114
|
+
<Heading level="level3" semanticLevel={2}>{t("memo.title")}</Heading>
|
|
115
|
+
<Button ref={trigger} onClick={() => setOpen(true)}>{t("memo.write")}</Button>
|
|
116
|
+
<Text role="status">{t(draft ? draftStatusKey.kept : draftStatusKey.empty)}</Text>
|
|
117
|
+
</Stack>
|
|
118
|
+
</Stack>
|
|
119
|
+
<Sheet open={open} onOpenChange={setOpen} title={t("memo.write")} closeLabel={t("memo.closeKeepDraft")}
|
|
120
|
+
returnFocusRef={trigger} busy={saving}
|
|
121
|
+
footer={<>
|
|
122
|
+
<Button tone="secondary" disabled={saving} onClick={() => { setDraft(""); setOpen(false); }}>{t("memo.discard")}</Button>
|
|
123
|
+
<Button disabled={!draft.trim()} loading={saving} onClick={save}>{t("memo.save")}</Button>
|
|
124
|
+
</>}>
|
|
125
|
+
<Stack gap="md">
|
|
126
|
+
<TextField label={t("memo.body")} value={draft} onValueChange={setDraft} />
|
|
127
|
+
{saveFailed ? <Text role="status">{t(draftStatusKey.failed)}</Text> : null}
|
|
128
|
+
</Stack>
|
|
129
|
+
</Sheet>
|
|
130
|
+
</Container>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// Native
|
|
135
|
+
import { ScrollView, View, useWindowDimensions } from "react-native";
|
|
136
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
137
|
+
import { Heading } from "@hjmds/react-native/heading";
|
|
138
|
+
import { TextField } from "@hjmds/react-native/inputs";
|
|
139
|
+
import { Sheet } from "@hjmds/react-native/overlays";
|
|
140
|
+
import { Container, Stack } from "@hjmds/react-native/primitives";
|
|
141
|
+
import { useHjmNativeTheme } from "@hjmds/react-native/provider";
|
|
142
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
143
|
+
|
|
144
|
+
const { spacing } = useHjmNativeTheme().tokens;
|
|
145
|
+
const gutter = resolveWindowClass(useWindowDimensions().width) === "compact" ? "compact" : "regular";
|
|
146
|
+
|
|
147
|
+
<ScrollView contentContainerStyle={{ paddingVertical: spacing.md }}>
|
|
148
|
+
<Container gutter={gutter}>
|
|
149
|
+
<Stack gap="xl">
|
|
150
|
+
<Stack gap="md">
|
|
151
|
+
<Heading level="level3" semanticLevel={2}>{t("memo.title")}</Heading>
|
|
152
|
+
<View ref={trigger} collapsable={false}>
|
|
153
|
+
<Button onPress={() => setOpen(true)}>{t("memo.write")}</Button>
|
|
154
|
+
</View>
|
|
155
|
+
<StatusText>{t(draft ? draftStatusKey.kept : draftStatusKey.empty)}</StatusText>
|
|
156
|
+
</Stack>
|
|
157
|
+
</Stack>
|
|
158
|
+
</Container>
|
|
159
|
+
</ScrollView>
|
|
160
|
+
<Sheet open={open} onOpenChange={setOpen} title={t("memo.write")} closeLabel={t("memo.closeKeepDraft")}
|
|
161
|
+
returnFocusRef={trigger} keyboardAvoidance scrollable busy={saving}
|
|
162
|
+
footer={<>
|
|
163
|
+
<Button disabled={!draft.trim()} loading={saving} onPress={save}>{t("memo.save")}</Button>
|
|
164
|
+
<Button tone="secondary" disabled={saving} onPress={() => { setDraft(""); setOpen(false); }}>{t("memo.discard")}</Button>
|
|
165
|
+
</>}>
|
|
166
|
+
<TextField label={t("memo.body")} value={draft} onValueChange={setDraft} />
|
|
167
|
+
{saveFailed ? <StatusText>{t(draftStatusKey.failed)}</StatusText> : null}
|
|
168
|
+
</Sheet>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`save`(성공하면 초안을 비우고 닫고, 실패하면 `saveFailed`만 켠다)·`saving`·문구 키는 제품 소유다. `StatusText`는
|
|
172
|
+
[저장과 재시도의 공통 절](action-recovery-save.md#공통-세션과-상태-알림)의 Native 상태 알림 helper다.
|
|
173
|
+
|
|
174
|
+
## 플랫폼 차이
|
|
175
|
+
|
|
176
|
+
| 항목 | Web | Native |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| 키보드 | 브라우저가 처리 | `keyboardAvoidance` + `scrollable`을 함께 켠다 |
|
|
179
|
+
| 입력 이벤트 | `onValueChange`(문자열 값). DOM 이벤트가 필요할 때만 `onChange`(둘 다 호출된다) | `onValueChange` |
|
|
180
|
+
| 트리거 ref | `Button ref` | `View ref collapsable={false}`로 감쌈 |
|
|
181
|
+
| footer | 가로 [버리기][저장] | 세로 [저장] 위 [버리기] 아래 |
|
|
182
|
+
| 시트 body 간격 | 없음 → `Stack gap="md"` | `sheetRecipe.body.gap` 16 |
|
|
183
|
+
| 상태 알림 | `role="status"` | live region(Android) + iOS 알림 호출 |
|
|
184
|
+
|
|
185
|
+
## 함정
|
|
186
|
+
|
|
187
|
+
- 초안 state를 시트 children 안 컴포넌트에 두지 않는다. 닫힌 시트의 내용이 계속 마운트돼 있다고 가정하지 않고, 시트를 여는 화면이 초안을 소유한다.
|
|
188
|
+
- 구획 제목은 Heading으로 표시한다. 이전 Text heading 예제는 2026-10-06 제목 의미 구조를 맞추면서 수정했다.
|