@hjmds/design-contracts 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +25 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +17 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +5 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +376 -0
- package/docs/sheet.md +12 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +7 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +104 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +215 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +138 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +151 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +274 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# 카드 묶음과 긴 목록
|
|
2
|
+
|
|
3
|
+
- 단계: 구성
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Masonry](../../masonry.md), [VirtualList](../../virtual-list.md), [QRCode](../../qr-code.md), `src/masonry.ts`, `src/qr-code-recipe.ts`, `src/foundations.ts`(`layout`), `showcase/web/src/patterns/DataLayouts.stories.tsx`, `showcase/native/src/data-layout-preview.tsx`
|
|
9
|
+
- 스토리북: `배포/구성/정보 표시/카드 묶음과 긴 목록`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
많은 항목을 화면에 늘어놓을 방식을 고를 때 쓴다. 높이가 다른 카드는 Masonry, 수백 개 이상의 같은 높이 행은
|
|
14
|
+
VirtualList, 다른 기기로 넘길 링크는 QRCode다. 세 스토리(카드 묶음·가상 목록 예제·공유 코드)가 각각 하나를 보여 준다.
|
|
15
|
+
|
|
16
|
+
## 구성 요소
|
|
17
|
+
|
|
18
|
+
| 컴포넌트 | 역할 | 지침 |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| Masonry | 높이가 다른 카드를 원래 순서대로 여러 열에 채운다 | [Masonry](../components/masonry.md) |
|
|
21
|
+
| VirtualList | 고정 높이 행을 보이는 범위만 그린다 | [VirtualList](../components/virtual-list.md) |
|
|
22
|
+
| QRCode | 링크를 QR로 보이고 대체 행동을 아래에 둔다 | [QRCode](../components/qr-code.md) |
|
|
23
|
+
| Button / 링크 | QR 대체 행동(Native Button, Web `<a>`) | [Button](../components/button.md), [Link](../components/link.md) |
|
|
24
|
+
|
|
25
|
+
고르는 기준이다.
|
|
26
|
+
|
|
27
|
+
| 상황 | 고를 것 | 고르지 않을 것 |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| 카드 높이가 내용마다 다르고 개수가 수십 개 이하 | Masonry | 수백 개(전부 그린다) |
|
|
30
|
+
| 행 높이가 모두 같고 수백~수천 개 | VirtualList | 행 높이가 내용마다 다를 때 |
|
|
31
|
+
| 휴대폰 카메라로 링크를 넘김 | QRCode + 대체 행동 | 화면 안 이동(그냥 링크) |
|
|
32
|
+
|
|
33
|
+
## 배치
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
카드 묶음 (Masonry, 2열) 가상 목록 (VirtualList) 공유 코드 (QRCode)
|
|
37
|
+
┌ 바깥 틀: 페이지 스크롤 ──────────┐ ┌ 바깥 틀: 스크롤 없음 ───────┐ ┌ 카드·시트 본문 ─┐
|
|
38
|
+
│←gutter→ Container ←gutter→ │ │←gutter→ Container │ │ ▓▓ ▓ ▓▓▓ │
|
|
39
|
+
│ ┌──── width = Container 안쪽 ─┐ │ │ ┌──── height = 남은 높이 ─┐ │ │ ▓ ▓▓▓ ▓ 192 │
|
|
40
|
+
│ │ ┌──────┐ 12 ┌──────┐ │ │ │ │ 항목 1 (64) │ │ │ ▓▓▓ ▓ ▓▓ │
|
|
41
|
+
│ │ │ 1 │ │ 2 │ │ │ │ │ 항목 2 (64) │ │ │ [공유 페이지 열기]│ ← 대체 행동
|
|
42
|
+
│ │ │ │ └──────┘ │ │ │ │ ... 보이는 행 + 앞뒤 3 │ │ └─────────────────┘
|
|
43
|
+
│ │ └──────┘ ┌──────┐ │ │ │ │ ↕ 목록 안 스크롤 │ │
|
|
44
|
+
│ │ ┌──────┐ │ 4 │ │ │ │ └─────────────────────────┘ │
|
|
45
|
+
│ │ │ 3 │ │ │ │ │ └─────────────────────────────┘
|
|
46
|
+
│ └─────────────────────────────┘ │
|
|
47
|
+
│ [ 더 보기 ] LoadMore (선택) │
|
|
48
|
+
└──────────────────────────────────┘
|
|
49
|
+
열 사이·행 사이 12(spacing.sm)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| 바깥 틀 | 카드 묶음: Web 문서 스크롤 > `Container`, Native `ScrollView` > `Container`. 가상 목록: `Container`만(바깥 세로 스크롤 없음). QR: 카드·시트 본문 | 제품 화면이 소유. 상단 안전 영역은 내비게이션 헤더가 맡는다 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)). Native ScrollView 위아래 `paddingVertical: spacing.lg` 20 |
|
|
55
|
+
| 카드 묶음 | Masonry | 본문 흐름 안, 페이지와 함께 스크롤 | `columns` 기본 2(600 이상에서 늘린다), `gap`은 숫자(기본 12 = `spacing.sm`), `width`는 Container 안쪽에 놓인 실제 호스트의 측정 폭 |
|
|
56
|
+
| 카드 | 제품 `renderItem` | 가장 짧은 열 아래 | 높이는 `getItemHeight`가 확정, 안쪽 여백 `spacing.sm` 12 |
|
|
57
|
+
| 다음 페이지 | [LoadMore](../components/load-more.md) | Masonry 다음 형제, 같은 스크롤 | LoadMore 기본 |
|
|
58
|
+
| 가상 목록 | VirtualList | 고정 높이 상자, 목록 안 스크롤 | `height`는 고정 머리·하단 바를 뺀 남은 높이(스토리 400), `rowHeight` 64(예), `overscan` 기본 3 |
|
|
59
|
+
| QR | QRCode | 감싸는 레이아웃이 배치 | `size` 기본 192(모듈 수 정수배로 내림), quiet zone 4모듈 |
|
|
60
|
+
| 대체 행동 | Button / 링크 | QR 바로 아래 | Button 높이 44 |
|
|
61
|
+
|
|
62
|
+
- VirtualList는 그 자체가 세로 스크롤이다. 페이지 스크롤·`ScrollView` 안에 넣지 않는다([VirtualList](../components/virtual-list.md)).
|
|
63
|
+
- Masonry는 스크롤을 갖지 않는다. 바깥 스크롤 안에 둔다.
|
|
64
|
+
|
|
65
|
+
## 흐름과 상태
|
|
66
|
+
|
|
67
|
+
1. 카드 묶음: 화면이 폭을 측정해 `width`로 넘기고, 각 항목 높이를 계산해 넘긴다. 읽기 순서는 열이 아니라 원래 순서다.
|
|
68
|
+
2. 가상 목록: 사용자가 목록 안에서 스크롤하면 보이는 범위와 앞뒤 `overscan` 행만 그린다.
|
|
69
|
+
3. 공유 코드: 카메라로 읽거나, 읽을 수 없으면 아래 대체 행동으로 같은 링크를 연다.
|
|
70
|
+
4. 데이터를 서버에서 받으면 첫 로드·다음 페이지의 진행·실패는 이 구성 바깥(제품 흐름·LoadMore)이 그린다.
|
|
71
|
+
|
|
72
|
+
| 상태 | 모습 | 포커스·알림 |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| 기본 | 각 구성이 항목을 보인다 | 각 컴포넌트 `label`이 목록·QR 이름이 된다 |
|
|
75
|
+
| 진행 중 | 첫 로드는 목록 자리에 제품이 [Skeleton](../components/skeleton.md)을 둔다(Masonry·VirtualList에 로딩 prop은 없다). 다음 페이지는 LoadMore `loading` | 포커스 이동 없음 |
|
|
76
|
+
| 실패 | 첫 로드 실패(네트워크·서버)는 목록 자리에 [Result](../components/result.md)·[Notice](../components/notice.md)와 다시 시도 버튼. 다음 페이지 실패는 LoadMore `error`(이미 받은 항목 유지, 재시도 버튼). 재시도도 실패하면 같은 `error`로 남는다 | Notice·LoadMore 알림, 포커스 이동 없음 |
|
|
77
|
+
| 빈 목록 | Masonry·VirtualList의 `empty` 내용 | — |
|
|
78
|
+
| 큰 글자 | 카드·행 높이가 그대로라 제품이 `getItemHeight`·`rowHeight`를 글자 배율에 맞춰 다시 계산한다 | — |
|
|
79
|
+
|
|
80
|
+
## 코드 골격
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// Web
|
|
84
|
+
import { Masonry } from "@hjmds/react/masonry";
|
|
85
|
+
import { VirtualList } from "@hjmds/react/virtual-list";
|
|
86
|
+
import { QRCode } from "@hjmds/react/qr-code";
|
|
87
|
+
import { Card } from "@hjmds/react/display";
|
|
88
|
+
import { Container, Text } from "@hjmds/react/layout";
|
|
89
|
+
import { spacing } from "@hjmds/design-contracts/foundations";
|
|
90
|
+
|
|
91
|
+
<Container size="content" gutter={gutter}>
|
|
92
|
+
<Masonry items={cards} keyExtractor={(c) => c.id} width={measuredWidth} gap={spacing.sm} label={t("cards.label")}
|
|
93
|
+
getItemHeight={(c, itemWidth) => measureCard(c, itemWidth)}
|
|
94
|
+
renderItem={(c) => <Card title={c.title}>{c.body}</Card>} />
|
|
95
|
+
</Container>
|
|
96
|
+
|
|
97
|
+
<Container size="content" gutter={gutter}>
|
|
98
|
+
<VirtualList items={rows} keyExtractor={(r) => r.id} renderItem={(r) => <Text>{r.label}</Text>}
|
|
99
|
+
rowHeight={64} height={availableHeight} label={t("rows.label")} />
|
|
100
|
+
</Container>
|
|
101
|
+
|
|
102
|
+
<QRCode value={shareUrl} label={t("share.qr")} fallback={<a href={shareUrl}>{t("share.open")}</a>} />
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// Native
|
|
107
|
+
import { useState } from "react";
|
|
108
|
+
import { Linking, ScrollView, View, useWindowDimensions } from "react-native";
|
|
109
|
+
import { Masonry } from "@hjmds/react-native/masonry";
|
|
110
|
+
import { VirtualList } from "@hjmds/react-native/virtual-list";
|
|
111
|
+
import { QRCode } from "@hjmds/react-native/qr-code";
|
|
112
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
113
|
+
import { Card } from "@hjmds/react-native/data-display";
|
|
114
|
+
import { Container, Text } from "@hjmds/react-native/primitives";
|
|
115
|
+
import { spacing } from "@hjmds/design-contracts/foundations";
|
|
116
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
117
|
+
|
|
118
|
+
const { width } = useWindowDimensions();
|
|
119
|
+
const [measuredWidth, setMeasuredWidth] = useState(0);
|
|
120
|
+
const gutter = resolveWindowClass(width) === "compact" ? "compact" : "regular";
|
|
121
|
+
|
|
122
|
+
<ScrollView contentContainerStyle={{ paddingVertical: spacing.lg }}>
|
|
123
|
+
<Container size="content" gutter={gutter}>
|
|
124
|
+
<View onLayout={(event) => setMeasuredWidth(event.nativeEvent.layout.width)}>
|
|
125
|
+
{measuredWidth > spacing.sm ? <Masonry items={cards} keyExtractor={(c) => c.id} width={measuredWidth} gap={spacing.sm}
|
|
126
|
+
label={t("cards.label")} getItemHeight={(c, itemWidth) => measureCard(c, itemWidth)}
|
|
127
|
+
renderItem={(c) => <Card title={c.title}>{c.body}</Card>} /> : null}
|
|
128
|
+
</View>
|
|
129
|
+
</Container>
|
|
130
|
+
</ScrollView>
|
|
131
|
+
|
|
132
|
+
<Container size="content" gutter={gutter}>{/* ScrollView로 감싸지 않는다 */}
|
|
133
|
+
<VirtualList items={rows} keyExtractor={(r) => r.id} renderItem={(r) => <Text>{r.label}</Text>}
|
|
134
|
+
rowHeight={64} height={availableHeight} label={t("rows.label")} />
|
|
135
|
+
</Container>
|
|
136
|
+
|
|
137
|
+
<QRCode value={shareUrl} label={t("share.qr")}
|
|
138
|
+
fallback={<Button onPress={() => void Linking.openURL(shareUrl)}>{t("share.open")}</Button>} />
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
스토리의 1000개 예시 행, 카드 높이 공식(`80 + id % 3 × 40`), 예시 URL은 제품 데이터로 바꾼다. 2026-10-06 검수에서 고정 320(Web)·창 폭 − 32(Native)가 좁은 슬롯과 넓은 창의 Container 제한을 무시하는 것을 확인했다. 예제도 ResizeObserver/onLayout으로 실제 호스트 폭을 측정하고 측정 전에는 Masonry를 그리지 않는다.
|
|
142
|
+
|
|
143
|
+
## 플랫폼 차이
|
|
144
|
+
|
|
145
|
+
| 항목 | Web | Native |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| 폭 측정 | ResizeObserver 등 | 실제 호스트의 `onLayout` |
|
|
148
|
+
| 바깥 스크롤 | 문서 스크롤 | Masonry는 `ScrollView` 안, VirtualList(`FlatList`)는 바깥 없이 |
|
|
149
|
+
| QR 대체 행동 | `<a href>` | Button + `Linking.openURL` |
|
|
150
|
+
| 카드 여백 | `var(--hjm-space-sm)` | `spacing.sm` |
|
|
151
|
+
|
|
152
|
+
## 함정
|
|
153
|
+
|
|
154
|
+
- Masonry `width`를 측정 전 0으로 넘기면 `TypeError`다. 측정이 끝난 뒤 그린다.
|
|
155
|
+
- Masonry `gap`은 토큰 이름이 아니라 숫자다(`gap={spacing.sm}`). `gap="sm"`은 타입 오류다.
|
|
156
|
+
- QR 색은 recipe가 검정/흰색으로 고정한다. 다크 테마에서도 브랜드 색으로 바꾸지 않는다.
|
|
157
|
+
- 카드 표면은 `Surface bordered padding="sm"`, 행 텍스트는 `Text`를 재사용한다. 가상 목록 바깥에 세로 ScrollView를 추가하지 않는다.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# 펼침과 메뉴
|
|
2
|
+
|
|
3
|
+
- 단계: 구성
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Collapsible](../../collapsible.md), [ContextMenu](../../context-menu.md), [Menubar](../../menubar.md), `showcase/web/src/patterns/Disclosure.stories.tsx`, `src/collapsible.ts`, `src/menubar.ts`, `packages/react/src/styles.css`(`.hjm-collapsible`, `.hjm-context-menu`, `.hjm-menubar`)
|
|
9
|
+
- 스토리북: `배포/구성/탐색과 이동/펼침과 메뉴`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
Web에서 내용을 숨겼다 펼치거나(Collapsible), 대상에 붙은 작업 메뉴를 우클릭·키보드로 열거나(ContextMenu),
|
|
14
|
+
데스크톱 앱처럼 상단 메뉴 막대를 두는(Menubar) 세 방식을 각각 보여 주는 모음이다. 스토리 셋은 이어진 흐름이 아니다.
|
|
15
|
+
|
|
16
|
+
| 필요 | 고를 것 | 스토리 |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| 이웃 없는 단독 펼침 하나(배송 정보 더 보기) | `Collapsible` | 단일 펼침 |
|
|
19
|
+
| 펼침 여러 개가 한 묶음, "하나만 열림"·구분선·그룹 키보드 이동이 필요 | [Accordion](../components/accordion.md) | — |
|
|
20
|
+
| 목록 항목·카드에 붙는 보조 작업(이름 바꾸기·삭제) | `ContextMenu` | 포인터 메뉴 |
|
|
21
|
+
| 버튼 하나로 여는 작업 목록 | [Menu](../components/menu.md) | — |
|
|
22
|
+
| 문서 편집기 같은 데스크톱 Web의 파일·편집·도움말 막대 | `Menubar` | 데스크톱 메뉴 막대 |
|
|
23
|
+
|
|
24
|
+
ContextMenu로만 닿는 작업을 두지 않는다. 같은 작업을 보이는 버튼·Menu로도 제공한다.
|
|
25
|
+
|
|
26
|
+
## 구성 요소
|
|
27
|
+
|
|
28
|
+
| 컴포넌트 | 역할 | 지침 |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `Collapsible` | 트리거(문자열) + 펼침 내용. `defaultOpen` 또는 controlled | [Collapsible](../components/collapsible.md) |
|
|
31
|
+
| `ContextMenu` | 자식 요소를 대상으로 우클릭·길게 누르기(500ms)·Shift+F10·메뉴 키로 여는 메뉴 | [ContextMenu](../components/context-menu.md) |
|
|
32
|
+
| `Menubar` | 메뉴 여러 개를 가로로 둔 막대. 항목은 Menu와 같은 `MenuItemDescriptor` | [Menubar](../components/menubar.md) |
|
|
33
|
+
| `Notice` · `Toast` | 작업 결과·실패 알림(스토리의 Notice는 데모) | [Notice](../components/notice.md), [Toast](../components/toast.md) |
|
|
34
|
+
| `Text` | 키보드 사용 안내 | [Text](../components/text.md) |
|
|
35
|
+
| `Stack gap="md"` | 세로 흐름 | [Stack](../components/stack.md) |
|
|
36
|
+
| `Container` | 바깥 틀. 제품 화면이 소유한다 | [Container](../components/container.md) |
|
|
37
|
+
|
|
38
|
+
## 배치
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
┌ 바깥 틀: 문서 스크롤 > Container (gutter 16 | 20) ──────────────────────────┐
|
|
42
|
+
│ 단일 펼침 포인터 메뉴 │
|
|
43
|
+
│ ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ │
|
|
44
|
+
│ │ 배송 정보 더 보기 ▾ │ │ 우클릭하거나 Shift+F10 … (안내) │ │
|
|
45
|
+
│ │ ↕ spacing.xs 8 │ │ ┌ 대상: 2026년 9월 18일 기록 ┐ │ │
|
|
46
|
+
│ │ 주문 다음 날 도착합니다… │ │ └──────────────┬─────────────┘ │ │
|
|
47
|
+
│ └──────────────────────────────────┘ │ ┌ 메뉴(fixed) ───────────┐ │ │
|
|
48
|
+
│ │ │ 이름 바꾸기 F2│ │ │ ← 항목 높이 44
|
|
49
|
+
│ 데스크톱 메뉴 막대 │ │ 복제하기 (흐림) │ │ │
|
|
50
|
+
│ ┌──────────────────────────────────┐ │ │ 삭제하기 (danger) │ │ │ ← 파괴 행동은 맨 끝
|
|
51
|
+
│ │ [파일] [편집] [도움말(흐림)] │ │ └─────────────────────────┘ │ │
|
|
52
|
+
│ │ ┌ 패널 ───────────────┐ │ └──────────────────────────────────┘ │
|
|
53
|
+
│ │ │ 새 기록 ⌘N │ │ ← 막대 높이 44, 좌우 spacing.xs 8 │
|
|
54
|
+
│ │ │ 열기 ⌘O │ │ ← 패널은 라벨 바로 아래, 시작 정렬 │
|
|
55
|
+
│ │ └─────────────────────┘ │ │
|
|
56
|
+
│ └──────────────────────────────────┘ │
|
|
57
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| 바깥 틀 | 문서 스크롤 > `Container` | 제품 화면이 소유. 메뉴 표면은 `position: fixed`라 스크롤 컨테이너에 잘리지 않는다 | 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)). Menubar는 편집 영역 맨 위 |
|
|
63
|
+
| 펼침 트리거 | `Collapsible` trigger | 펼침 내용 위, 꽉 찬 폭, 표시(▸/▾)는 끝 | 최소 높이 `control.minTouchTarget` 44, 위아래 `spacing.xs` 8, 글자–표시 `spacing.sm` 12 |
|
|
64
|
+
| 펼침 내용 | `Collapsible` content | 트리거 바로 아래 | 트리거와 `collapsibleRecipe.gap` `spacing.xs` 8 |
|
|
65
|
+
| 메뉴 대상 | `ContextMenu` 자식 | 제품 레이아웃 소유(스토리의 대상 상자는 showcase CSS) | 포커스 링 2px |
|
|
66
|
+
| 메뉴 표면 | ContextMenu 메뉴 | 포인터 위치에 `position: fixed`, 뷰포트 안 | 최대 폭 `min(24rem, 90vw)`, 최대 높이 뷰포트 − 2×`spacing.md`, padding `spacing.xxs` 4, radius `md` 12. 항목 높이 44, padding `spacing.xs` 8 · `spacing.sm` 12 |
|
|
67
|
+
| 메뉴 막대 | `Menubar` | 화면·편집 영역 상단, 가로, 좁으면 줄바꿈 | 최소 높이 `menubarRecipe.minHeight` 44, 라벨 사이 `spacing.xxs` 4, 좌우 `spacing.xs` 8, 라벨 좌우 `spacing.sm` 12 |
|
|
68
|
+
| 메뉴 패널 | Menubar 패널 | 라벨 바로 아래(`top: 100%`), 시작 정렬 | 최소 폭 13.75rem, 최대 `min(24rem, 90vw)`, 항목 높이 44 |
|
|
69
|
+
| 결과 알림 | `Notice` 또는 `Toast` | 대상·막대 아래 본문, 또는 Toast 자리 | Stack `gap="md"` 16 |
|
|
70
|
+
|
|
71
|
+
- 단독 펼침은 이웃 없이 하나만 둔다. 펼침 여러 개가 이어지면 [Accordion](../components/accordion.md)이다.
|
|
72
|
+
- 메뉴 항목 순서: 자주 쓰는 작업 → 드문 작업 → 파괴 작업(`tone="danger"`)을 맨 끝에 둔다(스토리 순서).
|
|
73
|
+
- 단축키는 항목 끝에 label 크기·보조 색으로 표시된다. 긴 라벨은 줄바꿈되고 잘리지 않는다.
|
|
74
|
+
|
|
75
|
+
## 흐름과 상태
|
|
76
|
+
|
|
77
|
+
1. 단일 펼침: 트리거를 누르면 내용이 열리고 ▸가 ▾로 바뀐다. 다시 누르면 닫힌다.
|
|
78
|
+
2. 포인터 메뉴: 대상에서 우클릭(터치는 500ms 길게 누르기, 키보드는 포커스 후 Shift+F10·메뉴 키) → 메뉴 → 항목 선택 → `onAction(id)`.
|
|
79
|
+
선택하면 메뉴가 닫히고 포커스가 대상으로 돌아온 뒤 제품이 작업을 실행한다.
|
|
80
|
+
3. 메뉴 막대: 라벨을 눌러 열고, 연 상태에서 ←/→로 옆 메뉴로 넘어간다. 비활성 메뉴는 건너뛴다 → 항목 선택 → `onAction(id, menuId)`.
|
|
81
|
+
4. `onAction`은 동기 콜백이다. 서버 작업(삭제·복제)의 진행·실패는 메뉴가 아니라 대상 쪽 제품 UI가 보인다.
|
|
82
|
+
|
|
83
|
+
| 상태 | 모습 | 포커스·알림 |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| 기본 | 펼침은 닫힘(▸) 또는 `defaultOpen`, 메뉴·막대 패널은 닫힘 | 트리거 `aria-expanded`, 내용은 `aria-controls` + `role="region"` |
|
|
86
|
+
| 진행 중 | 메뉴는 이미 닫혀 있다. 같은 작업이 다시 실행되지 않게 진행 중인 항목을 `disabled`로 주고, 대상에는 제품이 진행 표시를 둔다 | 포커스는 대상에 남는다 |
|
|
87
|
+
| 실패 | 서버 작업 실패(네트워크·서버)는 대상 근처 [Notice](../components/notice.md) `tone="danger"` 또는 [Toast](../components/toast.md)로 알리고, 다시 하려면 메뉴를 다시 연다. 재시도도 실패하면 같은 알림을 다시 띄운다 | Notice·Toast 알림, 포커스 이동 없음 |
|
|
88
|
+
| 펼침 열림 | ▾(장식), 내용 표시 | `aria-expanded="true"` |
|
|
89
|
+
| 펼침 비활성 | 트리거 opacity 0.5 | 누를 수 없음 |
|
|
90
|
+
| 메뉴 열림 | 대상 근처 떠 있는 표면 | 메뉴로 포커스가 옮겨지고, 선택·닫기 뒤 대상으로 돌아간다 |
|
|
91
|
+
| 항목 활성(키보드) | 안쪽 1px `content.brand` 테두리 | — |
|
|
92
|
+
| 항목 비활성 | opacity 0.5, 선택 안 됨 | — |
|
|
93
|
+
| 막대 메뉴 열림 | 라벨에 안쪽 brand 테두리 | — |
|
|
94
|
+
|
|
95
|
+
## 코드 골격
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// Web
|
|
99
|
+
import { Collapsible } from "@hjmds/react/collapsible";
|
|
100
|
+
import { ContextMenu } from "@hjmds/react/context-menu";
|
|
101
|
+
import { Menubar } from "@hjmds/react/menubar";
|
|
102
|
+
import { Container, Stack, Text } from "@hjmds/react/layout";
|
|
103
|
+
|
|
104
|
+
<Container size="content" gutter={gutter}>
|
|
105
|
+
<Stack gap="md">
|
|
106
|
+
<Collapsible trigger={t("order.shipping.more")} defaultOpen>
|
|
107
|
+
<Text as="p">{t("order.shipping.body")}</Text>
|
|
108
|
+
</Collapsible>
|
|
109
|
+
|
|
110
|
+
<ContextMenu accessibilityLabel={t("record.actions")} onAction={runRecordAction} items={[
|
|
111
|
+
{ id: "rename", label: t("record.rename"), textValue: t("record.rename"), shortcut: "F2" },
|
|
112
|
+
{ id: "delete", label: t("record.delete"), textValue: t("record.delete"), tone: "danger", disabled: deleting },
|
|
113
|
+
]}>
|
|
114
|
+
<RecordCard record={record} /* 제품 소유 대상 */ />
|
|
115
|
+
</ContextMenu>
|
|
116
|
+
|
|
117
|
+
<Menubar descriptor={{ accessibilityLabel: t("editor.menu"), menus }} onAction={(id, menuId) => run(menuId, id)} />
|
|
118
|
+
</Stack>
|
|
119
|
+
</Container>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Native
|
|
124
|
+
// 없음. ContextMenu·Menubar는 Web 전용이다. Native의 단독 펼침은 `@hjmds/react-native/collapsible`의 Collapsible,
|
|
125
|
+
// 길게 누르기 메뉴는 실험적 `NativeContextMenu`(`/context-menu-native`)가 있다.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
스토리의 항목(파일·편집·도움말, 기록 작업)과 실행 결과 Notice는 데모다. 항목 구성과 실행은 제품 소유다.
|
|
129
|
+
|
|
130
|
+
## 플랫폼 차이
|
|
131
|
+
|
|
132
|
+
| 항목 | Web | Native |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `Collapsible` | `/collapsible`, 배치는 `layoutStyle`(또는 `className`) | `/collapsible`, 배치는 `layoutStyle`(`style`은 deprecated) |
|
|
135
|
+
| 대상 메뉴 | `ContextMenu`(stable) | `NativeContextMenu`(실험적 어댑터) |
|
|
136
|
+
| `Menubar` | 있음 | 없음 |
|
|
137
|
+
|
|
138
|
+
## 함정
|
|
139
|
+
|
|
140
|
+
- `Collapsible`에 `open`과 `defaultOpen`을 함께 주거나 `open`만 주고 `onOpenChange`를 빼면 `TypeError`다.
|
|
141
|
+
- 펼침 여러 개를 Collapsible로 나열하면 구분선·그룹 키보드 이동·"하나만 열림"이 없다. 그런 묶음은 Accordion이다.
|
|
142
|
+
- `ContextMenu` `accessibilityLabel`은 필수다.
|
|
143
|
+
- 현재 단일 펼침 스토리는 Collapsible 두 개(배송·환불)를 `Stack gap="md"`로 이어 둔다. 이웃이 있는 펼침 묶음은 Accordion 기준이므로 따라 하지 않는다.
|
|
144
|
+
- 현재 스토리는 실행 결과 Notice 설명을 `` `${last} 작업을 실행했습니다.` ``처럼 템플릿 문자열로 만들고 문구가 한국어 리터럴이다. 제품은 작업 id→i18n 키 상수 표를 둔다.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# 환경 조합 검증
|
|
2
|
+
|
|
3
|
+
- 단계: 구성
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [HJM Showcase · 환경 도구](../../showcase.md#환경-도구), [DesignSystemProvider](../../design-system-provider.md), `src/showcase.ts`(`showcaseEnvironmentMatrix`, `showcaseScenarios`, `getShowcaseEnvironmentInput`), `showcase/web/src/patterns/EnvironmentMatrix.stories.tsx`, `showcase/web/.storybook/preview.tsx`, `showcase/native/.rnstorybook/preview.tsx`
|
|
9
|
+
- 스토리북: `배포/구성/비교와 검증/환경 조합 검증`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
제품 화면이 테마·쓰기 방향·글자 크기·모션 설정이 달라져도 같은 의미를 유지하는지 확인할 때, 어떤 환경 조합과 검증 항목을 골라 볼지 정하는 기준표로 쓴다.
|
|
14
|
+
|
|
15
|
+
이 스토리는 UI 조립 예가 아니라 비교 기준 모음이다. 모든 조합(곱)을 보지 않고, 위험이 큰 차이를 잡는 다섯 환경과
|
|
16
|
+
컴포넌트가 통과해야 하는 열한 가지 검증 항목을 보여 준다. Native showcase에는 이 스토리가 없고, Native Storybook toolbar가
|
|
17
|
+
같은 네 축(theme·direction·textScale·reducedMotion)을 `HjmNativeProvider`에 넘긴다(`showcase/native/.rnstorybook/preview.tsx`).
|
|
18
|
+
|
|
19
|
+
## 구성 요소
|
|
20
|
+
|
|
21
|
+
| 컴포넌트 | 역할 | 지침 |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `showcaseEnvironmentMatrix` | 다섯 환경 프리셋(테마·방향·글자 배율·모션) | [Showcase](../../showcase.md) |
|
|
24
|
+
| `showcaseScenarios` | 검증 항목 열한 개(id·이름·설명) | [Showcase](../../showcase.md) |
|
|
25
|
+
| `getShowcaseEnvironmentInput` | 프리셋을 Provider 입력(`theme`·`direction`·`textScale`·`reducedMotion`)으로 변환 | [DesignSystemProvider](../components/design-system-provider.md) |
|
|
26
|
+
| `HjmProvider`(Web)·`HjmNativeProvider`(Native) | 제품에서 환경 축을 실제로 바꾸는 곳 | [DesignSystemProvider](../components/design-system-provider.md) |
|
|
27
|
+
|
|
28
|
+
모두 `@hjmds/design-contracts/showcase`에서 import한다. 스토리의 `hjm-page`·`hjm-showcase-grid`·`hjm-showcase-card` 클래스는 showcase 전용 스타일이다.
|
|
29
|
+
|
|
30
|
+
## 배치
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
┌ 바깥 틀: 검증 대상 제품 화면 그대로 ─────────────────────┐
|
|
34
|
+
│ HjmProvider / HjmNativeProvider (환경 입력만 바꾼다) │
|
|
35
|
+
│ ┌ 제품 화면(스크롤·Container·안전 영역은 화면 소유) ───┐ │
|
|
36
|
+
│ │ … │ │
|
|
37
|
+
│ └───────────────────────────────────────────────────────┘ │
|
|
38
|
+
└───────────────────────────────────────────────────────────┘
|
|
39
|
+
|
|
40
|
+
스토리 페이지(참고, showcase 전용 스타일)
|
|
41
|
+
┌──────────────────────────────────────────────────────────┐
|
|
42
|
+
│ Patterns / Evidence matrix / 안내 │
|
|
43
|
+
│ ┌ Environment presets (카드 격자) ─────────────────────┐ │
|
|
44
|
+
│ │ [default] [dark] [large-text] [rtl] [reduced-motion] │ │
|
|
45
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
46
|
+
│ ┌ Story requirements (카드 격자) ──────────────────────┐ │
|
|
47
|
+
│ │ [Contract] [Default] [Dark] [Long copy] [200% text] │ │
|
|
48
|
+
│ │ [RTL] [Reduced motion] [Accessibility] [Keyboard] │ │
|
|
49
|
+
│ │ [Native actions] [Web / Native parity] │ │
|
|
50
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
51
|
+
└──────────────────────────────────────────────────────────┘
|
|
52
|
+
상단 Storybook toolbar: light/dark · LTR/RTL · 100/150/200% · full/reduced
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| 바깥 틀 | `HjmProvider`(Web)·`HjmNativeProvider`(Native) | 검증할 제품 화면의 맨 바깥. 화면의 스크롤·`Container`·안전 영역은 바꾸지 않는다 | 환경 입력만 바꾼다. 여백·크기는 제품 화면 값 그대로. Native는 `safeAreaInsets`도 실제 기기 값으로 넘긴다 |
|
|
58
|
+
| 환경 프리셋 | `showcaseEnvironmentMatrix` 카드 5개 | 스토리 첫 섹션 | showcase 전용 격자(제품 기준 아님) |
|
|
59
|
+
| 검증 항목 | `showcaseScenarios` 카드 11개 | 스토리 둘째 섹션 | showcase 전용 격자 |
|
|
60
|
+
| 환경 전환 | Storybook toolbar | 페이지 밖 상단 | — |
|
|
61
|
+
|
|
62
|
+
### 비교하는 다섯 환경
|
|
63
|
+
|
|
64
|
+
| id | 테마 | 방향 | 글자 배율 | 모션 |
|
|
65
|
+
| --- | --- | --- | --- | --- |
|
|
66
|
+
| `default` | light | LTR | 1 | full |
|
|
67
|
+
| `dark` | dark | LTR | 1 | full |
|
|
68
|
+
| `large-text` | light | LTR | 2 | full |
|
|
69
|
+
| `rtl` | light | RTL | 1 | full |
|
|
70
|
+
| `reduced-motion` | light | LTR | 1 | reduced |
|
|
71
|
+
|
|
72
|
+
근거: `src/showcase.ts`
|
|
73
|
+
|
|
74
|
+
## 흐름과 상태
|
|
75
|
+
|
|
76
|
+
1. 제품 화면을 `default`에서 먼저 확인한다.
|
|
77
|
+
2. 한 축씩 바꿔 `dark` → `large-text` → `rtl` → `reduced-motion` 순으로 본다. 한 번에 한 축만 바꿔야 원인이 갈린다.
|
|
78
|
+
3. 각 환경에서 아래 검증 항목 중 화면에 해당하는 것을 확인한다.
|
|
79
|
+
4. 제품에서 같은 조건을 재현할 때는 `getShowcaseEnvironmentInput(preset)`의 결과를 `HjmProvider`·`HjmNativeProvider`에 넘긴다.
|
|
80
|
+
5. 실패한 항목은 환경 id·검증 항목 id와 함께 기록하고, 고친 뒤 같은 환경에서 다시 본다.
|
|
81
|
+
|
|
82
|
+
| 상태 | 모습 | 포커스·알림 |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| 기본 | `default` 환경(light·LTR·1배·full motion)의 제품 화면 | — |
|
|
85
|
+
| 진행 중 | — (검증 기준표라 진행 상태가 없다. 화면 자체의 로딩 상태는 각 환경에서 따로 확인한다) | — |
|
|
86
|
+
| 실패 | 검증 항목 하나라도 어긋나면 그 환경·항목을 기록한다. 화면의 네트워크·서버 실패 상태도 각 환경에서 한 번 띄워 본다 | — |
|
|
87
|
+
| dark | 의미 색이 어두운 테마에서도 읽힌다 | — |
|
|
88
|
+
| 200% 글자 | 필수 정보가 잘리지 않고 줄바꿈된다 | — |
|
|
89
|
+
| RTL | 논리 시작·끝 배치와 방향 아이콘이 뒤집힌다 | — |
|
|
90
|
+
| 모션 줄이기 | recipe의 reduced-motion 대체 동작을 쓴다 | — |
|
|
91
|
+
| 긴 문구 | 한국어·영어 긴 문구가 잘리거나 의미를 숨기지 않는다 | — |
|
|
92
|
+
| 접근성·키보드 | 이름·상태·관계·대비·터치 영역 확인, 문서화된 키보드 동작·Native host action 실행 | 포커스 순서 확인 |
|
|
93
|
+
|
|
94
|
+
소비자가 고를 기준: 화면에 입력·탐색이 있으면 접근성·키보드(Native는 Native actions)까지, 문구가 바뀌는 화면이면 긴 문구와 200% 글자를,
|
|
95
|
+
아이콘·방향 의존 배치가 있으면 RTL을, 전환·제스처가 있으면 모션 줄이기를 반드시 포함한다. Web·Native가 같은 DOM/view를 만들 필요는 없고,
|
|
96
|
+
같은 의미·상태 전환·접근성 결과를 내면 된다.
|
|
97
|
+
|
|
98
|
+
## 코드 골격
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// Web
|
|
102
|
+
import { showcaseEnvironmentMatrix, getShowcaseEnvironmentInput } from "@hjmds/design-contracts/showcase";
|
|
103
|
+
import { HjmProvider } from "@hjmds/react/provider";
|
|
104
|
+
|
|
105
|
+
const preset = showcaseEnvironmentMatrix.find((env) => env.id === "large-text")!;
|
|
106
|
+
<HjmProvider {...getShowcaseEnvironmentInput(preset)}>
|
|
107
|
+
<ProductScreen />
|
|
108
|
+
</HjmProvider>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// Native
|
|
113
|
+
import { showcaseEnvironmentMatrix, getShowcaseEnvironmentInput } from "@hjmds/design-contracts/showcase";
|
|
114
|
+
import { HjmNativeProvider } from "@hjmds/react-native/provider";
|
|
115
|
+
|
|
116
|
+
const preset = showcaseEnvironmentMatrix.find((env) => env.id === "rtl")!;
|
|
117
|
+
<HjmNativeProvider {...getShowcaseEnvironmentInput(preset)}
|
|
118
|
+
safeAreaInsets={insets /* react-native-safe-area-context useSafeAreaInsets() */}>
|
|
119
|
+
<ProductScreen />
|
|
120
|
+
</HjmNativeProvider>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
이 코드는 테스트·검증 화면용이다. 실제 앱은 환경 축을 지정하지 않고 OS 신호를 따르게 둔다.
|
|
124
|
+
|
|
125
|
+
## 플랫폼 차이
|
|
126
|
+
|
|
127
|
+
| 항목 | Web | Native |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| 이 스토리 | 있음 | 없음(Native Storybook toolbar가 같은 네 축을 Provider에 넘긴다) |
|
|
130
|
+
| 글자 배율 | toolbar 100·150·200% | Provider `textScale`을 명시하면 HJM이 한 번만 적용(OS 배율과 곱하지 않음) |
|
|
131
|
+
| 키보드 검증 | DOM 키 바인딩 | host 접근성 action(`native-actions`) |
|
|
132
|
+
| 안전 영역 | 브라우저 | Provider `safeAreaInsets` |
|
|
133
|
+
|
|
134
|
+
## 함정
|
|
135
|
+
|
|
136
|
+
- 현재 스토리는 HJM 컴포넌트 대신 `hjm-page`·`hjm-showcase-card` 같은 showcase 전용 클래스의 HTML(`h1`·`article`)과 영어 제목을 쓴다. 제품 검증 화면을 만들 때 이 마크업을 따라 하지 않는다(제목은 [Heading](../components/heading.md), 문구는 i18n 키).
|
|
137
|
+
- `getShowcaseEnvironmentInput`의 결과는 펼쳐서(`{...input}`) 넘긴다. 필드를 하나씩 꺼내 넘기면 optional 타입 때문에
|
|
138
|
+
`exactOptionalPropertyTypes`에서 TS2375가 난다(Web·Native 같다).
|
|
139
|
+
- 검증용 Provider로 화면을 감쌀 때 바깥 틀(스크롤·Container·안전 영역)을 따로 바꾸면 실제 화면과 다른 것을 검증하게 된다.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# 중단해도 남는 현재 상태
|
|
2
|
+
|
|
3
|
+
- 단계: 구성
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: `showcase/native/src/ExpoInteractions.stories.tsx`, `src/content-transition.ts`, `src/foundations.ts`(`spacing`, `layout`), `packages/react-native/src/content-transition.tsx`, `packages/react-native/src/internal/recipe-button.tsx`(`selected`)
|
|
9
|
+
- 스토리북: `배포/구성/피드백과 복구/중단해도 남는 현재 상태`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
버튼으로 상태를 빠르게 바꾸거나 전환 도중 내용을 닫아도 현재 상태가 바로 보이고 남아야 하는 영역에 쓴다.
|
|
14
|
+
단계 표시, 상태 카드, 결과 문구처럼 한 영역의 내용이 바뀌는 곳이다. 화면 상태는 애니메이션 완료 콜백을
|
|
15
|
+
기다리지 않고 바뀌고, 전환은 그 뒤를 따라간다.
|
|
16
|
+
|
|
17
|
+
## 구성 요소
|
|
18
|
+
|
|
19
|
+
| 컴포넌트 | 역할 | 지침 |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `ScrollView` · `Container` | 바깥 틀. 제품 화면이 소유한다 | [Container](../components/container.md), [화면 여백](../tokens/layout.md) |
|
|
22
|
+
| `Heading level="level5"`, `Text` | 영역 제목(18)과 설명 | [Heading](../components/heading.md), [Text](../components/text.md) |
|
|
23
|
+
| `Button tone="secondary" selected` | 동작 줄이기 같은 켜고 끄는 설정(토글 버튼) | [Button](../components/button.md) |
|
|
24
|
+
| `Button` `primary`·`secondary`·`ghost` | 다음 상태 · 처음으로 · 내용 닫기/열기 | [Button](../components/button.md) |
|
|
25
|
+
| `Stack` | 영역 세로 쌓기 `gap="md"`, 행동 묶음 `gap="sm"` | [Stack](../components/stack.md) |
|
|
26
|
+
| `ContentTransition` | `stateKey`가 바뀔 때 새 내용을 `preset="rise"`로 들인다 | [ContentTransition](../components/content-transition.md) |
|
|
27
|
+
| `Text` 상태 문구 | 바뀐 상태를 보조기기에 알린다(Android live region + iOS 알림 helper) | [Text](../components/text.md), [상태 알림](action-recovery-save.md#공통-세션과-상태-알림) |
|
|
28
|
+
|
|
29
|
+
## 배치
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
┌ ScrollView (제품 화면 소유) ───────────────────────┐ ← 스크롤 영역(화면 전체)
|
|
33
|
+
│ ↕ 위아래 spacing.lg 20 (contentContainerStyle) │
|
|
34
|
+
│ ←gutter 16|20→ Container ←gutter 16|20→ │
|
|
35
|
+
│ ┌ Stack gap="md" 16 ─────────────────────────────┐ │
|
|
36
|
+
│ │ 제목 (Heading level5) │ │
|
|
37
|
+
│ │ 설명 (Text) │ │
|
|
38
|
+
│ │ [ 동작 줄이기 ] ← secondary + selected 토글 │ │
|
|
39
|
+
│ │ ┌ 행동 묶음 Stack gap="sm" 12 ───────────────┐ │ │
|
|
40
|
+
│ │ │ [ 다음 상태 ] primary ← 주 행동이 맨 위 │ │ │
|
|
41
|
+
│ │ │ [ 처음으로 ] secondary ← 보조 행동 │ │ │
|
|
42
|
+
│ │ │ [ 콘텐츠 닫기 ] ghost │ │ │
|
|
43
|
+
│ │ └────────────────────────────────────────────┘ │ │
|
|
44
|
+
│ │ ┌ ContentTransition (stateKey) ──────────────┐ │ │
|
|
45
|
+
│ │ │ 현재 상태 문구 (live region + iOS 알림) │ │ │
|
|
46
|
+
│ │ └────────────────────────────────────────────┘ │ │
|
|
47
|
+
│ └────────────────────────────────────────────────┘ │
|
|
48
|
+
└────────────────────────────────────────────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| 바깥 틀 | `ScrollView` > `Container size="reading"` | 화면 전체, 스크롤. 상단 안전 영역은 내비게이션 헤더(또는 [TopBar](../components/top-bar.md))가 맡는다. 입력이 없어 키보드 처리는 없다 | 위아래 `contentContainerStyle` `paddingVertical: spacing.lg` 20. 좌우 `Container gutter`: 폭 600 미만 `compact` 16, 이상 `regular` 20([화면 여백](../tokens/layout.md)) |
|
|
54
|
+
| 머리 | `Heading` + `Text` | Stack 맨 위 | 사이 `spacing.md` 16 |
|
|
55
|
+
| 설정 토글 | `Button tone="secondary" selected` | 머리 아래 | 높이 `medium` 44(`control.buttonHeight`). Stack 기본 `align="stretch"`로 꽉 찬 폭 |
|
|
56
|
+
| 행동 묶음 | `Stack gap="sm"` + Button ×3 세로 | 토글 아래, 주 행동이 위 | 버튼 사이 `spacing.sm` 12, 각 높이 44 |
|
|
57
|
+
| 바뀌는 내용 | `ContentTransition` | 행동 묶음 아래, 함께 스크롤 | `rise` 세로 12 이동, 시간 `motion.normal`, 곡선 `easing.enter`. 래퍼 자체 크기·여백 없음 |
|
|
58
|
+
|
|
59
|
+
- 한 영역(구획 하나) 안이므로 요소 사이는 `layout.contentGap`(`Stack gap="md"` 16)이다([화면 여백](../tokens/layout.md)).
|
|
60
|
+
- 한 화면의 primary는 "다음 상태" 하나다. 토글은 `tone="secondary"`로 두고 켜졌을 때만 `selected` 표시(배경 `bg`, 글자·테두리 `contentBrand`)가 된다.
|
|
61
|
+
`selected={false}`인 버튼은 tone 색으로 칠해지므로 tone을 비우면 꺼진 토글이 primary 채움이 된다([Button](../components/button.md) 꼭 지킬 것의 예외 규칙).
|
|
62
|
+
|
|
63
|
+
## 흐름과 상태
|
|
64
|
+
|
|
65
|
+
1. "다음 상태"를 누르면 상태 index가 즉시 바뀌고 `stateKey`가 바뀐다. 빠르게 여러 번 눌러도 마지막 상태가 남는다.
|
|
66
|
+
2. "처음으로"는 전환 중이어도 첫 상태로 바로 돌린다.
|
|
67
|
+
3. "콘텐츠 닫기"는 전환 도중이라도 ContentTransition을 즉시 unmount한다. 다시 열면 현재 상태가 첫 렌더로 보이고 움직이지 않는다.
|
|
68
|
+
4. 동작 줄이기를 켜면 `motion="none"`으로 항상 즉시 교체한다. 기본 `motion="system"`은 Provider의 reduced motion을 따른다.
|
|
69
|
+
5. 앱이 백그라운드로 가면 진행 중 전환을 멈추고 내용을 바로 다 보인다(`AppState` 감시).
|
|
70
|
+
|
|
71
|
+
| 상태 | 모습 | 포커스·알림 |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| 기본 | 현재 상태 문구가 보인다 | 첫 렌더는 알리지 않는다 |
|
|
74
|
+
| 진행 중 | 전환 중. 새 문구가 아래 12에서 올라오며 나타난다. 문구는 이미 확정된 값이다 | 바뀐 문구를 Android live region(polite)·iOS 알림 helper로 알린다 |
|
|
75
|
+
| 실패 | — 이 구성에는 서버 작업이 없다. 서버 작업과 묶으면 그 Button에 `loading`, 실패는 [저장과 재시도](action-recovery-save.md)를 따른다 | 전환 종료를 작업 완료 신호로 쓰지 않는다 |
|
|
76
|
+
| 전환 중 닫힘 | 영역이 즉시 사라진다 | 완료 콜백을 기다리지 않는다 |
|
|
77
|
+
| 동작 줄이기 | 문구가 즉시 교체된다 | 진행 중과 같다 |
|
|
78
|
+
| 앱 백그라운드 | 진행 중 전환을 멈추고 바로 표시한다 | — |
|
|
79
|
+
|
|
80
|
+
- 상태→문구 키는 상수 표(`flowStateKey`)로 둔다. 템플릿 문자열 키(`` `flow.state.${id}` ``)는 키 추출·누락 검사가 찾지 못한다.
|
|
81
|
+
|
|
82
|
+
## 코드 골격
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// Web
|
|
86
|
+
// 없음. Web 스토리는 없고 Web ContentTransition은 같은 stateKey·preset·motion 계약을 쓴다.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// Native
|
|
91
|
+
import { useState } from "react";
|
|
92
|
+
import { ScrollView, useWindowDimensions } from "react-native";
|
|
93
|
+
import { spacing } from "@hjmds/design-contracts/foundations";
|
|
94
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
95
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
96
|
+
import { ContentTransition } from "@hjmds/react-native/content-transition";
|
|
97
|
+
import { Heading } from "@hjmds/react-native/heading";
|
|
98
|
+
import { Container, Stack, Text } from "@hjmds/react-native/primitives";
|
|
99
|
+
|
|
100
|
+
const flowStateKey = ["flow.state.ready", "flow.state.review", "flow.state.done"] as const;
|
|
101
|
+
|
|
102
|
+
function FlowStatus() {
|
|
103
|
+
const [index, setIndex] = useState(0);
|
|
104
|
+
const [visible, setVisible] = useState(true);
|
|
105
|
+
const [reduced, setReduced] = useState(false); // 제품 설정이 없으면 토글과 이 state를 두지 않는다
|
|
106
|
+
const { width } = useWindowDimensions();
|
|
107
|
+
return (
|
|
108
|
+
<ScrollView contentContainerStyle={{ paddingVertical: spacing.lg }}>
|
|
109
|
+
<Container size="reading" gutter={resolveWindowClass(width) === "compact" ? "compact" : "regular"}>
|
|
110
|
+
<Stack gap="md">
|
|
111
|
+
<Heading level="level5" semanticLevel={2}>{t("flow.title")}</Heading>
|
|
112
|
+
<Text>{t("flow.description")}</Text>
|
|
113
|
+
<Button tone="secondary" selected={reduced} onPress={() => setReduced((value) => !value)}>{t("flow.reduceMotion")}</Button>
|
|
114
|
+
<Stack gap="sm">
|
|
115
|
+
<Button onPress={() => setIndex((value) => (value + 1) % flowStateKey.length)}>{t("flow.next")}</Button>
|
|
116
|
+
<Button tone="secondary" onPress={() => setIndex(0)}>{t("flow.reset")}</Button>
|
|
117
|
+
<Button tone="ghost" onPress={() => setVisible((value) => !value)}>{visible ? t("flow.hide") : t("flow.show")}</Button>
|
|
118
|
+
</Stack>
|
|
119
|
+
{visible ? (
|
|
120
|
+
<ContentTransition stateKey={String(index)} preset="rise" motion={reduced ? "none" : "system"}>
|
|
121
|
+
<StatusText>{t(flowStateKey[index] ?? flowStateKey[0])}</StatusText>{/* 저장과 재시도의 공통 절 helper */}
|
|
122
|
+
</ContentTransition>
|
|
123
|
+
) : null}
|
|
124
|
+
</Stack>
|
|
125
|
+
</Container>
|
|
126
|
+
</ScrollView>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
상태 목록·문구·동작 줄이기 설정의 저장 위치는 제품 소유다. 제품에 별도 설정이 없으면 토글을 두지 말고 `motion="system"`만 쓴다.
|
|
132
|
+
`StatusText`는 [저장과 재시도](action-recovery-save.md#공통-세션과-상태-알림)의 helper(Android live region + iOS active일 때 바뀐 문구 알림)다.
|
|
133
|
+
|
|
134
|
+
## 플랫폼 차이
|
|
135
|
+
|
|
136
|
+
| 항목 | Web | Native |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| 스토리 | 없음 | 있음 |
|
|
139
|
+
| 바깥 스크롤 | 문서 스크롤 + `Container` | `ScrollView`(위아래 `spacing.lg`) > `Container` |
|
|
140
|
+
| 포커스 복원 | `focusTarget`으로 전환 전 포커스를 복원 | 없음 |
|
|
141
|
+
| 상태 알림 | `Text role="status"` | `accessibilityLiveRegion`은 Android만, iOS는 `AccessibilityInfo.announceForAccessibilityWithOptions` |
|
|
142
|
+
|
|
143
|
+
## 함정
|
|
144
|
+
|
|
145
|
+
- `stateKey`를 매 렌더 새 값으로 주면 내용이 계속 다시 나타난다. 의미가 바뀔 때만 바꾼다.
|
|
146
|
+
- 다음 상태 계산을 전환 완료 콜백에 묶으면 빠른 연속 누름과 중간 닫기에서 상태가 어긋난다.
|
|
147
|
+
- ContentTransition은 움직이는 동안 래퍼 밖으로 세로 12만큼 밀려 나온다. 바로 아래 요소와 붙이지 않는다.
|
|
148
|
+
- 예제는 `Container gutter="compact"`와 `Stack gap="md"`로 바깥 틀을 공유하고 행동 묶음은 `Stack gap="sm"`으로 둔다. Native 위아래 여백은 `spacing.md` 16이다.
|
|
149
|
+
- 제목은 Heading, 동작 줄이기 토글은 secondary, 작업 결과는 iOS 알림도 처리하는 예제 helper를 쓴다. 제품은 같은 계약을 자체 상태 알림에 연결한다.
|