@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,142 @@
|
|
|
1
|
+
# Select
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [cross-platform core](../../cross-platform-core-normalization.md), [Native legacy 제거](../../migration-native-legacy-removal.md), `src/component-recipes.ts`(`selectRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/목록에서 선택`, `배포/컴포넌트/입력/단계별 선택`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
폼 한 칸에서 여러 선택지 중 하나를 고르게 할 때 쓴다. 선택지가 많거나(대략 7개 이상),
|
|
14
|
+
섹션으로 나뉘거나, 서버에서 비동기로 오거나, 화면에 펼쳐 둘 공간이 없을 때 맞다.
|
|
15
|
+
Web은 popover listbox, Native는 modal sheet로 목록을 연다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 2~6개 선택지를 펼쳐서 비교, 항목마다 설명 | [RadioGroup](radio-group.md) |
|
|
22
|
+
| 2~4개 짧은 보기 즉시 전환 | [SegmentedControl](segmented-control.md) |
|
|
23
|
+
| 입력하며 후보를 거름 | [Combobox](combobox.md) |
|
|
24
|
+
| 행동 목록(편집·삭제) | [Menu](menu.md) |
|
|
25
|
+
| 여러 개 선택 | [CheckboxGroup](checkbox-group.md), [TransferList](transfer-list.md) |
|
|
26
|
+
| Web 폼 제출·자동완성·모바일 브라우저 picker가 중요 | `NativeSelect`(아래) |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `Select` | 기본(커스텀 listbox / modal sheet) | `@hjmds/react`, `/forms` | `@hjmds/react-native`, `/forms` |
|
|
33
|
+
| `NativeSelect` | 동반(브라우저 `<select>` 대안) | `@hjmds/react`, `/forms` | — |
|
|
34
|
+
|
|
35
|
+
## 최소 사용 예
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Web
|
|
39
|
+
import { Select } from "@hjmds/react/forms";
|
|
40
|
+
|
|
41
|
+
<Select
|
|
42
|
+
label={t("profile.country.label")}
|
|
43
|
+
placeholder={t("profile.country.placeholder")}
|
|
44
|
+
emptySelectionLabel={t("profile.country.none")}
|
|
45
|
+
items={countries.map((c) => ({ id: c.code, label: t(c.nameKey), textValue: t(c.nameKey) }))}
|
|
46
|
+
selectedKey={country}
|
|
47
|
+
onSelectionChange={setCountry}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// Native
|
|
53
|
+
import { Select } from "@hjmds/react-native/forms";
|
|
54
|
+
|
|
55
|
+
<Select
|
|
56
|
+
label={t("profile.country.label")}
|
|
57
|
+
placeholder={t("profile.country.placeholder")}
|
|
58
|
+
dismissLabel={t("common.close")}
|
|
59
|
+
items={countries.map((c) => ({ id: c.code, label: t(c.nameKey), textValue: t(c.nameKey) }))}
|
|
60
|
+
selectedKey={country}
|
|
61
|
+
onSelectionChange={setCountry}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// Web
|
|
67
|
+
// 대안 — 브라우저 기본 select
|
|
68
|
+
import { NativeSelect } from "@hjmds/react/forms";
|
|
69
|
+
|
|
70
|
+
<NativeSelect name="country" label={t("profile.country.label")}
|
|
71
|
+
options={[{ value: "kr", label: t("country.kr") }]} value={code} onValueChange={setCode} />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 축과 기본값
|
|
75
|
+
|
|
76
|
+
| prop | 값 | 기본값 | 설명 |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
| `items` / `sections` | `readonly { id, label: string, textValue: string, description?, disabled? }[]` / 섹션 `{ id, label?, accessibilityLabel?, items }` | — | 항목 필수 필드는 `id`·`label`·`textValue`. Native는 `source`도 가능 |
|
|
79
|
+
| `size` | `medium` · `large` | `medium` | 트리거 높이 44 · 52 |
|
|
80
|
+
| `density` | `compact` · `comfortable` | `comfortable` | 선택지 행 44 · 56 |
|
|
81
|
+
| `selectedKey` · `defaultSelectedKey` | 키 · `null` | 비제어 `null` | 제어하면 Web은 `onSelectionChange` 필수 |
|
|
82
|
+
| `onSelectionChange` | `(key: Key \| null) => void` | — | 선택 해제 허용이 기본이라 `null`이 올 수 있다 |
|
|
83
|
+
| `onSelectionAfterDismiss`(Native) | `(value: Key) => void \| Promise<void>` | — | sheet가 실제로 닫힌 뒤. 화면 이동·다른 modal은 여기서 |
|
|
84
|
+
| `disallowEmptySelection` | `boolean` | `false` | — |
|
|
85
|
+
| `asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error", message: string }` | `{ status: "idle" }` | 비동기 상태를 그린다. 목록이 아직 없는 동안 선택된 항목은 `selectedItem` |
|
|
86
|
+
| `open` · `defaultOpen` | `boolean` | 비제어 `false` | 제어하면 Web은 `onOpenChange` 필수 |
|
|
87
|
+
| `onOpenChange` | `(open: boolean, reason) => void` | — | `reason`: `trigger` · `keyboard` · `selection` · `escape` · `outside` · `blur` · `programmatic` |
|
|
88
|
+
| `renderLeading` | `(item \| null, appearance: { color, size }) => ReactNode` | — | 트리거 앞 아이콘(선택 없으면 `item`이 `null`). Web `color`는 `"currentColor"` |
|
|
89
|
+
| `renderOptionLeading` | `(item, appearance: { color, size, selected, highlighted, disabled }) => ReactNode` | — | 선택지 앞 아이콘 |
|
|
90
|
+
| `busy`, `readOnly`, `required`, `disabled` | `boolean` | `false` | `busy`는 포커스 순서를 유지한 채 조작을 막는다. `disabled`는 라벨과 트리거만 `selectRecipe.states.disabledOpacity`(0.5)로 흐리고 도움말·오류는 그대로 둔다([Field](field.md)). NativeSelect는 Field 기본값 0.6 |
|
|
91
|
+
| `label` / `accessibilityLabel` | 문자열 | — | 둘 중 하나 필수 |
|
|
92
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 필드 전체(라벨·트리거·설명) 배치. Web `style`은 트리거 버튼에 붙는다 |
|
|
93
|
+
| `options`(NativeSelect) | `readonly { value: string; label: string; disabled? }[]` | 필수 | 브라우저 `<select>` 대안 |
|
|
94
|
+
| `onValueChange`(NativeSelect) | `(value: string) => void` | — | 원시 `onChange`도 받는다 |
|
|
95
|
+
|
|
96
|
+
## 배치
|
|
97
|
+
|
|
98
|
+
| 항목 | 값 | 근거 |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| 크기 | 트리거 최소 높이 `medium` 44 · `large` 52, 폼 열 폭을 채운다(Web `inline-size: 100%`). 선택지 행 `comfortable` 56 · `compact` 44. Web 목록 최대 폭 420(`min(26.25rem, 100vw − 2×spacing.md)`)·최대 높이 360. Native sheet 최대 높이 75% | `fieldFrameContract.minHeight`, `control.buttonHeight.large`, `selectRecipe.density`·`popover`, `.hjm-select__listbox`, Native `forms.tsx` |
|
|
101
|
+
| 간격 | 트리거 좌우 `medium` `spacing.md` 16 · `large` `spacing.lg` 20(recipe), 안쪽 요소 사이 `spacing.sm` 12. 라벨·설명·오류와 `spacing.xs` 8. Web 목록은 트리거에서 8 떨어지고 안쪽 여백 `spacing.xs` 8(`selectRecipe.popover.padding`; 2026-10-06까지 4, 1.12.1 이후 미게시), 화면 가장자리에서 8(`selectRecipe.popover.collisionPadding`). Native sheet 안쪽 `spacing.md` 16, 머리와 목록 사이 `spacing.sm` 12 | `selectRecipe.sizes`·`value.gap`, `formSupportContract.gap`, `selectRecipe.popover`(sideOffset 8, collisionPadding 8, padding 8), Native `forms.tsx` |
|
|
102
|
+
| 순서·정렬 | 트리거: 앞 아이콘 → 값(넘치면 말줄임) → 펼침 표시. Web 목록은 트리거 아래·트리거와 같은 폭, 공간이 없으면 위로 뒤집힌다. Native sheet 머리는 제목 + 끝쪽 닫기 ×(44). 확인 버튼은 없고 항목을 누르면 선택하고 닫힌다 | `.hjm-select__trigger`, `select.tsx`(`matchAnchorWidth`), `CollectionSheetHeader` |
|
|
103
|
+
| 고정·스크롤 | Web 목록은 `position: fixed` popover(z-index `layer.dropdown` 400), 넘치면 목록 안에서 스크롤. Native는 화면 아래에서 올라오는 modal sheet로 목록만 스크롤되고, 아래 여백에 안전 영역(`safeArea.bottom`)을 더한다. 배경(scrim)을 누르면 닫힌다 | `.hjm-select__listbox`, Native `Select`(Modal·ScrollView) |
|
|
104
|
+
| 좁은 폭·큰 글자 | 트리거 높이는 최소값이라 큰 글자에서 늘어나고 값은 한 줄 말줄임이다. Web 목록 폭은 화면 폭 − 32를 넘지 않는다 | `.hjm-select__value`, `.hjm-select__listbox` |
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Web popover Native sheet
|
|
108
|
+
라벨 ┌──────── scrim (누르면 닫힘) ────────┐
|
|
109
|
+
┌──────────────────────┐ │ │
|
|
110
|
+
│ 선택된 값 ⌄ │ 44 ├─────────────────────────────────────┤ radius.lg 16
|
|
111
|
+
└──────────────────────┘ │ 정렬 기준 (×) │ ← 머리, × 44
|
|
112
|
+
↓ 8 │ gap spacing.sm 12 │
|
|
113
|
+
┌──────────────────────┐ │ ┌─────────────────────────────────┐ │
|
|
114
|
+
│ 최신순 ✓ │ 56 │ │ 최신순 ✓ │ │ 56 ↕ 스크롤
|
|
115
|
+
│ 인기순 │ │ │ 인기순 │ │
|
|
116
|
+
└──────────────────────┘ max 360 │ └─────────────────────────────────┘ │
|
|
117
|
+
트리거 폭, max 420 │ padding 16 + 안전 영역 │ max 75%
|
|
118
|
+
└─────────────────────────────────────┘
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 꼭 지킬 것
|
|
122
|
+
|
|
123
|
+
- 모든 문구(`placeholder`, Web `emptySelectionLabel`, Native `dismissLabel`)는 i18n 키로 넣는다. 비면 `TypeError`.
|
|
124
|
+
- 항목이 0개인데 `asyncState`가 `idle`이면 오류를 던진다. 로딩·빈 결과는 `asyncState`로 알린다.
|
|
125
|
+
controlled 키가 목록에 없으면(그리고 `selectedItem`도 아니면) Native는 `RangeError`다.
|
|
126
|
+
- 옛 Native `options`·`value`·`onValueChange`는 1.11에서 제거됐다. `items`/`selectedKey`/`onSelectionChange`를 쓴다.
|
|
127
|
+
- Native 선택 후 화면 이동·다른 modal 열기는 `onSelectionAfterDismiss`에서 한다. sheet가 닫히기 전에
|
|
128
|
+
다음 modal을 띄우지 않는다.
|
|
129
|
+
- 배치는 `layoutStyle`로 한다. Web `className`·`style`은 트리거 버튼에 붙는다. Native `style`은 deprecated(개발 모드 경고,
|
|
130
|
+
다음 major 제거)다.
|
|
131
|
+
|
|
132
|
+
## 플랫폼 차이
|
|
133
|
+
|
|
134
|
+
| 항목 | Web | Native |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| 목록 표면 | anchored popover(`align`, `portalContainer`) | modal sheet(`dismissLabel` 필수) |
|
|
137
|
+
| 컬렉션 입력 | `items` 또는 `sections` | `source`·`items`·`sections` 중 정확히 하나 |
|
|
138
|
+
| 선택 해제 항목 문구 | `emptySelectionLabel` 필수 | 없음 |
|
|
139
|
+
| 재시도 | 없음 | `onRetry`, `retryLabel` |
|
|
140
|
+
| 낭독 보조 | 없음 | `readOnlyLabel`, `openHint`, `optionsAccessibilityLabel` |
|
|
141
|
+
| 키보드 | `loop`, typeahead(`locale`) | 해당 없음 |
|
|
142
|
+
| `description`·`error` 타입 | `ReactNode` | `string` |
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# SettingsScreen
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
|
|
9
|
+
- 스토리북: `배포/화면/설정/앱 설정`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
앱의 설정 화면 전체에 쓴다. 제목, 선택적 프로필 영역, id가 있는 섹션 목록을 받아
|
|
14
|
+
[ScreenLayout](screen-layout.md) 안에 [Section](section.md)으로 쌓는다. 섹션 안의 행은
|
|
15
|
+
기존 ListRow·Switch·Select·Field를 그대로 넣는다.
|
|
16
|
+
|
|
17
|
+
2026-09-23 소비 감사에서 제품마다 같은 설정 화면을 직접 조립하고 있었다. 제목·섹션 틀·구분선·
|
|
18
|
+
상태 교체를 제품이 다시 만들지 않는다.
|
|
19
|
+
|
|
20
|
+
## 쓰지 않을 때
|
|
21
|
+
|
|
22
|
+
| 상황 | 대신 쓸 것 |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| 섹션 구조가 아닌 일반 화면 | [ScreenLayout](screen-layout.md) |
|
|
25
|
+
| 프로필 보기·편집 화면 | [ProfileScreen](profile-screen.md) |
|
|
26
|
+
| 설정 한 항목을 편집하는 입력 패널 | [Sheet](sheet.md) |
|
|
27
|
+
| 섹션 하나만 필요(다른 화면 안) | [Section](section.md) |
|
|
28
|
+
|
|
29
|
+
## 공개 이름과 import
|
|
30
|
+
|
|
31
|
+
| 이름 | 역할 | Web | Native |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| `SettingsScreen` | supplemental. root에서는 내보내지 않는다 | `/screens` | `/screens` |
|
|
34
|
+
|
|
35
|
+
## 최소 사용 예
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Web — Switch는 /selection, ListRow는 /display, 행 이동은 onClick(또는 href)
|
|
39
|
+
import { SettingsScreen } from "@hjmds/react/screens";
|
|
40
|
+
import { Switch } from "@hjmds/react/selection";
|
|
41
|
+
import { ListRow } from "@hjmds/react/display";
|
|
42
|
+
|
|
43
|
+
<SettingsScreen
|
|
44
|
+
title={t("settings.title")}
|
|
45
|
+
sections={[
|
|
46
|
+
{ id: "notifications", title: t("settings.notifications"), children: (
|
|
47
|
+
<Switch
|
|
48
|
+
presentation="row"
|
|
49
|
+
label={t("settings.push.label")}
|
|
50
|
+
description={t("settings.push.description")}
|
|
51
|
+
checked={pushEnabled}
|
|
52
|
+
onCheckedChange={setPushEnabled}
|
|
53
|
+
/>
|
|
54
|
+
) },
|
|
55
|
+
{ id: "account", title: t("settings.account"), children: (
|
|
56
|
+
<ListRow title={t("settings.language")} description={currentLanguageName} onClick={openLanguageSheet} />
|
|
57
|
+
) },
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// Native — Switch는 /inputs, ListRow는 /data-display, 행 이동은 onPress
|
|
64
|
+
import { SettingsScreen } from "@hjmds/react-native/screens";
|
|
65
|
+
import { Switch } from "@hjmds/react-native/inputs";
|
|
66
|
+
import { ListRow } from "@hjmds/react-native/data-display";
|
|
67
|
+
|
|
68
|
+
<SettingsScreen
|
|
69
|
+
title={t("settings.title")}
|
|
70
|
+
sections={[
|
|
71
|
+
{ id: "notifications", title: t("settings.notifications"), children: (
|
|
72
|
+
<Switch
|
|
73
|
+
presentation="row"
|
|
74
|
+
label={t("settings.push.label")}
|
|
75
|
+
description={t("settings.push.description")}
|
|
76
|
+
checked={pushEnabled}
|
|
77
|
+
onCheckedChange={setPushEnabled}
|
|
78
|
+
/>
|
|
79
|
+
) },
|
|
80
|
+
{ id: "account", title: t("settings.account"), children: (
|
|
81
|
+
<ListRow title={t("settings.language")} description={currentLanguageName} onPress={openLanguageSheet} />
|
|
82
|
+
) },
|
|
83
|
+
]}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 축과 기본값
|
|
88
|
+
|
|
89
|
+
| prop | 값 | 기본값 | 설명 |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `sections` | `readonly { id: string; title: string; description?: string; children: ReactNode }[]` | 필수 | 각 항목을 Section 하나로 그린다. `id`는 안정적인 key |
|
|
92
|
+
| `profile` | `ReactNode` | 없음 | 섹션 위 프로필 영역 |
|
|
93
|
+
| 나머지 | `ScreenLayout`과 같음(`children`·`scroll` 제외) | — | `title` 필수, `state`·`stateAction`·`notice`·`footer`·`header`·`leading`·`actions`·`contentInset`. 화면 전체가 스크롤한다 |
|
|
94
|
+
|
|
95
|
+
두 플랫폼 모두 섹션 내용 위에 1px 구분선을 그리고 회색 카드 배경은 쓰지 않는다(2026-10-05 사용자 결정).
|
|
96
|
+
|
|
97
|
+
## 배치
|
|
98
|
+
|
|
99
|
+
| 항목 | 값 | 근거 |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| 크기 | ScreenLayout 폭(최대 720) 안에서 섹션을 세로로 쌓는다; 행 높이는 ListRow·Switch row 계약 | `ScreenLayout`, `Section` |
|
|
102
|
+
| 간격 | 화면 padding `spacing.md` 16; `profile`·섹션 사이 `spacing.xl` 24; 섹션 제목–내용 `spacing.xs` 8, 제목–설명 `spacing.xxs` 4; 행 사이 간격은 없고 구분선 1 | Web·Native `SettingsScreen` `Stack gap="xl"`, `sectionRecipe` |
|
|
103
|
+
| 순서·정렬 | 헤더(제목) → `notice` → `profile` → `sections` 순서대로 → `footer` | 렌더 순서 |
|
|
104
|
+
| 고정·스크롤 | 헤더·footer 고정, 본문 화면 스크롤(`scroll` 고정 `"screen"`); 저장 실패는 `notice` | `ScreenLayout` |
|
|
105
|
+
| 좁은 폭·큰 글자 | 섹션 제목·행 문구는 줄바꿈되고 자르지 않는다; 제목 열 최소 폭 120 × 글자 배율 | `.hjm-section` `overflow-wrap: anywhere`, `headerMinWidth` |
|
|
106
|
+
|
|
107
|
+
## 꼭 지킬 것
|
|
108
|
+
|
|
109
|
+
- **켜고 끄는 행은 `Switch presentation="row"`로 만든다.** label·description·checked·onCheckedChange를
|
|
110
|
+
넘기면 행 전체가 switch 하나다. Pressable·button·ListRow `onPress`로 다시 감싸지 않는다.
|
|
111
|
+
Web 기본은 `inline`, Native 기본은 `row`이므로 Web·Native 같은 화면에서는 `presentation`을 명시한다.
|
|
112
|
+
- ListRow의 trailing에 Switch만 둘 때는 `labelVisibility="hidden"`을 쓰고 그 ListRow에는 `onPress`를 두지 않는다.
|
|
113
|
+
- 현재 선택값(언어·테마)은 행 `description`에 두고, 선택은 [Select](select.md) 또는 [Sheet](sheet.md)로 연다.
|
|
114
|
+
- 섹션 `id`는 안정적인 키로 둔다(번역 문구를 id로 쓰지 않는다).
|
|
115
|
+
- 저장 방식·낙관적 갱신·실패 복구·탈퇴 확인은 제품과 [action-session](../../action-session.md)이 소유한다.
|
|
116
|
+
저장 실패는 `state`가 아니라 `notice`로 알린다.
|
|
117
|
+
- 배치: Web·Native 모두 `layoutStyle`(Web은 `className`도 있다). 섹션 색·여백을 덮지 않는다.
|
|
118
|
+
|
|
119
|
+
## 플랫폼 차이
|
|
120
|
+
|
|
121
|
+
| 항목 | Web | Native |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| 섹션 구분 | Section(`hjm-settings-section`) 내용 위 1px 구분선(CSS) | Section + 내용 위 1px 구분선(`View`) |
|
|
124
|
+
| 조각 import | `Switch` `/selection`, `ListRow` `/display` | `Switch` `/inputs`, `ListRow` `/data-display` |
|
|
125
|
+
| Switch 기본 `presentation` | `inline` | `row` |
|
|
126
|
+
| 행 이동 이벤트 | ListRow `href`/`onClick` | ListRow `onPress` |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SharedTransitionElement
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Optional interaction adapters · Shared screen transition](../../../../../docs/interaction-adapters.md#shared-screen-transition)(Native 전용, experimental, supplemental, 카탈로그 계약 없음), `packages/react-native/src/screen-transition.tsx`, 시연은 구성 스토리 `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환` › 카드 확대와 화면 전환. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
|
|
9
|
+
- 스토리북: `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
목록의 카드(사진·썸네일)를 눌러 상세 화면으로 갈 때, 같은 요소가 두 화면 사이에서 확대·축소되어
|
|
14
|
+
이어지는 공유 요소 전환에 쓴다. 출발 화면과 도착 화면에 **같은 `id`** 로 하나씩 둔다.
|
|
15
|
+
[SharedTransitionScreen](shared-transition-screen.md), `useSharedTransitionOptions`, `createHjmTransitionStack`과
|
|
16
|
+
한 묶음으로만 동작한다.
|
|
17
|
+
|
|
18
|
+
2026-09-30 기준 포트폴리오 Expo 앱은 모두 expo-router 57을 React Navigation 없이 쓰므로 이 경로를
|
|
19
|
+
쓸 수 있는 소비 앱이 아직 없다. 도입 전에 아래 전제를 모두 충족하는지 먼저 확인한다.
|
|
20
|
+
|
|
21
|
+
## 쓰지 않을 때
|
|
22
|
+
|
|
23
|
+
| 상황 | 대신 쓸 것 |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| 같은 화면 안에서 내용만 바뀜 | [ContentTransition](content-transition.md) |
|
|
26
|
+
| Web 화면 전환 | 없음. 제품 router를 쓴다 |
|
|
27
|
+
| 한 화면 위에 보조 내용을 띄움 | [Sheet](sheet.md) |
|
|
28
|
+
| expo-router만 쓰는 앱 | 제품 router 기본 전환 |
|
|
29
|
+
|
|
30
|
+
## 공개 이름과 import
|
|
31
|
+
|
|
32
|
+
| 이름 | 역할 | Web | Native |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `SharedTransitionElement` | 기본(공유 요소 경계) | — | `/screen-transition` |
|
|
35
|
+
| `SharedTransitionScreen` | 동반(라우트 본문 감싸개) | — | `/screen-transition` |
|
|
36
|
+
| `useSharedTransitionOptions` | 보조(화면 옵션 hook) | — | `/screen-transition` |
|
|
37
|
+
| `createHjmTransitionStack` | 보조(stack navigator 생성) | — | `/screen-transition` |
|
|
38
|
+
|
|
39
|
+
granular subpath로만 가져온다(root·barrel에 없음). 이 subpath는 아래 optional peer를 import 하며,
|
|
40
|
+
없으면 tsc·테스트는 통과해도 기기 Metro 번들에서 죽는다.
|
|
41
|
+
|
|
42
|
+
- 직접: `react-native-screen-transitions` 4.0.0, `@react-navigation/native` 7.4.1(HJM `peerDependencies`, optional).
|
|
43
|
+
- 상위 라이브러리 요구: `react-native-gesture-handler`, `react-native-reanimated` 4, `react-native-worklets`,
|
|
44
|
+
`react-native-safe-area-context`.
|
|
45
|
+
- 소비 앱 패키지 관리자에 [exports patch](../../../../react-native/docs/patches/react-native-screen-transitions.patch)를
|
|
46
|
+
등록해야 한다. HJM tarball이 patch를 대신 적용하지 않는다.
|
|
47
|
+
|
|
48
|
+
## 최소 사용 예
|
|
49
|
+
|
|
50
|
+
Web: 없음.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Native
|
|
54
|
+
// 목록 화면과 상세 화면 양쪽에 같은 id
|
|
55
|
+
import { SharedTransitionElement } from "@hjmds/react-native/screen-transition";
|
|
56
|
+
|
|
57
|
+
<SharedTransitionElement id={`place-${place.id}`} accessibilityLabel={place.name}>
|
|
58
|
+
<PlacePhoto place={place} /> {/* 제품 콘텐츠 */}
|
|
59
|
+
</SharedTransitionElement>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
화면 옵션: `const options = useSharedTransitionOptions(\`place-${place.id}\`)`를 `createHjmTransitionStack()`이 만든
|
|
63
|
+
`Stack.Screen`에 붙인다. 전체 예는 설계 문서의 Shared screen transition 절을 따른다.
|
|
64
|
+
|
|
65
|
+
## 축과 기본값
|
|
66
|
+
|
|
67
|
+
| prop | 값 | 기본값 | 설명 |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `id` | 문자열 | 필수 | 데이터의 안정 키. 빈 문자열이면 `TypeError` |
|
|
70
|
+
| `children` | ReactNode | 필수 | 크기가 분명한 시각 요소 하나 |
|
|
71
|
+
| `accessibilityLabel` | 문자열 | — | 경계의 접근성 이름 |
|
|
72
|
+
| `style` | `StyleProp<ViewStyle>` | — | 경계 바깥 배치. 이 컴포넌트는 optional motion host frame이라 1.13 deprecated 대상에서 제외돼 있다 |
|
|
73
|
+
| `useSharedTransitionOptions` | `(id: string) => ScreenTransitionConfig` | — | 도착 화면 `Stack.Screen` `options`에 붙인다. 동작 줄이기면 전환 시간 0·제스처 꺼짐 |
|
|
74
|
+
| `createHjmTransitionStack` | `() => Stack` | — | `react-native-screen-transitions`의 blank stack navigator를 그대로 내보낸다 |
|
|
75
|
+
|
|
76
|
+
Props는 `id`·`children`·`accessibilityLabel`·`style` 넷뿐이고 콜백은 없다.
|
|
77
|
+
동작 줄이기가 켜지면 경계가 비활성화되고 화면 옵션의 전환 시간이 0, 제스처가 꺼진다.
|
|
78
|
+
live-view handoff·clipping escape는 꺼져 있고 공개하지 않는다(`react-native-teleport` 불필요).
|
|
79
|
+
|
|
80
|
+
## 배치
|
|
81
|
+
|
|
82
|
+
| 항목 | 값 | 근거 |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| 크기 | 경계는 자체 크기·여백을 갖지 않는다. 크기는 `children`, 바깥 배치는 `style`이 정한다. 크기가 분명한 시각 요소 하나(사진·썸네일)만 감싼다. 전환이 경계의 측정 크기 사이를 확대·축소(`zoom`, target `bound`)하기 때문이다 | `screen-transition.tsx`(`Transition.Boundary`, `useSharedTransitionOptions`) |
|
|
85
|
+
| 간격 | 간격 토큰 없음. 감싸는 레이아웃이 정한다 | — |
|
|
86
|
+
| 순서·정렬 | 출발 화면(목록 카드의 사진)과 도착 화면(상세 상단의 큰 사진)에 하나씩, 같은 `id`로 둔다. 한 화면에 같은 `id`를 둘 이상 두지 않는다. 각 화면 본문은 [SharedTransitionScreen](shared-transition-screen.md)(`flex: 1`, 테마 배경) 안에 둔다 | `SharedTransitionScreen` |
|
|
87
|
+
| 고정·스크롤 | 상세 화면은 아래로 쓸어내려 닫힌다(`gestureDirection: "vertical"`). 도착 화면 상단 사진 위에 세로 드래그 제스처를 겹치지 않는다 | `useSharedTransitionOptions` |
|
|
88
|
+
| 좁은 폭·큰 글자 | 크기는 `children`을 따른다. 동작 줄이기에서는 전환 없이 바로 바뀐다 | `environment.reducedMotion` |
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
목록 화면 상세 화면
|
|
92
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
93
|
+
│ ┌────┐ 장소 이름 │ zoom → │ ┌──────────────┐ │
|
|
94
|
+
│ │ id │ 설명 │ │ │ id │ │ ← 같은 id
|
|
95
|
+
│ └────┘ │ ← 아래로 │ └──────────────┘ │
|
|
96
|
+
│ ┌────┐ … │ 쓸어 닫기 │ 본문 (제품) │
|
|
97
|
+
└──────────────────┘ └──────────────────┘
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 꼭 지킬 것
|
|
101
|
+
|
|
102
|
+
- `id`는 데이터의 안정 키로 만든다. 빈 문자열이면 `TypeError`, 두 화면의 id가 다르면 전환이 맺히지 않는다.
|
|
103
|
+
- 각 라우트 본문은 SharedTransitionScreen으로 감싼다. 남아 있는 이전 화면이 접근성·터치에 새지 않게 한다.
|
|
104
|
+
- host와 어댑터가 같은 navigation 인스턴스를 써야 한다. pnpm에서 peer가 둘로 갈리면
|
|
105
|
+
`LinkingContext`/`DescriptorsStore` 오류가 난다.
|
|
106
|
+
- 라우트 파라미터·포커스·스크롤 복원·navigation container는 제품 소유다.
|
|
107
|
+
|
|
108
|
+
## 함정
|
|
109
|
+
|
|
110
|
+
- patch 없이 설치하면 Expo TypeScript가 upstream `.tsx`를 읽어 오류가 수백 개 난다.
|
|
111
|
+
teleport 네이티브 뷰가 링크되지 않은 client에서는 patch가 없으면 `Unimplemented component: PortalHostView`가 났다.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# SharedTransitionScreen
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Optional interaction adapters · Shared screen transition](../../../../../docs/interaction-adapters.md#shared-screen-transition), `src/screen-transition.tsx`. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
|
|
9
|
+
- 스토리북: `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
`createHjmTransitionStack()`으로 만든 stack에서 공유 요소 전환을 쓸 때, **각 라우트 본문**을 감싼다.
|
|
14
|
+
전환 뒤에도 출발 화면은 역방향 전환 기하를 위해 mount된 채 남는데, 이 감싸개가 포커스를 잃은 화면을
|
|
15
|
+
접근성 트리와 터치에서 빼고, 테마 배경으로 불투명하게 칠해 전환 backdrop이 내용에 비치지 않게 한다.
|
|
16
|
+
공유 요소 자체는 [SharedTransitionElement](shared-transition-element.md)가 맡는다.
|
|
17
|
+
|
|
18
|
+
설치 전제(peer, exports patch, 단일 navigation 인스턴스)와 "현재 쓸 수 있는 소비 앱 없음"(expo-router 전용)은
|
|
19
|
+
[SharedTransitionElement](shared-transition-element.md#공개-이름과-import)와 같다.
|
|
20
|
+
|
|
21
|
+
## 쓰지 않을 때
|
|
22
|
+
|
|
23
|
+
| 상황 | 대신 쓸 것 |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| 공유 요소 전환이 없는 stack | 감싸지 않는다 |
|
|
26
|
+
| Web | 없음 |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `SharedTransitionScreen` | 기본(라우트 본문 감싸개) | — | `/screen-transition` |
|
|
33
|
+
| `SharedTransitionElement` | 동반(공유 요소 경계, 같이 씀) | — | `/screen-transition` |
|
|
34
|
+
|
|
35
|
+
granular subpath로만 가져온다. `react-native-screen-transitions` 4.0.0과 `@react-navigation/native` 7.4.1
|
|
36
|
+
(optional peer)이 설치돼 있지 않으면 기기 Metro 번들에서 죽는다.
|
|
37
|
+
|
|
38
|
+
## 최소 사용 예
|
|
39
|
+
|
|
40
|
+
Web: 없음.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// Native — 라우트 컴포넌트마다
|
|
44
|
+
import { SharedTransitionScreen, SharedTransitionElement } from "@hjmds/react-native/screen-transition";
|
|
45
|
+
|
|
46
|
+
function PlaceDetail({ place }: Props) {
|
|
47
|
+
return (
|
|
48
|
+
<SharedTransitionScreen testID="place-detail">
|
|
49
|
+
<SharedTransitionElement id={`place-${place.id}`}>
|
|
50
|
+
<PlacePhoto place={place} />
|
|
51
|
+
</SharedTransitionElement>
|
|
52
|
+
<PlaceBody place={place} />
|
|
53
|
+
</SharedTransitionScreen>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 축과 기본값
|
|
59
|
+
|
|
60
|
+
| prop | 값 | 기본값 | 설명 |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| `children` | ReactNode | — | 라우트 본문 |
|
|
63
|
+
| `style` | `StyleProp<ViewStyle>` | `flex: 1` + 테마 `bg` 배경 | 배치용. optional motion host frame이라 1.13 deprecated 대상에서 제외돼 있다 |
|
|
64
|
+
| 나머지 | React Native `ViewProps`(`testID` 등) | — | `pointerEvents`·`accessibilityElementsHidden`·`importantForAccessibility`는 덮어쓰인다 |
|
|
65
|
+
| (포커스 상태) | `useIsFocused(): boolean` | — | 포커스가 없으면 `importantForAccessibility="no-hide-descendants"`, `accessibilityElementsHidden`, `pointerEvents="none"`을 건다 |
|
|
66
|
+
|
|
67
|
+
콜백 prop은 없다.
|
|
68
|
+
|
|
69
|
+
## 배치
|
|
70
|
+
|
|
71
|
+
| 항목 | 값 | 근거 |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| 크기 | 라우트 영역 전체(`flex: 1`)를 채우고 테마 `bg`로 불투명하게 칠한다 | `src/screen-transition.tsx` |
|
|
74
|
+
| 간격 | 자체 여백 없음. 화면 여백·안전 영역은 안쪽 화면 골격이 맡는다 | `src/screen-transition.tsx` |
|
|
75
|
+
| 순서·정렬 | 라우트 컴포넌트의 가장 바깥 요소. 그 안에 [SharedTransitionElement](shared-transition-element.md)와 나머지 본문을 둔다 | — |
|
|
76
|
+
| 고정·스크롤 | 스크롤하지 않는다. 스크롤 영역(ScrollView·FlatList)은 안쪽에 둔다 | `src/screen-transition.tsx` |
|
|
77
|
+
| 좁은 폭·큰 글자 | 바뀌는 것 없음(부모 크기를 따른다) | — |
|
|
78
|
+
|
|
79
|
+
## 꼭 지킬 것
|
|
80
|
+
|
|
81
|
+
- React Navigation의 navigation context 안(host `NavigationContainer` 아래)에서만 렌더한다.
|
|
82
|
+
`useIsFocused`가 그 context를 요구한다.
|
|
83
|
+
- `HjmNativeProvider` 아래에 둔다(테마 배경을 읽는다).
|
|
84
|
+
- `style`은 배치용으로만 쓴다. 배경색을 덮으면 전환 중 backdrop이 내용에 비친다.
|
|
85
|
+
- `pointerEvents`·`accessibilityElementsHidden`·`importantForAccessibility`를 직접 넘기지 않는다.
|
|
86
|
+
감싸개가 포커스 상태로 다시 덮어쓴다.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Sheet
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Sheet 입력 화면과 가용 영역](../../sheet.md), [optional adapters](../../optional-adapters.md)(Native 제스처 확장), `src/component-recipes.ts`(`sheetRecipe`), `src/sheet.ts`(`sheetBehaviorDefaults`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/오버레이/시트`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
현재 화면 위에 모달로 띄우는 보조 작업 패널에 쓴다. 설정 한 항목 편집, 선택 목록(테마·언어),
|
|
14
|
+
짧은 입력 폼, 공유·사진 출처 선택처럼 끝나면 원래 화면으로 돌아오는 작업이다. 기본은 하단에서 올라온다.
|
|
15
|
+
|
|
16
|
+
2026-09-23 소비 감사에서 제품마다 Sheet를 직접 조립하며 제목 정렬, 키보드 높이, top inset, 본문 maxHeight를
|
|
17
|
+
따로 보정하고 있었다. 이 보정은 Sheet가 소유한다. 제품 wrapper에서 Modal·키보드 listener·강제 maxHeight를
|
|
18
|
+
다시 만들지 않는다.
|
|
19
|
+
|
|
20
|
+
## 쓰지 않을 때
|
|
21
|
+
|
|
22
|
+
| 상황 | 대신 쓸 것 |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| 짧은 확인·되돌리기 어려운 결정 | [AlertDialog](alert-dialog.md) |
|
|
25
|
+
| 화면 중앙의 집중 작업 | [Dialog](dialog.md) |
|
|
26
|
+
| Web 가장자리 서랍, 비모달 보조 패널 | [SidePanel](side-panel.md) |
|
|
27
|
+
| 트리거 옆에 붙는 작은 내용 | [Popover](popover.md) |
|
|
28
|
+
| 행동 목록 | [Menu](menu.md) |
|
|
29
|
+
| 폼 한 칸의 선택 | [Select](select.md)(Native는 자체 sheet를 연다) |
|
|
30
|
+
|
|
31
|
+
## 공개 이름과 import
|
|
32
|
+
|
|
33
|
+
| 이름 | 역할 | Web | Native |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `Sheet` | 기본(모달) | `@hjmds/react`, `/overlays` | `@hjmds/react-native`, `/overlays` |
|
|
36
|
+
| `GestureSheet` | 확장(optional: 끌어서 snap·닫기) | — | `/sheet-gesture` |
|
|
37
|
+
| `GestureSheetProvider` | 동반(optional: GestureSheet host) | — | `/sheet-gesture` |
|
|
38
|
+
| `GestureSheetInput` | 동반(GestureSheet 안 입력, 계약은 [Field](field.md)) | — | `/sheet-gesture` |
|
|
39
|
+
|
|
40
|
+
`/sheet-gesture`는 granular subpath로만 가져오며 optional peer `@gorhom/bottom-sheet` 5.2.14,
|
|
41
|
+
`react-native-reanimated` 4.x, `react-native-gesture-handler` 2.32.0이 필요하다. 없으면 기기 Metro 번들에서 죽는다.
|
|
42
|
+
기본 `Sheet`는 추가 peer가 없다.
|
|
43
|
+
|
|
44
|
+
## 최소 사용 예
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Web
|
|
48
|
+
import { Button } from "@hjmds/react/actions";
|
|
49
|
+
import { TextField } from "@hjmds/react/forms";
|
|
50
|
+
import { Sheet } from "@hjmds/react/overlays";
|
|
51
|
+
|
|
52
|
+
<Sheet open={open} onOpenChange={(next) => setOpen(next)}
|
|
53
|
+
title={t("profile.edit.title")} closeLabel={t("common.close")}
|
|
54
|
+
footer={<Button onClick={save} loading={saving}>{t("common.save")}</Button>}>
|
|
55
|
+
<TextField label={t("profile.name")} value={name} onValueChange={setName} />
|
|
56
|
+
</Sheet>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// Native — 입력 폼은 keyboardAvoidance + scrollable
|
|
61
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
62
|
+
import { TextField } from "@hjmds/react-native/inputs";
|
|
63
|
+
import { Sheet } from "@hjmds/react-native/overlays";
|
|
64
|
+
|
|
65
|
+
<Sheet open={open} onOpenChange={(next) => setOpen(next)}
|
|
66
|
+
title={t("profile.edit.title")} closeLabel={t("common.close")}
|
|
67
|
+
keyboardAvoidance scrollable busy={saving}
|
|
68
|
+
footer={<Button onPress={save} loading={saving}>{t("common.save")}</Button>}>
|
|
69
|
+
<TextField label={t("profile.name")} value={name} onValueChange={setName} />
|
|
70
|
+
</Sheet>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 축과 기본값
|
|
74
|
+
|
|
75
|
+
| prop | 값 | 기본값 | 설명 |
|
|
76
|
+
| --- | --- | --- | --- |
|
|
77
|
+
| `placement` | `bottom` · `start` · `end` | `bottom` | 나오는 가장자리 |
|
|
78
|
+
| `size` | `auto` · `medium`(60%) · `large`(85%) · `full`(100%) | `auto` | `auto`는 내용 높이. 비율은 화면 높이 기준(Web `dvh`), 두 플랫폼 모두 위 safe area(Native는 키보드도)를 뺀 남은 높이를 넘지 않는다. `maxHeightRatio` 0.9 상한은 `auto`에만 걸린다 |
|
|
79
|
+
| `open` · `defaultOpen` | `boolean` | 비제어 `false` | Web은 제어하면 `onOpenChange` 필수, 비제어면 `trigger` 필수 |
|
|
80
|
+
| `onOpenChange` | `(open: boolean, detail: { reason }) => void` | — | reason: `trigger` · `close-action` · `escape` · `back` · `outside` · `swipe` · `programmatic`. Android 하드웨어 back은 Native에서만 `back` |
|
|
81
|
+
| `onDismissComplete` | `(detail: { reason }) => void` | — | 표면이 실제로 사라진 뒤 한 번. 다음 modal·화면 이동은 여기서 |
|
|
82
|
+
| `dismissPolicy` | `{ dismissible?, dismissWhileBusy?, outsideDismiss?, escapeOrBackDismiss?, swipeDismiss? }` | `dismissible` true · `dismissWhileBusy` false · `outsideDismiss` true · `escapeOrBackDismiss` true · `swipeDismiss` false | 닫기 허용 범위 |
|
|
83
|
+
| `busy` | `boolean` | `false` | 사용자 닫기를 막는다 |
|
|
84
|
+
| `title` · `closeLabel` | 문구 | 필수 | Native `title`이 element면 `accessibilityTitle`(string) 필수 |
|
|
85
|
+
| `description` · `footer` | 노드(Native `description`은 `string`) | — | `footer`는 스크롤 밖 고정. 저장 행동은 여기 |
|
|
86
|
+
| `detents`(Web) · `activeDetent` · `onDetentChange` | `readonly ("medium" \| "large" \| "full")[]` · `(detent) => void` | — | 사용자가 높이를 바꿀 단계. `detentLabels: { expand, collapse }` 필수 |
|
|
87
|
+
| `keyboardAvoidance` · `scrollable`(Native) | `boolean` | `false` | 입력 폼은 둘 다 켠다 |
|
|
88
|
+
| `safeAreaInsets`(Native) | `Partial<Insets>` | provider inset | — |
|
|
89
|
+
| `contentStyle`(Native) | 배치 key만 | — | 배치 밖 key(색·높이 등)는 deprecated(개발 모드 경고), 다음 major에서 배치 전용 타입으로 좁힌다 |
|
|
90
|
+
|
|
91
|
+
## 배치
|
|
92
|
+
|
|
93
|
+
| 항목 | 값 | 근거 |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| 크기 | 하단 시트 높이: `auto` 내용 높이(최대 화면 높이 × `maxHeightRatio` 0.9), `medium` 60%, `large` 85%, `full` 위 safe area 안 전체 높이(0.9 상한 없음). Web 하단 시트 최대 폭 640. 위 두 모서리만 `radius.xl` 24. 옆 시트(`start`·`end`): Web 폭 `min(28rem, 100%)`·높이 화면 − 32, Native 폭 88%(최대 420)·화면 높이 | `sheetRecipe.sizes`·`content`·`web`, `.hjm-sheet`, Native `Sheet` |
|
|
96
|
+
| 간격 | 두 플랫폼 같은 값: 좌우 `content.paddingHorizontal`(`spacing.lg` 20), 위·아래 `content.paddingTop/Bottom`(`spacing.sm` 12, 하단 시트는 아래 safe area를 더함), 머리·본문·footer 사이 `body.gap`(`spacing.md` 16), footer 위 `footer.paddingTop`(`spacing.sm` 12), 제목–닫기 `header.gap`(`spacing.sm` 12), footer 버튼 사이 `footer.gap`(`spacing.sm` 12) | `.hjm-sheet__*`, `sheetRecipe.content`·`body`·`footer` |
|
|
97
|
+
| 순서·정렬 | 머리(제목·설명 + 닫기) → 본문 → `footer`. 머리 최소 높이 44(`control.minTouchTarget`), 닫기는 끝 쪽에서 제목 세로 중앙. Web footer는 오른쪽 정렬 가로 줄 [보조][주]. Native footer는 세로 열이라 꽉 찬 폭 버튼을 주 행동 먼저 둔다. Web에서 `detents`를 주면 위 가운데 36×4 손잡이 버튼(터치 최소 폭 44 · 높이 `spacing.lg` 20). Native 기본 Sheet는 손잡이가 없다 | `sheetRecipe.header`·`handle`, `.hjm-sheet__footer`, `.hjm-sheet__handle` |
|
|
98
|
+
| 고정·스크롤 | 본문만 스크롤, 머리·`footer` 고정(Native는 `scrollable`일 때 ScrollView). 고정 높이(`medium`·`large`·`full`, 옆 시트)에서는 본문이 머리·footer를 뺀 남은 높이를 차지하고 `footer`는 시트 아래에 붙는다. 그래서 본문의 `flex: 1` 자식(SearchScreen, 목록 화면)이 그 높이를 채운다(Web `.hjm-sheet__body` `flex: 1 1 auto`, Native 본문 `flexGrow: 1`·`minHeight: 0`, 미게시(1.13.1 이후)). `auto`는 내용 높이 그대로다. 저장·확인은 `footer`에. 아래 안전 영역은 시트가 자기 여백에 더한다(`spacing.sm` 12 + 아래 inset, Web은 `env(safe-area-inset-bottom)`). 위 inset은 시트가 올라갈 높이를 줄인다. 제품이 inset을 다시 더하지 않는다 | `.hjm-sheet__body`, `sheetRecipe.safeArea`, Native `Sheet` |
|
|
99
|
+
| 좁은 폭·큰 글자 | 폭 < 640이면 하단 시트가 화면 폭을 채운다. Web footer는 줄을 바꾼다. 큰 글자로 내용이 길어지면 `auto`는 최대 90%에서 멈추고 본문이 스크롤된다(`full`은 위 safe area까지) | `.hjm-sheet[data-placement="bottom"]`, `maxHeightRatio` |
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
폭 < 640(모바일) Web 넓은 폭: 가운데, 최대 640
|
|
103
|
+
┌──────────────────────────────┐ ┌────────────────────────────────────┐
|
|
104
|
+
│ (뒤 화면 + scrim, 누르면 닫힘)│ │ (scrim) │
|
|
105
|
+
│ ╭──────────────────────────╮ │ │ ╭────────────────────╮ │
|
|
106
|
+
│ │ ▬▬ (Web detents일 때) │ │ │ │ 제목 [×] │ │
|
|
107
|
+
│ │ 제목·설명 [×] │ │ ← 고정 │ │ 본문(스크롤) │ │
|
|
108
|
+
│ │──────────────────────────│ │ │ │ [취소] [저장] │ │
|
|
109
|
+
│ │ 본문 ↕ 스크롤 │ │ └──────┴────────────────────┴────────┘
|
|
110
|
+
│ │──────────────────────────│ │
|
|
111
|
+
│ │ footer: Web [취소][저장] │ │ ← 고정
|
|
112
|
+
│ │ Native [ 저장 ] │ │
|
|
113
|
+
│ │ [ 취소 ] │ │
|
|
114
|
+
│ │ ░ 안전 영역(inset 더함) ░ │ │
|
|
115
|
+
└─┴──────────────────────────┴─┘
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 꼭 지킬 것
|
|
119
|
+
|
|
120
|
+
- 입력 폼 Native Sheet는 `keyboardAvoidance`와 `scrollable`을 함께 켜고, 제품의 키보드 listener·maxHeight·
|
|
121
|
+
중첩 ScrollView를 지운다. 본문이 FlatList 등 가상화 목록이면 `scrollable={false}`로 둔다.
|
|
122
|
+
- 저장 버튼은 `footer`에 둔다. 저장 중에는 `busy`로 닫기를 막는다.
|
|
123
|
+
- 닫힌 뒤 다른 modal을 열거나 화면 이동은 `onDismissComplete`에서 한다. 두 modal을 동시에 띄우지 않는다.
|
|
124
|
+
- 문구(`title`, `closeLabel`, 본문)는 i18n 키로 넣는다. 제목 정렬을 위해 `as unknown as string` 캐스트나
|
|
125
|
+
`.hjm-sheet__header` CSS 덮어쓰기를 하지 않는다.
|
|
126
|
+
- Native `contentStyle`은 배치 key만 쓴다. 높이는 `size`로 정한다(`contentStyle={{ height }}` 금지, 배치 밖 key는 deprecated).
|
|
127
|
+
- Web Sheet는 `layoutStyle`을 받지 않는다(Web `layoutStyle` 제외 15개 중 하나). 표면 위치·크기는 `placement`·`size`가 정한다.
|
|
128
|
+
|
|
129
|
+
## 플랫폼 차이
|
|
130
|
+
|
|
131
|
+
| 항목 | Web | Native |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| uncontrolled 트리거 | `trigger`(uncontrolled면 필수) | 없음 |
|
|
134
|
+
| 사용자 높이 조절 | `detents`·`activeDetent`·`onDetentChange`·`detentLabels` | 없음(GestureSheet `snapPoints`) |
|
|
135
|
+
| 키보드·스크롤 | CSS scroll body | `keyboardAvoidance`, `scrollable` |
|
|
136
|
+
| 고정 높이 본문 채우기 | 항상 `flex: 1 1 auto` | `size`가 `auto`가 아니거나 옆 시트일 때 `flexGrow: 1`(1.13.1부터). `scrollable`이면 ScrollView 자체만 늘고 그 안 내용은 내용 높이다 |
|
|
137
|
+
| 초점 | `initialFocusRef`, `returnFocusRef`, focus trap | `returnFocusRef` |
|
|
138
|
+
| 겹침 순서 | `modalPriority`, `portalContainer` | RN `Modal` props(`testID` 등) |
|
|
139
|
+
| `title` 타입 | `ReactNode` | `string` 또는 element + `accessibilityTitle` |
|
|
140
|
+
|
|
141
|
+
### GestureSheet(Native optional)
|
|
142
|
+
|
|
143
|
+
- controlled `open`/`onOpenChange(open)`만 있다. `title`·`closeLabel`·`children` 필수, `snapPoints` 기본 `["50%", "90%"]`,
|
|
144
|
+
`initialIndex` 기본 `0`, `busy`면 끌어 닫기·backdrop 닫기를 막는다. 닫기 버튼이 본문 끝에 자동으로 들어간다.
|
|
145
|
+
- 화면을 `GestureHandlerRootView` 안의 `GestureSheetProvider`로 감싼다. 입력은 `GestureSheetInput`을 쓴다.
|
|
146
|
+
- RN `Modal` 안에 GestureSheet를 두면 Android back이 `onRequestClose`로만 간다. 그 host는
|
|
147
|
+
`dismissTopGestureSheet()`를 먼저 부르고 `false`일 때만 자기 자신을 닫는다.
|
|
148
|
+
- 동적 높이·footer·dismissPolicy·reason은 없다. 이것들이 필요하면 기본 Sheet를 쓴다.
|
|
149
|
+
|
|
150
|
+
## 함정
|
|
151
|
+
|
|
152
|
+
- 미게시(1.12.1 이후) 변경: 1.12.1까지 `size="full"`(Web `detents`의 `full` 포함)은 0.9 상한에 걸려 90%에서 멈췄다. 1.12.1을 쓰는 앱에서 화면 전체가 필요하면 화면 전환을 쓴다.
|
|
153
|
+
- 1.13.0 이하 Native 고정 높이 Sheet는 본문이 내용 높이라서 `flex: 1` 자식이 0pt가 됐다. 시트 안 SearchScreen이 검색 입력만 그리고
|
|
154
|
+
필터 줄·목록이 사라졌다(2026-10-06 utilverse 채팅 도구 선택, iPhone 17 Pro · iOS 26.5). 제품은 창 높이 `flexBasis`로 우회했다.
|
|
155
|
+
1.13.1부터는 그 우회 없이 채워진다. 1.13.0 이하에서도 footer는 본문 바로 아래에 있었고, 1.13.1부터 고정 높이 시트에서는 시트 아래에 붙는다.
|
|
156
|
+
- `scrollable` 본문 안에서는 자식이 `flex: 1`로 높이를 채울 수 없다(ScrollView 내용은 내용 높이다). 자기 스크롤을 가진 화면(SearchScreen 등)은
|
|
157
|
+
`scrollable` 없이 넣는다.
|