@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,124 @@
|
|
|
1
|
+
# RadioGroup
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [cross-platform core](../../cross-platform-core-normalization.md), `src/component-recipes.ts`(`selectionGroupRecipe`·`selectionControlRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/라디오 버튼 그룹`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
한 화면에 펼쳐 둔 선택지 중 정확히 하나를 고를 때 쓴다. 선택지마다 설명이 붙거나,
|
|
14
|
+
사용자가 모든 보기를 한눈에 비교해야 할 때(배송 방법, 알림 빈도, 신고 사유) 맞다.
|
|
15
|
+
대략 2~6개, 각 항목이 한 행을 차지해도 되는 경우다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 2~4개의 짧은 라벨로 보기·필터를 즉시 전환 | [SegmentedControl](segmented-control.md) |
|
|
22
|
+
| 선택지가 많거나(대략 7개 이상) 공간이 좁음, 폼 한 칸 | [Select](select.md) |
|
|
23
|
+
| 여러 개를 동시에 고름 | [CheckboxGroup](checkbox-group.md) |
|
|
24
|
+
| 켜고 끄는 설정 한 줄 | [Switch](switch.md) |
|
|
25
|
+
| 선택지 사이에 다른 콘텐츠가 끼어 직접 배치해야 함 | [Radio](radio.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `RadioGroup` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { RadioGroup } from "@hjmds/react/selection";
|
|
38
|
+
|
|
39
|
+
<RadioGroup
|
|
40
|
+
label={t("settings.frequency.label")}
|
|
41
|
+
items={[
|
|
42
|
+
{ value: "daily", label: t("settings.frequency.daily") },
|
|
43
|
+
{ value: "weekly", label: t("settings.frequency.weekly"), description: t("settings.frequency.weeklyHint") },
|
|
44
|
+
]}
|
|
45
|
+
value={frequency}
|
|
46
|
+
onValueChange={setFrequency}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Native
|
|
52
|
+
import { RadioGroup } from "@hjmds/react-native/inputs";
|
|
53
|
+
|
|
54
|
+
<RadioGroup
|
|
55
|
+
label={t("settings.frequency.label")}
|
|
56
|
+
items={[
|
|
57
|
+
{ value: "daily", label: t("settings.frequency.daily") },
|
|
58
|
+
{ value: "weekly", label: t("settings.frequency.weekly") },
|
|
59
|
+
]}
|
|
60
|
+
value={frequency}
|
|
61
|
+
onValueChange={(next) => next && setFrequency(next)}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 축과 기본값
|
|
66
|
+
|
|
67
|
+
| prop | 값 | 기본값 | 설명 |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `items` | Web `readonly { value: string; label: ReactNode; description?; disabled? }[]` · Native `readonly { value: Value; label: string; description?; disabled?; accessibilityHint?; leading? }[]` | 필수 | 비거나 값이 중복되면 `TypeError` |
|
|
70
|
+
| `value` · `defaultValue` | 항목 값 · `null` | `null` | 선택 없음 허용. items에 없는 값이면 `RangeError` |
|
|
71
|
+
| `onValueChange` | Web `(value: string) => void` · Native `(value: Value \| null) => void` | — | 고를 때 호출 |
|
|
72
|
+
| `orientation` | `vertical` · `horizontal` | `vertical` | Native는 글자 크기 160% 이상이면 가로를 세로로 쌓는다 |
|
|
73
|
+
| `presentation` | `plain` · `card` · `grouped` | `card` | `grouped`는 한 카드 안에 행이 붙는다 |
|
|
74
|
+
| `size` | `small` · `medium` | `medium` | — |
|
|
75
|
+
| `required` | `boolean` | `false` | 켜면 초기 선택을 보정한다 |
|
|
76
|
+
| `description` · `error` | Web `ReactNode` · Native `string` | — | 그룹 설명·오류 문구 |
|
|
77
|
+
| `disabled` · `readOnly` | `boolean` | `false` | — |
|
|
78
|
+
| `renderLeading` | `(item, appearance) => ReactNode` — appearance Web `{ selected, color: "currentColor", size }`, Native `{ checked, selected, disabled, readOnly, color, size }` | — | 항목 앞 제품 아이콘 |
|
|
79
|
+
| `label` / `accessibilityLabel` | 문자열(Web `label`은 `ReactNode`) | — | 둘 중 하나 필수. 보이는 제목이 없으면 `accessibilityLabel` |
|
|
80
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 그룹 프레임 배치 |
|
|
81
|
+
|
|
82
|
+
## 배치
|
|
83
|
+
|
|
84
|
+
| 항목 | 값 | 근거 |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| 크기 | 행 최소 높이 `medium` 56 · `small` 44. 행 안쪽은 [Radio](radio.md#배치)와 같다. `grouped`는 그룹이 테두리 1·모서리 `radius.lg` 16을 갖는다 | `layout.rowHeight.singleLine`, `control.minTouchTarget`, `.hjm-radio-group[data-presentation="grouped"]` |
|
|
87
|
+
| 간격 | 항목 사이 세로 `plain` `spacing.xxs` 4 · `card` `spacing.xs` 8 · `grouped` 0, 가로 `plain` `spacing.sm` 12 · `card` `spacing.md` 16 · `grouped` 0. 제목·항목·설명·오류 사이 `spacing.xs` 8 | `selectionGroupRecipe.orientations`·`supportGap` |
|
|
88
|
+
| 순서·정렬 | 제목 맨 위. 설명은 항목 **위**(`selectionGroupRecipe.slots` 순서, 두 플랫폼). 오류는 항목 아래이며 있으면 설명을 대신한다. 폼 저장 버튼은 그룹 아래 | `selection.tsx`(RadioGroup), Native `inputs.tsx` |
|
|
89
|
+
| 고정·스크롤 | 고정되지 않는다. 폼·화면 본문 스크롤 안에 둔다 | — |
|
|
90
|
+
| 좁은 폭·큰 글자 | Web 가로 배치는 넘치면 다음 줄로 감긴다(`flex-wrap`). Native 가로 배치는 감지 않고 글자 크기 160% 이상에서 세로로 바뀐다. 가로 배치는 짧은 선택지 2~3개에만 쓴다 | `.hjm-radio-group[data-orientation="horizontal"]`, `largeTextThreshold` 1.6 |
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
card(vertical) grouped(vertical)
|
|
94
|
+
알림 받기 알림 받기
|
|
95
|
+
└ gap spacing.xs 8
|
|
96
|
+
┌─────────────────────┐ ┌─────────────────────┐
|
|
97
|
+
│ ◉ 모두 │ 56 │ ◉ 모두 │
|
|
98
|
+
└─────────────────────┘ │ ○ 멘션만 │
|
|
99
|
+
gap spacing.xs 8 │ ○ 받지 않음 │
|
|
100
|
+
┌─────────────────────┐ └─────────────────────┘ radius.lg 16
|
|
101
|
+
│ ○ 멘션만 │
|
|
102
|
+
└─────────────────────┘
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 꼭 지킬 것
|
|
106
|
+
|
|
107
|
+
- `items`가 비었거나 항목 `value`·문자열 `label`이 비었거나 `value`가 중복되면 렌더 중 `TypeError`,
|
|
108
|
+
`value`/`defaultValue`가 items에 없으면 `RangeError`를 던진다. 서버 목록은 정리한 뒤 넘긴다.
|
|
109
|
+
- 옛 Native `options` prop은 1.11에서 제거됐다. 넘기면 `TypeError`다([이관표](../../migration-native-legacy-removal.md)).
|
|
110
|
+
- 오류 문구는 `error`로 넘긴다. 그룹 아래에 별도 Text로 직접 그리지 않는다.
|
|
111
|
+
- 배치는 `layoutStyle`로 한다. Native의 `style`과 slot style(`controlStyle`·`indicatorStyle`·`labelStyle` 등)은 deprecated
|
|
112
|
+
(개발 모드 경고, 다음 major 제거)다([소비 정책 §3.1](../../consumer-policy.md)). 외형은 `presentation`·`size`·`renderIndicator`로 바꾼다.
|
|
113
|
+
|
|
114
|
+
## 플랫폼 차이
|
|
115
|
+
|
|
116
|
+
| 항목 | Web | Native |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `onValueChange` 값 | `string` | `Value \| null`(제네릭) |
|
|
119
|
+
| `label`·항목 `label` 타입 | `ReactNode` | `string` |
|
|
120
|
+
| 항목 `leading`·`accessibilityHint` | 없음 | 있음 |
|
|
121
|
+
| indicator 숨김·교체 | 없음 | `indicator="none"`, `renderIndicator` |
|
|
122
|
+
| 낭독 문구 `requiredLabel`·`readOnlyLabel`·`invalidLabel`, `invalid` | 없음 | 있음 |
|
|
123
|
+
| `name` | 있음(기본 자동 생성) | 없음 |
|
|
124
|
+
| 루트 요소 | `<fieldset>` + `<legend>` | `View` `accessibilityRole="radiogroup"` |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Radio
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: `src/component-recipes.ts`(`selectionControlRecipe`). 별도 계약 문서는 없다
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/라디오 버튼`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
라디오 한 개를 제품이 직접 배치해야 할 때만 쓴다. 예: 선택지 사이에 다른 콘텐츠가 끼어 있어
|
|
14
|
+
한 묶음 목록으로 그릴 수 없는 경우. 선택 상태는 제품이 들고 `checked`로 내려 준다.
|
|
15
|
+
선택지가 한 곳에 모여 있으면 거의 항상 [RadioGroup](radio-group.md)이 맞다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 모여 있는 2개 이상 선택지 중 하나(그룹 상태·키보드 이동 포함) | [RadioGroup](radio-group.md) |
|
|
22
|
+
| 2~4개의 짧은 보기 전환(어떤 목록을 볼지) | [SegmentedControl](segmented-control.md) |
|
|
23
|
+
| 선택지가 많거나 화면 공간이 좁음 | [Select](select.md) |
|
|
24
|
+
| 켜고 끄기 | [Switch](switch.md), [Checkbox](checkbox.md) |
|
|
25
|
+
| 여러 개를 동시에 고름 | [CheckboxGroup](checkbox-group.md), [ToggleGroup](toggle-group.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `Radio` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
// 같은 묶음은 name을 같게 둔다
|
|
38
|
+
import { Radio } from "@hjmds/react/selection";
|
|
39
|
+
|
|
40
|
+
<Radio
|
|
41
|
+
name="delivery"
|
|
42
|
+
value="pickup"
|
|
43
|
+
label={t("order.delivery.pickup")}
|
|
44
|
+
checked={method === "pickup"}
|
|
45
|
+
onCheckedChange={() => setMethod("pickup")}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Native
|
|
51
|
+
import { Radio } from "@hjmds/react-native/inputs";
|
|
52
|
+
|
|
53
|
+
<Radio
|
|
54
|
+
label={t("order.delivery.pickup")}
|
|
55
|
+
checked={method === "pickup"}
|
|
56
|
+
onCheckedChange={() => setMethod("pickup")}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 축과 기본값
|
|
61
|
+
|
|
62
|
+
| prop | 값 | 기본값 | 설명 |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| `label` | Web `ReactNode` · Native `string` | 필수 | 현지화 |
|
|
65
|
+
| `presentation` | `plain` · `card` · `grouped` | `card` | — |
|
|
66
|
+
| `size` | `small` · `medium` | `medium` | — |
|
|
67
|
+
| `checked` · `defaultChecked` | `boolean` | `false` | Web은 제어하면 `onCheckedChange` 필수 |
|
|
68
|
+
| `onCheckedChange` | `(checked: true) => void` | — | 라디오는 스스로 해제되지 않아 항상 `true`로만 불린다. 해제는 제품이 다른 항목을 선택해 `checked`를 내려서 한다 |
|
|
69
|
+
| `onChange`(Web) | `(event: ChangeEvent<HTMLInputElement>) => void` | — | 원시 input 이벤트 |
|
|
70
|
+
| `renderLeading` | Web `(appearance: { selected, color: "currentColor", size }) => ReactNode` · Native `(props: { checked, selected, disabled, readOnly, color, size }) => ReactNode` | — | 앞 제품 아이콘 |
|
|
71
|
+
| `renderIndicator`(Native) | `(props) => ReactNode`(위 Native 모양) | 기본 dot | `indicator="none"`이면 숨긴다 |
|
|
72
|
+
| `description`, `readOnly`, `disabled` | — | `readOnly` `false` | 두 renderer에 있다 |
|
|
73
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 행 배치. Web `style`은 안쪽 input에 붙는다 |
|
|
74
|
+
|
|
75
|
+
## 배치
|
|
76
|
+
|
|
77
|
+
| 항목 | 값 | 근거 |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| 크기 | 행 최소 높이 `medium` 56 · `small` 44. 표시 원 `medium` 24 · `small` 20. `plain`은 Native hitSlop `medium` 10 · `small` 12로 터치 영역을 넓힌다 | `selectionControlRecipe.sizes`, `control.selectionIndicator` |
|
|
80
|
+
| 간격 | 원과 글자 사이 `medium` `spacing.sm` 12 · `small` `spacing.xs` 8. `card`·`grouped` 안쪽 여백 `medium` 세로 `spacing.sm` 12·가로 `spacing.md` 16, `small` 세로 `spacing.xs` 8·가로 `spacing.sm` 12. `card` 모서리 `radius.md` 12, 테두리 1. `plain`은 여백·테두리 없음. 글자와 설명 사이 `spacing.xxs` 4 | `selectionControlRecipe.sizes`·`presentations`, `.hjm-choice`, `.hjm-choice__copy` |
|
|
81
|
+
| 순서·정렬 | 표시 원이 시작 쪽(LTR 왼쪽), 글자가 그 뒤, 설명은 글자 아래. RTL은 좌우가 바뀐다. 여러 개는 단독으로 늘어놓지 말고 [RadioGroup](radio-group.md)에 넣는다 | `.hjm-choice`, Native `inputs.tsx` |
|
|
82
|
+
| 고정·스크롤 | 고정되지 않는다. 폼·본문 스크롤 안에 둔다 | — |
|
|
83
|
+
| 좁은 폭·큰 글자 | Native는 부모 폭을 채운다(`alignSelf: "stretch"`). Web 단독 Radio는 내용 폭(`inline-flex`)이고 RadioGroup 세로 목록 안에서만 꽉 찬다. 글자가 커지면 행 높이가 늘어난다(최소 높이만 고정) | Native `inputs.tsx`, `.hjm-choice` |
|
|
84
|
+
|
|
85
|
+
## 꼭 지킬 것
|
|
86
|
+
|
|
87
|
+
- `label`은 i18n 문구로 넣는다. Native는 `string`만 받는다.
|
|
88
|
+
- 묶음의 접근성 이름(fieldset/radiogroup)은 Radio가 만들지 않는다. 단독 Radio 여러 개로 묶음을
|
|
89
|
+
흉내 내지 말고 RadioGroup을 쓴다.
|
|
90
|
+
- 선택 표시는 indicator(dot)가 맡는다. 색만으로 선택을 알리도록 바꾸지 않는다.
|
|
91
|
+
- 배치는 `layoutStyle`로 한다. Native의 `style`·`controlStyle`·`labelStyle` 등 slot style은 deprecated(개발 모드 경고,
|
|
92
|
+
다음 major 제거)다. 색·글자·radius·높이를 덮지 않는다([소비 정책 §3.1](../../consumer-policy.md)).
|
|
93
|
+
|
|
94
|
+
## 플랫폼 차이
|
|
95
|
+
|
|
96
|
+
| 항목 | Web | Native |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `label`·`description` 타입 | `ReactNode` | `string` |
|
|
99
|
+
| 묶음 연결 | `name`(HTML input 속성) | 없음, 제품이 상태로 묶음 |
|
|
100
|
+
| `required`·`invalid`와 낭독 문구(`requiredLabel`·`invalidLabel`·`readOnlyLabel`) | `required`만 HTML 속성 | 있음 |
|
|
101
|
+
| indicator 숨김·교체 | 없음 | `indicator="none"`, `renderIndicator` |
|
|
102
|
+
| 앞 아이콘 | `renderLeading` | `leading`, `renderLeading` |
|
|
103
|
+
| 이벤트 | `onCheckedChange`, 원시 `onChange` | `onCheckedChange` |
|
|
104
|
+
| 배치 | `layoutStyle`(루트 `<label>`), `style`은 안쪽 input | `layoutStyle`(행) |
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Result
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Result](../../result.md), `src/result.ts`(`resultRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/상태와 알림/결과 안내`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
사용자 행동 뒤 흐름이 **끝난** 화면에 쓴다. 결제·제출 성공, 제출 실패, 존재하지 않는 페이지처럼
|
|
14
|
+
상태 하나와 최대 두 개의 다음 행동(다른 곳으로 이동, 다시 시도)을 보여 준다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 목록·검색 결과가 비었고 조건이 바뀌면 채워짐 | [EmptyState](empty-state.md) |
|
|
21
|
+
| 화면 일부 구역 실패, 나머지는 정상 | [Notice](notice.md) + 재시도 |
|
|
22
|
+
| 최초 로딩 | [Skeleton](skeleton.md) |
|
|
23
|
+
| 잠깐 알리고 사라지는 결과 | [Toast](toast.md) |
|
|
24
|
+
| 확인이 필요한 결정 | [AlertDialog](alert-dialog.md) |
|
|
25
|
+
|
|
26
|
+
상태 화면 조합 전체는 [1.4 제품 채택 가이드 · 상태 화면](../../product-adoption-1.4.md#상태-화면)을 따른다.
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `Result` | 기본 | `@hjmds/react`, `/feedback` | `@hjmds/react-native`, `/feedback` |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { Result } from "@hjmds/react/feedback";
|
|
39
|
+
|
|
40
|
+
<Result
|
|
41
|
+
status="success"
|
|
42
|
+
title={t("checkout.done.title")}
|
|
43
|
+
description={t("checkout.done.body")}
|
|
44
|
+
actions={[
|
|
45
|
+
{ label: t("checkout.done.viewOrder"), onAction: openOrder },
|
|
46
|
+
{ label: t("common.goHome"), onAction: goHome },
|
|
47
|
+
]}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// Native
|
|
53
|
+
import { Result } from "@hjmds/react-native/feedback";
|
|
54
|
+
|
|
55
|
+
<Result
|
|
56
|
+
status="failure"
|
|
57
|
+
title={t("upload.failed.title")}
|
|
58
|
+
description={t("upload.failed.body")}
|
|
59
|
+
actions={[{ label: t("common.retry"), onAction: retry }]}
|
|
60
|
+
renderIcon={({ color }) => <AlertGlyph color={color} size={28} />} // 제품 소유 glyph
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 축과 기본값
|
|
65
|
+
|
|
66
|
+
| prop | 값 | 기본값 | 설명 |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `status` | `success` · `failure` · `info` | 필수 | 403/404/500 같은 HTTP 의미는 제품이 `failure`와 자기 문구로 번역한다 |
|
|
69
|
+
| `title` | `string` | 필수 | 현지화 |
|
|
70
|
+
| `description` | `string` | — | — |
|
|
71
|
+
| `actions` | 0~2개 `readonly { label: string; onAction(): void; accessibilityLabel?: string }[]` | — | 첫째가 primary Button, 둘째가 secondary Button. `accessibilityLabel` 생략 시 `label`. 3개 이상이면 `RangeError` |
|
|
72
|
+
| `icon`(Web) | `ReactNode` | — | 제품 glyph. 의미는 title·status가 이미 전한다 |
|
|
73
|
+
| `renderIcon`(Native) | `(props: { status, color: string, backgroundColor: string }) => ReactNode` | — | 받은 `color`로 그린다 |
|
|
74
|
+
| `headingLevel`(Web) | `1` · `2` | `2` | 화면 전체가 Result면 `1` |
|
|
75
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 바깥 여백·폭만 |
|
|
76
|
+
|
|
77
|
+
## 배치
|
|
78
|
+
|
|
79
|
+
| 항목 | 값 | 근거 |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| 크기 | 아이콘 원 56×56(Web `3.5rem`), `radius.full`. Web 설명 최대 폭 `40rem` | `.hjm-result__icon`, `.hjm-result__description`, Native `feedback.tsx` |
|
|
82
|
+
| 간격 | 안쪽 여백 위아래 `spacing.xxxl` 40, 좌우 `spacing.xl` 24. 요소 사이 `spacing.sm` 12. 행동 줄은 `spacing.xs` 8을 더 띄우고 버튼 사이 `spacing.sm` 12. 목록·카드 사이에 넣을 때는 앞뒤 `layout.sectionGap` 24 | `resultRecipe.paddingVertical`·`paddingHorizontal`·`gap`·`actionsGap`, `.hjm-result__actions` |
|
|
83
|
+
| 순서·정렬 | 가운데 정렬 세로 묶음: 아이콘 → 제목 → 설명 → 행동. 행동은 [Button](button.md#배치) 규칙대로 **secondary(둘째) → primary(첫째)** 순서로 그린다(두 표면 동일. 1.12.1까지는 primary가 먼저였다) | `feedback.tsx`(Web·Native Result) |
|
|
84
|
+
| 고정·스크롤 | 고정되지 않는다. 화면 전체를 채울 때는 Result를 세로 가운데에 두고 하단 고정 CTA를 따로 붙이지 않는다(행동은 `actions`) | — |
|
|
85
|
+
| 좁은 폭·큰 글자 | 행동 줄은 가운데 정렬로 넘치면 다음 줄로 감긴다. 제목·설명은 줄바꿈된다(`overflow-wrap: anywhere`) | `.hjm-result__actions`(flex-wrap), Native `flexWrap: "wrap"` |
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
┌──────────────────────────────┐
|
|
89
|
+
│ (padding 40) │
|
|
90
|
+
│ ( ✓ ) 56 │
|
|
91
|
+
│ gap 12 │
|
|
92
|
+
│ 결제를 마쳤어요 │ titleLarge
|
|
93
|
+
│ 영수증을 메일로 보냈어요 │ body, max 40rem(Web)
|
|
94
|
+
│ gap 12 + 8 │
|
|
95
|
+
│ [ 홈으로 ] [ 주문 보기 ] │ secondary → primary, gap 12
|
|
96
|
+
│ (padding 40) │
|
|
97
|
+
└──────────────────────────────┘
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 꼭 지킬 것
|
|
101
|
+
|
|
102
|
+
- 행동이 3개 이상이면 `RangeError`를 던진다. 잘리지 않으니 흐름을 다시 설계한다.
|
|
103
|
+
- 행동 버튼을 children으로 직접 조립하지 않는다(`children` prop이 없다). `actions`로 넘긴다.
|
|
104
|
+
- 아이콘은 제품 소유 glyph다. Web은 `icon`, Native는 `renderIcon`이 주는 `color`를 그대로 쓴다.
|
|
105
|
+
아이콘 배경·tone 색은 HJM(`resultRecipe.tones`) 소유다.
|
|
106
|
+
- `failure`는 Web `role="alert"`, Native live region·iOS 낭독으로 바로 알린다. 같은 실패를 Toast로
|
|
107
|
+
한 번 더 알리지 않는다.
|
|
108
|
+
|
|
109
|
+
## 플랫폼 차이
|
|
110
|
+
|
|
111
|
+
| 항목 | Web | Native |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| 아이콘 | `icon`(ReactNode) | `renderIcon({ status, color, backgroundColor })` |
|
|
114
|
+
| 제목 heading 수준 | `headingLevel` | `accessibilityRole="header"` 고정 |
|
|
115
|
+
| 실패 알림 | `role="alert"`(그 외 `status`) | assertive live region, iOS는 `announceForAccessibility` |
|
|
116
|
+
| 바깥 배치 | `className`, `layoutStyle`(HTML `style`도 받음) | `layoutStyle`(`style`은 deprecated — 개발 모드 경고, 다음 major 제거) |
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# SavedItemsScreen
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 계약](../../screen-patterns.md); 저장한 항목을 Instagram의 컬렉션 탐색 방식으로 바꾸라는 사용자 요청. 기존 ListDetailScreen·Grid를 합성하며 별도 저장 엔진은 만들지 않는다. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
|
|
9
|
+
- 스토리북: `배포/화면/콘텐츠/저장한 항목`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
저장한 이미지·게시물을 컬렉션 표지 → 사진 격자 → 상세 순서로 탐색할 때 쓴다.
|
|
14
|
+
공통 규격은 컬렉션 2열과 게시물 3열, 뒤로가기, 빈 컬렉션이다. 저장·해제·Undo·권한·라우팅은 제품 상태를 콜백으로 연결한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 텍스트 중심의 즐겨찾기 목록 | ListDetailScreen + ListRow |
|
|
21
|
+
| 검색·조건 변경이 주 행동 | SearchScreen |
|
|
22
|
+
| 상품 구매·결제가 주 행동 | 제품 화면 + ScreenLayout |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `SavedItemsScreen` | 컬렉션·격자·상세 화면 | `@hjmds/react/saved-items` | `@hjmds/react-native/saved-items` |
|
|
29
|
+
| `SavedItem` · `SavedCollection` · `SavedItemsLabels` | 데이터·문구 타입 | 같은 subpath의 타입 | 같은 subpath의 타입 |
|
|
30
|
+
|
|
31
|
+
루트 barrel에는 추가하지 않는다. 전용 화면이 불필요한 앱에 화면 의존성을 강제하지 않기 위해 별도 진입점을 쓴다.
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { SavedItemsScreen } from "@hjmds/react/saved-items";
|
|
38
|
+
|
|
39
|
+
<SavedItemsScreen title={t("saved.title")} items={items} collections={collections}
|
|
40
|
+
{...(collectionId !== undefined ? {collectionId} : {})}
|
|
41
|
+
selectedItemId={selectedItemId}
|
|
42
|
+
labels={{allItems:t("saved.all"),privateNotice:t("saved.private"),back:t("common.back"),empty:t("saved.empty"),createCollection:t("saved.create")}}
|
|
43
|
+
onOpenCollection={openCollection} onOpenItem={openItem} onBack={goBack}
|
|
44
|
+
onCreateCollection={openCreateSheet}
|
|
45
|
+
renderThumbnail={renderThumbnail} renderDetail={renderDetail}/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Native — 저장 해제는 renderDetail 머리의 ghost IconButton
|
|
50
|
+
import { Image } from "react-native";
|
|
51
|
+
import { IconButton } from "@hjmds/react-native/actions";
|
|
52
|
+
import { Stack, Text } from "@hjmds/react-native/primitives";
|
|
53
|
+
import { SavedItemsScreen } from "@hjmds/react-native/saved-items";
|
|
54
|
+
|
|
55
|
+
<SavedItemsScreen title={t("saved.title")} items={items} collections={collections}
|
|
56
|
+
{...(collectionId !== undefined ? { collectionId } : {})}
|
|
57
|
+
selectedItemId={selectedItemId}
|
|
58
|
+
labels={{ allItems: t("saved.all"), privateNotice: t("saved.private"), back: t("common.back"),
|
|
59
|
+
empty: t("saved.empty"), createCollection: t("saved.create") }}
|
|
60
|
+
onOpenCollection={openCollection} onOpenItem={openItem} onBack={goBack}
|
|
61
|
+
onCreateCollection={openCreateSheet}
|
|
62
|
+
renderThumbnail={(post) => <Image source={{ uri: post.imageUrl }} resizeMode="cover" style={{ width: "100%", height: "100%" }} />}
|
|
63
|
+
renderDetail={(post) => <Stack gap="md">
|
|
64
|
+
<Stack axis="inline" align="center" justify="between">
|
|
65
|
+
<Text emphasis="strong">{post.title}</Text>
|
|
66
|
+
<IconButton label={t("saved.unsave", { title: post.title })} tone="ghost" onPress={() => unsave(post.id)}>
|
|
67
|
+
<BookmarkIcon />
|
|
68
|
+
</IconButton>
|
|
69
|
+
</Stack>
|
|
70
|
+
<PostDetail post={post} />
|
|
71
|
+
</Stack>} />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
썸네일은 해당 플랫폼 이미지로 슬롯 전체 크기를 `cover`로 채운다.
|
|
75
|
+
|
|
76
|
+
## 축과 기본값
|
|
77
|
+
|
|
78
|
+
| prop | 값 | 기본값 | 설명 |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| items | `readonly {id,title,...}[]`, 필수 | — | 현재 저장된 항목. 해제된 항목은 이 배열에서 제거 |
|
|
81
|
+
| collections | `readonly {id,title,itemIds:readonly string[]}[]`, 필수 | — | 제품이 관리하는 컬렉션과 멤버십 |
|
|
82
|
+
| collectionId | `string` 또는 `null` | `undefined` | 생략하면 컬렉션 홈, `null`이면 모든 게시물, 문자열이면 해당 컬렉션 |
|
|
83
|
+
| selectedItemId | `string` 또는 `null`, 생략 시 상세 없음 | — | 현재 컬렉션에 존재하는 항목만 상세로 표시 |
|
|
84
|
+
| labels | SavedItemsLabels, 필수 | — | 모든 공개 문구를 지역화하여 공급 |
|
|
85
|
+
| onOpenCollection · onOpenItem · onCreateCollection | 콜백, 필수 | — | 제품 상태/라우터 변경 요청 |
|
|
86
|
+
| onBack | `() => void`, 필수 | — | 한 단계만 올라간다. 상세가 열려 있으면 제품이 `selectedItemId`를 비우고, 아니면 `collectionId`를 생략해 컬렉션 홈으로 간다. 화면은 제품 history를 모른다 |
|
|
87
|
+
| actions · leading | ScreenLayout 상속 | 없음 | 홈에서는 제품 `actions` 대신 컬렉션 만들기 버튼이 그 자리를 쓰고 제품 `leading`은 유지된다. 컬렉션·상세에서는 `leading`이 뒤로 버튼이 되고 제품 `actions`가 보인다 |
|
|
88
|
+
| renderThumbnail · renderDetail | `(item) => ReactNode`, 필수 | — | 사진·상세 슬롯, API·자산은 제품 소유 |
|
|
89
|
+
| state · notice | ScreenLayout 상속 | ready · 없음 | 조회 상태, 저장 해제/복구 안내 |
|
|
90
|
+
| layoutStyle | `HjmCompositionStyleProp` | 없음 | Web·Native 모두 목록·상세를 담는 ListDetailScreen 바깥 틀에 적용한다(컬렉션·격자·상세 전환과 상관없이 같은 루트). 미게시(1.12.1 이후) |
|
|
91
|
+
| className(Web) | `string` | 없음 | 안쪽 목록 ScreenLayout에 붙는다 |
|
|
92
|
+
|
|
93
|
+
## 배치
|
|
94
|
+
|
|
95
|
+
| 항목 | 값 | 근거 |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| 크기 | host의 남은 높이 100%, ScreenLayout 폭(최대 720); 컬렉션 표지·게시물 썸네일 1:1, 표지 radius `radius.md` | `ListDetailScreen`, Web `.hjm-saved-*`, Native `saved-items.tsx` |
|
|
98
|
+
| 간격 | 화면 padding `spacing.md` 16; 개인 저장 안내–격자 `spacing.md` 16; 컬렉션 2열 간격 `spacing.md` 16; 표지 4장 사이·게시물 3열 간격 `spacing.xxs` 4; 표지–컬렉션 제목 `spacing.sm` 12 | `Stack gap="md"`, `Grid gap`, Web `.hjm-saved-collection`, Native `gap: spacing.sm` |
|
|
99
|
+
| 순서·정렬 | 홈: 헤더(제목 → 새 컬렉션) → 개인 저장 안내 → 모든 게시물 → 사용자 컬렉션; 컬렉션: 헤더(뒤로 → 컬렉션 제목) → 사진 격자 또는 빈 안내; 상세: 헤더(뒤로 → 항목 제목) → `renderDetail` | `SavedItemsScreen` 렌더 순서 |
|
|
100
|
+
| 고정·스크롤 | 헤더 고정, 본문 스크롤; 상세 동안 격자 mount 유지 | ListDetailScreen·ScreenLayout |
|
|
101
|
+
| 좁은 폭·큰 글자 | 좁은 폭(320 창, 본문 288)에서도 컬렉션 2열·사진 3열을 유지한다. 열 수는 사용 가능 폭이 컬렉션 176 미만(최소 열 폭 80), 사진 140 미만(최소 열 폭 44)일 때만 줄어든다; 컬렉션 제목은 줄바꿈; 사진 속 글자를 유일한 이름으로 쓰지 않는다(버튼 접근성 이름은 `title`) | `Grid minColumnWidth`, `grid.ts` 열 계산 |
|
|
102
|
+
|
|
103
|
+
### 행동 위치
|
|
104
|
+
|
|
105
|
+
| 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
|
|
106
|
+
| --- | --- | --- | --- |
|
|
107
|
+
| 새 컬렉션 | `Button` ghost(`labels.createCollection`) → `onCreateCollection` | 홈 헤더 `actions` 자리(홈에 넘긴 제품 `actions`는 그리지 않는다). 생성 Sheet와 그 footer 확정 버튼은 제품이 그린다 | 홈에서만 1개. 컬렉션·상세에서는 제품 `actions`로 바뀐다 |
|
|
108
|
+
| 뒤로 | `Button` ghost(`labels.back`) → `onBack` | 컬렉션·상세 헤더 `leading` | 1개. 상세 → 격자 → 홈으로 한 단계씩, 단계는 제품이 상태를 비워 정한다 |
|
|
109
|
+
| 컬렉션·게시물 열기 | 표지·썸네일 전체가 버튼 → `onOpenCollection` · `onOpenItem` | 격자 각 칸 | 항목마다 1개 |
|
|
110
|
+
| 저장 해제 | `IconButton` ghost(제품) | `renderDetail` 머리 줄 끝(제목과 같은 줄) | 상세마다 1개. 실행 취소는 `notice` |
|
|
111
|
+
|
|
112
|
+
## 꼭 지킬 것
|
|
113
|
+
|
|
114
|
+
- 컬렉션 홈으로 돌아갈 때 `collectionId` prop을 생략한다. `null`은 모든 게시물을 뜻한다.
|
|
115
|
+
- 저장 해제의 서버 실패/Undo는 앱이 관리한다. 예제의 메모리 저장을 서버 저장으로 주장하지 않는다.
|
|
116
|
+
- 항목·컬렉션 ID와 제목은 비지 않고 ID는 고유해야 한다. 삭제된 컬렉션은 홈으로 복귀하고 없는 멤버는 렌더에서 제외한다.
|
|
117
|
+
- 상세가 열리면 제목·뒤로·상세만 표시한다. 생성 시트의 확정 행동은 footer 한 곳에 둔다.
|
|
118
|
+
- 제품이 목록을 필터링하거나 정렬하여 공급한다. 사진 권한·서버 페이지네이션은 이 화면이 수행하지 않는다.
|
|
119
|
+
|
|
120
|
+
## 함정
|
|
121
|
+
|
|
122
|
+
- 홈 화면에 보여야 할 제품 행동을 `actions`로 넘기면 그려지지 않는다(홈에서는 컬렉션 만들기가 그 자리를 쓴다). Web·Native가 같은 규칙이라 바꾸려면 두 renderer의 API 결정이다.
|
|
123
|
+
- `onBack`에서 `collectionId`와 `selectedItemId`를 한꺼번에 비우면 상세에서 바로 홈으로 건너뛴다.
|
|
124
|
+
|
|
125
|
+
- 비공개 안내는 권한 구현이 아니다. 서버에서 소유자 접근을 검사한다.
|
|
126
|
+
- HTML/WebView나 Instagram 자산을 복사하는 구현이 아니다. 제품 사진·테마·문구를 슬롯으로 전달한다.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# ScreenLayout
|
|
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
|
+
한 라우트 화면의 뼈대가 필요할 때 쓴다. 제목·뒤로 가기 슬롯·도구, 안내(notice), 본문,
|
|
14
|
+
하단 행동을 한 화면으로 배치하고, 화면 전체의 로딩·빈 상태·오류·접근 제한을 본문 자리에서 교체한다.
|
|
15
|
+
데이터 조회·권한 판단·라우팅은 제품 소유다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 앱 전체 navigation·sidebar 셸 | [Layout](layout.md) |
|
|
22
|
+
| 상단 제목 막대만 필요 | [TopBar](top-bar.md) |
|
|
23
|
+
| 하단 고정 주 행동만 필요 | [BottomCTA](bottom-cta.md) |
|
|
24
|
+
| 설정·검색·알림·채팅 화면 | [SettingsScreen](settings-screen.md), [SearchScreen](search-screen.md), [NotificationInboxScreen](notification-inbox-screen.md), [ChatScreen](chat-screen.md) |
|
|
25
|
+
| 로그인 화면 | [AuthScreenLayout](auth-screen-layout.md) |
|
|
26
|
+
| 흐름이 끝난 결과 화면 | 본문에 [Result](result.md) |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `ScreenLayout` | supplemental. root에서는 내보내지 않는다 | `/screens` | `/screens` |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { IconButton } from "@hjmds/react/actions";
|
|
39
|
+
import { BottomCTA } from "@hjmds/react/bottom-cta";
|
|
40
|
+
import { ScreenLayout } from "@hjmds/react/screens";
|
|
41
|
+
|
|
42
|
+
<ScreenLayout
|
|
43
|
+
title={t("saved.title")}
|
|
44
|
+
leading={<IconButton label={t("common.back")} onClick={goBack}><BackGlyph /></IconButton>}
|
|
45
|
+
state={isLoading ? { kind: "loading", title: t("saved.loading") } : { kind: "ready" }}
|
|
46
|
+
footer={<BottomCTA primaryAction={{ label: t("saved.add"), onClick: add }} />}
|
|
47
|
+
>
|
|
48
|
+
<SavedList items={items} />
|
|
49
|
+
</ScreenLayout>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Native — FlatList 본문은 scroll="content"
|
|
54
|
+
import { FlatList } from "react-native";
|
|
55
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
56
|
+
import { ScreenLayout } from "@hjmds/react-native/screens";
|
|
57
|
+
|
|
58
|
+
<ScreenLayout
|
|
59
|
+
title={t("saved.title")}
|
|
60
|
+
scroll="content"
|
|
61
|
+
state={error ? { kind: "error", title: t("saved.error") } : { kind: "ready" }}
|
|
62
|
+
stateAction={<Button onPress={refetch}>{t("common.retry")}</Button>}
|
|
63
|
+
>
|
|
64
|
+
<FlatList data={items} renderItem={renderItem} />
|
|
65
|
+
</ScreenLayout>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 축과 기본값
|
|
69
|
+
|
|
70
|
+
| prop | 값 | 기본값 | 설명 |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `title` | `string` | 필수 | 화면 제목. `header`를 주면 접근성 이름으로만 쓴다 |
|
|
73
|
+
| `header` | `ReactNode` | 없음 | 기존 navigator 헤더 유지용. 주면 기본 헤더(`leading`·제목·`description`·`actions`)를 그리지 않는다 |
|
|
74
|
+
| `description` | `string` | 없음 | 제목 아래 muted 설명 |
|
|
75
|
+
| `leading` · `actions` | `ReactNode` | 없음 | 제목 앞(뒤로 가기)·뒤(도구) 슬롯 |
|
|
76
|
+
| `notice` | `ReactNode` | 없음 | 헤더 아래 비차단 안내. 상태 교체 중에도 남는다 |
|
|
77
|
+
| `footer` | `ReactNode` | 없음 | 하단 고정 행동 |
|
|
78
|
+
| `state` | `{ kind: "ready" }` · `{ kind: "loading" \| "empty" \| "error" \| "restricted"; title; description? }` | `{ kind: "ready" }` | ready가 아니면 children을 그리지 않고 상태와 `stateAction`을 가운데에 놓는다. 로딩은 Spinner 하나만 보이고 `title`·`description`은 Spinner의 접근성 이름이 된다 |
|
|
79
|
+
| `stateAction` | `ReactNode` | 없음 | 상태 안내 아래 행동(재시도 등) |
|
|
80
|
+
| `scroll` | `"screen"` · `"content"` | `"screen"` | `content`는 본문 자식(가상화 목록)이 스크롤을 소유한다. 상태 교체 중에는 `screen`으로 돌아간다 |
|
|
81
|
+
| `contentInset` | `"default"` · `"none"` | `"default"` | `none`은 헤더·notice·본문·footer padding을 0으로(Native는 footer 위 테두리도 뺀다) |
|
|
82
|
+
| `as` (Web) | `"main"` · `"section"` | `"main"` | 제품 셸에 이미 `<main>`이 있으면 `section` |
|
|
83
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | 없음 | 화면 루트 배치 전용(예: 분할 화면의 `flex`·`width`). ScreenLayout 위에 만든 화면은 모두 이 prop을 루트까지 넘긴다 |
|
|
84
|
+
| `className` (Web) | `string` | 없음 | 식별·배치 보조. 색·여백을 덮지 않는다 |
|
|
85
|
+
| `testID` · `scrollRef` · `scrollProps` (Native) | `string` · `Ref<ScrollView>` · `refreshControl`·`keyboardDismissMode`·스크롤 막대 표시 | 없음 | ScrollView가 있을 때(`scroll="screen"` 또는 상태 교체 중)만 `scrollRef`·`scrollProps`가 연결된다 |
|
|
86
|
+
|
|
87
|
+
## 배치
|
|
88
|
+
|
|
89
|
+
| 항목 | 값 | 근거 |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| 크기 | 폭 100%, 최대 `screenPatternRecipe.maxWidth`(`layout.readingMaxWidth` 720), 가운데 정렬; 높이는 host가 준 남은 높이(Web `block-size: 100%`, Native `flex: 1`) | Web `.hjm-screen`, Native `ScreenLayout` |
|
|
92
|
+
| 간격 | 바깥 padding `screenPatternRecipe.padding`(`spacing.md` 16)을 헤더 사방·notice 좌우·본문 사방·footer 사방에 준다(`contentInset="none"`이면 0); 헤더 안 `leading`·제목·`actions` 간격 `itemGap`(`spacing.sm`) 12(두 플랫폼, `contentInset="none"`이어도 유지); 상태 안내는 위아래 `sectionGap`(`spacing.xl`) 24, 안내–`stateAction` `stateGap`(`spacing.md`) 16 | Web `src/screens.tsx`(`--hjm-screen-item-gap`·`--hjm-screen-state-gap`)·`.hjm-screen__*`, Native `ScreenLayout` |
|
|
93
|
+
| 순서·정렬 | 헤더(`leading` → 제목·설명 → `actions`) → notice → 본문 → footer; 상태 안내는 본문 세로 가운데 | 렌더 순서 |
|
|
94
|
+
| 고정·스크롤 | 헤더·notice·footer 고정, `scroll="screen"`은 본문이 스크롤, `"content"`는 자식이 스크롤; footer 위 테두리 1, Web은 하단 safe area만큼 padding을 늘린다 | Web `data-scroll`·`.hjm-screen__footer`, Native `ScrollView` |
|
|
95
|
+
| 좁은 폭·큰 글자 | 제목 열 최소 폭 `headerMinWidth` 120 × 글자 배율, 모자라면 `actions`가 다음 줄로 내려간다; 제목은 줄바꿈(`overflow-wrap: anywhere`)되고 자르지 않는다; Native safe area·탭바 inset은 host | `screenPatternRecipe.headerMinWidth`, Web `.hjm-screen__heading` |
|
|
96
|
+
|
|
97
|
+
## 꼭 지킬 것
|
|
98
|
+
|
|
99
|
+
- Web의 화면 스크롤 본문은 Tab으로 진입할 수 있다. `scroll="content"`의 정상 본문은 자식이 키보드 스크롤을 소유하지만, 오류·빈 상태 등 대체 안내는 화면 스크롤로 바뀌며 Tab 진입도 함께 복원된다.
|
|
100
|
+
- 가상화 목록(FlatList, VirtualList)을 넣으면 `scroll="content"`로 둔다. 스크롤을 이중으로 중첩하지 않는다.
|
|
101
|
+
- 새로고침 실패·저장 오류는 `state`를 바꾸지 말고 `ready` + `notice`로 알린다. `loading`으로 바꾸면 본문이
|
|
102
|
+
unmount되어 입력 초안이 사라진다.
|
|
103
|
+
- `state.title`·문구는 i18n 키로 넣는다. ready 외 상태의 빈 `title`은 `TypeError`다.
|
|
104
|
+
- Web host는 실제 남은 높이(`height:100%` 등)를, Native host는 safe area·탭바 inset을 먼저 처리한다.
|
|
105
|
+
HJM은 기종별 높이를 추측하지 않는다.
|
|
106
|
+
- 배치는 Web·Native 모두 `layoutStyle`로 한다(Web은 `className`도 있다). 색·여백을 덮지 않는다.
|
|
107
|
+
|
|
108
|
+
## 플랫폼 차이
|
|
109
|
+
|
|
110
|
+
| 항목 | Web | Native |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| 루트 | `<main>`(`as="section"` 가능), 제목과 연결 | `View`, 제목 `accessibilityRole="header"` |
|
|
113
|
+
| 상태 알림 | error는 `role="alert"`, 그 외 `status` | `accessibilityLiveRegion`(error assertive) |
|
|
114
|
+
| 스크롤 | CSS(`data-scroll`) | `ScrollView`(`keyboardShouldPersistTaps="handled"`) |
|
|
115
|
+
| 배치·식별 | `layoutStyle`, `className` | `layoutStyle`, `testID` |
|
|
116
|
+
|
|
117
|
+
## 함정
|
|
118
|
+
|
|
119
|
+
- 이미 `<main>`이 있는 제품 셸 안에서는 `as="section"`을 준다. 중첩 main이 생긴다.
|