@hjmds/design-contracts 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +25 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +17 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +5 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +376 -0
- package/docs/sheet.md +12 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +7 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +104 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +215 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +138 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +151 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +274 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# EditorScreen
|
|
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
|
+
글쓰기·프로필 수정처럼 한 화면 전체가 편집 흐름일 때 쓴다. 닫기 버튼, 수정 중 닫기 확인
|
|
14
|
+
(AlertDialog), 저장 버튼의 busy 표시, 초안 상태 안내를 ScreenLayout 위에 한 번에 묶어 준다.
|
|
15
|
+
검증·초안 저장·저장 mutation·라우터/OS 뒤로가기 guard는 제품이 소유한다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 편집 흐름이 없는 일반 화면 | [ScreenLayout](screen-layout.md) |
|
|
22
|
+
| 화면 일부의 입력 묶음과 submit | [Form](form.md) |
|
|
23
|
+
| 편집 없이 확인만 받기 | [AlertDialog](alert-dialog.md) |
|
|
24
|
+
| 짧은 입력을 화면 위에 띄움 | [Sheet](sheet.md), [Dialog](dialog.md) |
|
|
25
|
+
| 프로필 요약과 수정 진입 | [ProfileScreen](profile-screen.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `EditorScreen` | 편집 화면 흐름 | `/screen-flows` | `/screen-flows` |
|
|
32
|
+
| `ScreenFlowAction` (타입) | `submit`·`cancel` 행동 | `/screen-flows` | `/screen-flows` |
|
|
33
|
+
|
|
34
|
+
루트(`@hjmds/react`, `@hjmds/react-native`)에서는 import 할 수 없다. granular subpath만 쓴다.
|
|
35
|
+
|
|
36
|
+
## 최소 사용 예
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Web
|
|
40
|
+
import { EditorScreen } from "@hjmds/react/screen-flows";
|
|
41
|
+
import { Text } from "@hjmds/react/layout";
|
|
42
|
+
import { TextField } from "@hjmds/react/forms";
|
|
43
|
+
|
|
44
|
+
<EditorScreen
|
|
45
|
+
title={t("post.edit.title")}
|
|
46
|
+
dirty={draft !== saved}
|
|
47
|
+
submit={{ label: t("post.edit.save"), pending: saving, disabled: !draft.trim(), onAction: save }}
|
|
48
|
+
cancel={{ label: t("post.edit.close"), onAction: close }}
|
|
49
|
+
discard={{
|
|
50
|
+
mode: "confirm",
|
|
51
|
+
title: t("post.discard.title"),
|
|
52
|
+
description: t("post.discard.description"),
|
|
53
|
+
confirmLabel: t("post.discard.confirm"),
|
|
54
|
+
cancelLabel: t("post.discard.keep"),
|
|
55
|
+
tone: "danger",
|
|
56
|
+
fallbackErrorMessage: t("common.error"),
|
|
57
|
+
}}
|
|
58
|
+
draftStatus={<Text variant="caption" tone="muted">{t("post.edit.draftSaved")}</Text>}
|
|
59
|
+
>
|
|
60
|
+
<TextField label={t("post.edit.body")} value={draft} onValueChange={setDraft} />
|
|
61
|
+
</EditorScreen>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// Native — props는 같다. 조각은 Native subpath에서 가져온다.
|
|
66
|
+
import { EditorScreen } from "@hjmds/react-native/screen-flows";
|
|
67
|
+
import { Text } from "@hjmds/react-native/primitives";
|
|
68
|
+
import { TextField } from "@hjmds/react-native/inputs";
|
|
69
|
+
|
|
70
|
+
<EditorScreen
|
|
71
|
+
title={t("post.edit.title")}
|
|
72
|
+
dirty={draft !== saved}
|
|
73
|
+
submit={{ label: t("post.edit.save"), pending: saving, disabled: !draft.trim(), onAction: save }}
|
|
74
|
+
cancel={{ label: t("post.edit.close"), onAction: close }}
|
|
75
|
+
discard={{
|
|
76
|
+
mode: "confirm",
|
|
77
|
+
title: t("post.discard.title"),
|
|
78
|
+
description: t("post.discard.description"),
|
|
79
|
+
confirmLabel: t("post.discard.confirm"),
|
|
80
|
+
cancelLabel: t("post.discard.keep"),
|
|
81
|
+
tone: "danger",
|
|
82
|
+
fallbackErrorMessage: t("common.error"),
|
|
83
|
+
}}
|
|
84
|
+
draftStatus={<Text variant="caption" tone="muted">{t("post.edit.draftSaved")}</Text>}
|
|
85
|
+
>
|
|
86
|
+
<TextField label={t("post.edit.body")} value={draft} onValueChange={setDraft} />
|
|
87
|
+
</EditorScreen>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 축과 기본값
|
|
91
|
+
|
|
92
|
+
| prop | 값 | 기본값 | 설명 |
|
|
93
|
+
| --- | --- | --- | --- |
|
|
94
|
+
| `dirty` | `boolean` | 필수 | `true`에서 닫기를 누르면 `discard` 확인창을 띄우고, 확인하면 `cancel.onAction`을 부른다. `false`면 바로 부른다 |
|
|
95
|
+
| `submit` | `ScreenFlowAction`(`{ label, onAction(), disabled?, pending? }`) | 필수 | `pending`이면 loading·비활성. 그동안 닫기 버튼도 비활성 |
|
|
96
|
+
| `cancel` | `ScreenFlowAction` | 필수 | `leading` 자리의 ghost 닫기 버튼 |
|
|
97
|
+
| `discard` | AlertDialog confirm 요청에서 `onConfirm`을 뺀 것 + `fallbackErrorMessage: string` | 필수 | `onConfirm`은 EditorScreen이 채운다 |
|
|
98
|
+
| `submitPlacement` | `"footer"` · `"header"` | `"footer"` | `header`면 저장이 상단 `actions` 자리로 가고 footer에는 `draftStatus`만 남는다 |
|
|
99
|
+
| `draftStatus` | `ReactNode` | 없음 | 초안 저장 상태 안내. footer에 놓인다 |
|
|
100
|
+
| `children` | `ReactNode` | 필수 | 편집 본문 |
|
|
101
|
+
| 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | `leading`은 닫기 버튼으로 덮인다. `submitPlacement="footer"`일 때만 `actions`가 그대로 전달된다 |
|
|
102
|
+
|
|
103
|
+
## 배치
|
|
104
|
+
|
|
105
|
+
| 항목 | 값 | 근거 |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| 크기 | ScreenLayout 폭(최대 720) 안에 입력 본문; 저장·닫기는 `Button` 기본 크기 | `ScreenLayout`, `screen-flows.tsx` `Action` |
|
|
108
|
+
| 간격 | 화면 padding `spacing.md` 16; footer 안 `draftStatus`–저장 `spacing.sm` 12(`Stack gap="sm"`); 본문 안 입력 간격은 제품(Form 등) 소유 | Web·Native `EditorScreen` |
|
|
109
|
+
| 순서·정렬 | 헤더(닫기 → 제목 → [저장: header 배치]) → 본문 → footer(`draftStatus` → 저장) | `EditorScreen` 렌더 순서 |
|
|
110
|
+
| 고정·스크롤 | 헤더·footer 고정, 본문 스크롤(`scroll` 기본 `"screen"`); 이탈 확인은 AlertDialog 오버레이 | `ScreenLayout`, `AlertDialog` |
|
|
111
|
+
| 좁은 폭·큰 글자 | 제목 열 최소 폭 120 × 글자 배율, 모자라면 header 저장 버튼이 다음 줄로 내려간다; 키보드·safe area는 host | `screenPatternRecipe.headerMinWidth` |
|
|
112
|
+
|
|
113
|
+
## 꼭 지킬 것
|
|
114
|
+
|
|
115
|
+
- 모든 문구(`label`, `discard`의 제목·설명·버튼)는 i18n 키로 넣는다. 컴포넌트는 기본 문구를 갖지 않는다.
|
|
116
|
+
- `discard`는 `fallbackErrorMessage`까지 필수 타입이다. `onConfirm`은 EditorScreen이 채우므로 넘기지 않는다.
|
|
117
|
+
- 확인 후 초안을 되돌리는 일은 `cancel.onAction` 안에서 제품이 한다.
|
|
118
|
+
- 라우터 뒤로가기·Android back·브라우저 이탈은 이 컴포넌트가 막지 않는다. 제품이 `dirty`로 guard를 건다.
|
|
119
|
+
- 저장 성공 판정·오류 표시는 제품 상태(`notice`, Toast 등)로 한다.
|
|
120
|
+
|
|
121
|
+
## 플랫폼 차이
|
|
122
|
+
|
|
123
|
+
| 항목 | Web | Native |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| 화면 셸 전용 props | `as`, `className` | `testID`, `scrollRef`, `scrollProps` (`layoutStyle`은 양쪽 모두 받는다) |
|
|
126
|
+
| 버튼 이벤트 | 내부에서 `onClick` → `onAction` | 내부에서 `onPress` → `onAction` |
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# EffectSurface
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Composable decorative surfaces](../../effect-surface.md), 별도 보조 기능(supplemental)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/시각 효과/배경 시각 효과`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
환영·온보딩·빈 히어로처럼 분위기를 주는 배경이 필요할 때 내용 뒤에 장식 레이어(mesh·glow·grain)를
|
|
14
|
+
깐다. 장식은 포커스·터치·접근성 이름을 갖지 않고, 내용은 일반 레이아웃에 그대로 남는다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 일반 카드·패널 면 | [Surface](surface.md), [Card](card.md) |
|
|
21
|
+
| 실제 작업(생성·분석) 진행 표시 | [ThinkingOrb](thinking-orb.md) |
|
|
22
|
+
| 성공 순간의 축하 효과 | [Celebration](celebration.md) |
|
|
23
|
+
| 로딩 자리 표시 | [Skeleton](skeleton.md), [Spinner](spinner.md) |
|
|
24
|
+
| 목록의 여러 행마다 움직이는 배경 | 쓰지 않는다(동시에 움직이는 행을 늘리지 않는다) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `EffectSurface` | 기본 — 장식 배경 셸 | `/effect-surface` | `/effect-surface` |
|
|
31
|
+
| `EffectSurfaceDescriptor` (타입) | 보조 — 레이어·seed·강도·주기·색 | `@hjmds/design-contracts/effect-surface` | 같음 |
|
|
32
|
+
|
|
33
|
+
루트에서는 import 할 수 없다. granular subpath만 쓴다.
|
|
34
|
+
|
|
35
|
+
**Native는 optional peer `react-native-svg`(정확히 `15.15.5`)가 앱에 설치돼 있어야 한다.**
|
|
36
|
+
`/effect-surface`가 이 모듈을 파일 최상단에서 import 하므로, 없으면 tsc·단위 테스트는 통과하고 기기 Metro
|
|
37
|
+
번들에서 `Unable to resolve module`로 크래시한다(2026-10 utilverse 사고, celebration·qr-code·thinking-orb·
|
|
38
|
+
toast-liquid도 같은 유형). 쓰기 전에 앱 `package.json`에 peer가 있는지 확인한다. 움직임은 core `Animated`라
|
|
39
|
+
Reanimated·Skia는 필요 없다. Web은 추가 peer가 없다(SVG·WAAPI).
|
|
40
|
+
|
|
41
|
+
## 최소 사용 예
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// Web
|
|
45
|
+
import { EffectSurface } from "@hjmds/react/effect-surface";
|
|
46
|
+
|
|
47
|
+
<EffectSurface descriptor={{ layers: ["mesh", "grain"], seed: "welcome", active: true }}>
|
|
48
|
+
<WelcomeContent />
|
|
49
|
+
</EffectSurface>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Native
|
|
54
|
+
import { EffectSurface } from "@hjmds/react-native/effect-surface";
|
|
55
|
+
|
|
56
|
+
<EffectSurface descriptor={{ layers: ["mesh", "grain"], seed: "welcome", active: true }} visible={isFocused}>
|
|
57
|
+
<WelcomeContent />
|
|
58
|
+
</EffectSurface>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 축과 기본값
|
|
62
|
+
|
|
63
|
+
| prop | 값 | 기본값 | 설명 |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| `descriptor.layers` | `mesh` · `glow` · `grain` | `["mesh"]` | 서로 다른 1~3개 |
|
|
66
|
+
| `descriptor.intensity` | 0~1 | `0.22` | — |
|
|
67
|
+
| `descriptor.period` | 2~120초 | `12` | — |
|
|
68
|
+
| `descriptor.seed` | 문자열 | `"hjm"` | 빈 문자열 금지 |
|
|
69
|
+
| `descriptor.active` | `true` · `false` | `false` | 켤 때만 천천히 움직인다. reduced motion이면 provider 설정에 따라 멈춘다 |
|
|
70
|
+
| `descriptor.colors` | `ColorReference` 세 개 | `themeColor("primary")` · `themeColor("contentBrand")` · `themeColor("surfaceAccent")` | — |
|
|
71
|
+
|
|
72
|
+
| `children` | `ReactNode` | — (필수) | 일반 레이아웃 내용 |
|
|
73
|
+
| Native `visible` | `boolean` | `true` | 화면·목록 소유자가 가려짐을 알린다. `false`면 장식이 멈춘다 |
|
|
74
|
+
| Web `layoutStyle` | 배치 전용 style | — | 루트 배치 |
|
|
75
|
+
| Native `style` | `StyleProp<ViewStyle>` | — | 컨테이너 `View`. 정식 motion host 프레임이라 deprecated 대상이 아니다 |
|
|
76
|
+
|
|
77
|
+
- 콜백 prop은 없다. `descriptor`는 생략할 수 있고(`{}`), 잘못된 값은 렌더 중 `TypeError`/`RangeError`를 던진다.
|
|
78
|
+
|
|
79
|
+
## 배치
|
|
80
|
+
|
|
81
|
+
| 항목 | 값 | 근거 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| 크기 | EffectSurface는 여백을 갖지 않는다. 크기는 children이 정하고 장식 레이어가 그 영역 전체(`absolute`, inset 0)를 덮는다. 넘치는 장식은 잘린다(`overflow: hidden`). 배경은 테마 `bg`다. 높이를 늘리려면 children 높이, Web `layoutStyle`·Native `style`의 높이로 정한다. 장식 레이어는 배치에 참여하지 않는다 | `react/src/effect-surface.tsx`, `react-native/src/effect-surface.tsx` |
|
|
84
|
+
| 간격 | 내용 여백·간격은 children 쪽 [Stack](stack.md) 등으로 준다 | `react/src/effect-surface.tsx` |
|
|
85
|
+
| 순서·정렬 | children은 일반 레이아웃이다. [Stack](stack.md)으로 eyebrow → [Heading](heading.md) → 소개 → [Button](button.md)을 쌓는다(Showcase 랜딩 예는 `gap="lg"` 20, Heading `level1`) | `showcase/web/src/patterns/Landing.stories.tsx` |
|
|
86
|
+
| 고정·스크롤 | 화면 위쪽 히어로 하나에만 둔다. 같은 화면에 여러 개를 겹치거나 목록 행마다 두지 않는다 | — |
|
|
87
|
+
| 좁은 폭·큰 글자 | — | — |
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
┌────────────────────────────┐ ← EffectSurface(배경 bg + mesh/glow/grain, 터치 없음)
|
|
91
|
+
│ eyebrow │
|
|
92
|
+
│ 큰 제목(Heading level1) │ ← children: 일반 레이아웃(Stack gap lg 20)
|
|
93
|
+
│ 소개 문장 │
|
|
94
|
+
│ [시작하기] primary │
|
|
95
|
+
└────────────────────────────┘
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 꼭 지킬 것
|
|
99
|
+
|
|
100
|
+
- 색은 `@hjmds/design-contracts/color-references`의 `themeColor`·`accentColor` 참조로 넘겨 제품 테마를 따르게 한다.
|
|
101
|
+
hex를 하드코딩하거나 Showcase의 예시 색·seed를 제품 기본값으로 복사하지 않는다([테마](../../theming.md)).
|
|
102
|
+
- 임의 브랜드 색·강도에서 글자 대비를 보장하지 않는다. 실제 조합을 확인하거나 중요한 글자·버튼은 불투명
|
|
103
|
+
[Surface](surface.md) 위에 둔다.
|
|
104
|
+
- Native 화면·목록 소유자는 화면이 가려지거나 목록 창 밖이면 `visible={false}`를 넘긴다.
|
|
105
|
+
- `active`는 효과가 의미 있을 때만 켠다.
|
|
106
|
+
|
|
107
|
+
## 플랫폼 차이
|
|
108
|
+
|
|
109
|
+
| 항목 | Web | Native |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| 컨테이너 배치 | `layoutStyle`(그 밖에 `className`) | `style`(컨테이너 `View`, `layoutStyle` 없음) |
|
|
112
|
+
| 가시성 | IntersectionObserver·`document.hidden`로 자동 | `visible`(기본 `true`) + AppState |
|
|
113
|
+
| 그리기 | 인라인 SVG + WAAPI | `react-native-svg` + core `Animated` |
|
|
114
|
+
| 렌더 실패 | 애니메이션 생성 거부 시 정적 SVG 유지 | 장식만 제거하고 children은 유지(remount 전까지) |
|
|
115
|
+
|
|
116
|
+
## 함정
|
|
117
|
+
|
|
118
|
+
- Native의 장식 실패 대비(error boundary)는 **렌더 오류**만 잡는다. peer가 없어 모듈 해석이 실패하면 앱 번들 자체가
|
|
119
|
+
깨진다. `tsc` 통과를 설치 확인으로 보지 않는다.
|
|
120
|
+
- descriptor 검증 오류는 대비 밖에 있어 그대로 던져진다. 값 범위를 지킨다.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Result와의 경계](../../result.md#emptystate와의-경계), [ContentState 범위 축](../../content-state.md), recipe `emptyStateRecipe`(`src/component-recipes.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/상태와 알림/빈 상태`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
목록이 비었거나 검색 결과가 0건이라 **아직 없음**을 알릴 때 쓴다. 조건이 바뀌면 다시 채워질 자리이고,
|
|
14
|
+
사용자는 그 화면에 머물며 필터를 바꾸거나 첫 항목을 만든다. 아이콘은 항상 중립색이고 상태 tone이 없다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 흐름이 끝남(결제 성공·실패 등) | [Result](result.md) |
|
|
21
|
+
| 불러오는 중 | [Skeleton](skeleton.md), [Spinner](spinner.md) |
|
|
22
|
+
| 화면은 그대로 두고 알릴 문제 | [Notice](notice.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `EmptyState` | 기본 | `@hjmds/react`, `/feedback` | `@hjmds/react-native`, `/feedback` |
|
|
29
|
+
|
|
30
|
+
## 최소 사용 예
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Web
|
|
34
|
+
import { Button } from "@hjmds/react/actions";
|
|
35
|
+
import { EmptyState } from "@hjmds/react/feedback";
|
|
36
|
+
|
|
37
|
+
<EmptyState
|
|
38
|
+
icon={<InboxGlyph />} // 제품 소유 아이콘
|
|
39
|
+
title={t("inbox.empty.title")}
|
|
40
|
+
description={t("inbox.empty.description")}
|
|
41
|
+
action={<Button tone="secondary" onClick={compose}>{t("inbox.empty.compose")}</Button>}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// Native
|
|
47
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
48
|
+
import { EmptyState } from "@hjmds/react-native/feedback";
|
|
49
|
+
|
|
50
|
+
<EmptyState
|
|
51
|
+
illustration={<InboxGlyph />} // 제품 소유 아이콘
|
|
52
|
+
title={t("inbox.empty.title")}
|
|
53
|
+
description={t("inbox.empty.description")}
|
|
54
|
+
action={<Button tone="secondary" onPress={compose}>{t("inbox.empty.compose")}</Button>}
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 축과 기본값
|
|
59
|
+
|
|
60
|
+
| prop | 값 | 기본값 | 설명 |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| `density` | `compact` · `regular` | `regular` | `compact`는 세로 여백이 작고 Native에서 남은 공간을 채우지 않는다 |
|
|
63
|
+
| `align`(Native) | `center` · `upper` | `center` | `upper`는 `regular`일 때 내용을 위쪽(1:3 여백)에 둔다 |
|
|
64
|
+
| `announcement`(Native) | `none` · `polite` · `assertive` | `none` | 상태가 바뀌어 빈 화면이 새로 나타날 때만 켠다 |
|
|
65
|
+
| `titleRole`(Native) | 접근성 role | `"header"` | Web은 항상 `role="status"`로 그린다 |
|
|
66
|
+
| `title` | Web `ReactNode`(필수) · Native `string` | — | — |
|
|
67
|
+
| `description` | Web `ReactNode` · Native `string` | — | — |
|
|
68
|
+
| `action` | `ReactNode`(Button 하나) | — | 콜백은 Button의 `onClick`·`onPress`가 갖는다 |
|
|
69
|
+
| Web `icon` · Native `illustration` | `ReactNode` | — | 장식, 접근성에서 숨김 |
|
|
70
|
+
| `layoutStyle` | 배치 전용 style | — | 루트 배치. Native `style`·`illustrationStyle`·`titleStyle`·`descriptionStyle`·`actionStyle`은 deprecated |
|
|
71
|
+
|
|
72
|
+
EmptyState 자체에는 콜백 prop이 없다.
|
|
73
|
+
|
|
74
|
+
## 배치
|
|
75
|
+
|
|
76
|
+
| 항목 | 값 | 근거 |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 크기 | 그림 슬롯은 `glyph.lg`(28) 정사각형이다(Native `illustration` 상자 고정 크기). 세로 여백: `compact` `spacing.xl`(24). `regular` `spacing.xxxl`(40, 두 플랫폼) | `component-recipes.ts` `emptyStateRecipe`, `styles.css` `.hjm-empty-state` |
|
|
79
|
+
| 간격 | 항목 사이 간격 `spacing.xs`(8), 좌우 여백 `spacing.xl`(24) | `component-recipes.ts` `emptyStateRecipe` |
|
|
80
|
+
| 순서·정렬 | 위→아래 순서는 그림 → 제목 → 설명 → 행동이고 가운데 정렬이다. 행동은 Button 하나(`tone="secondary"` 또는 `primary`)를 내용 폭만큼 둔다 | `react-native/src/feedback.tsx` `EmptyState` |
|
|
81
|
+
| 고정·스크롤 | 비어 있는 목록·검색 결과 영역 **안**에 둔다. 위 TopBar·검색칸·필터는 그대로 남기고 그 아래 내용 영역만 EmptyState로 바꾼다. Native `regular`는 남은 높이를 채우고(`flexGrow: 1`) 세로 가운데에 놓는다. `align="upper"`는 위:아래 빈 공간을 1:3으로 나눠 내용이 위쪽에 온다. 화면 하단 고정 행동이 필요하면 [BottomCTA](bottom-cta.md)로 따로 둔다 | `react-native/src/feedback.tsx` `EmptyState` |
|
|
82
|
+
| 좁은 폭·큰 글자 | 카드·시트 안처럼 높이가 작은 자리는 `compact`(채우지 않음)를 쓴다 | `react-native/src/feedback.tsx` `EmptyState` |
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
┌──────────────────────────┐
|
|
86
|
+
│ TopBar / 검색칸 (그대로) │
|
|
87
|
+
├──────────────────────────┤
|
|
88
|
+
│ │ ← flexGrow 1(upper: 위 1)
|
|
89
|
+
│ (그림) │
|
|
90
|
+
│ 제목 │ gap xs 8
|
|
91
|
+
│ 설명 │
|
|
92
|
+
│ [첫 항목 만들기] │
|
|
93
|
+
│ │ ← (upper: 아래 3)
|
|
94
|
+
└──────────────────────────┘
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 꼭 지킬 것
|
|
98
|
+
|
|
99
|
+
- 제목·설명·행동 문구는 i18n 키로 넣는다. "검색 0건"과 "아직 만든 것 없음"은 다른 문구로 구분한다.
|
|
100
|
+
- 다음 행동이 있으면 `action`에 Button 하나를 둔다. 아이콘·일러스트는 장식이라 접근성에서 숨겨진다.
|
|
101
|
+
- Native는 `title`·`description`·`accessibilityLabel` 중 하나는 있어야 한다. 모두 없으면 `TypeError`를 던진다.
|
|
102
|
+
- 일러스트·아이콘 자산은 제품 소유다. 색은 recipe가 정하므로 슬롯 색을 덮지 않는다.
|
|
103
|
+
- 배치는 `layoutStyle`로 한다. Native 슬롯 style(`style`·`illustrationStyle`·`titleStyle`·`descriptionStyle`·`actionStyle`)은
|
|
104
|
+
deprecated(개발 모드 1회 경고, 다음 major 제거)다 — 배치는 `layoutStyle`, 외형은 `density`·`align`으로 옮긴다.
|
|
105
|
+
|
|
106
|
+
## 플랫폼 차이
|
|
107
|
+
|
|
108
|
+
| 항목 | Web | Native |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| 그림 슬롯 | `icon`(ReactNode) | `illustration`(ReactNode, 아이콘 크기 상자) |
|
|
111
|
+
| `title` | 필수, ReactNode | 선택, string |
|
|
112
|
+
| `description` | ReactNode | string |
|
|
113
|
+
| 정렬·알림 | 없음 | `align`, `announcement`, `accessibilityLabel`, `titleRole` |
|
|
114
|
+
| 배치 | `layoutStyle`(그 밖에 `className`과 div 속성) | `layoutStyle`(슬롯 style 다섯 개는 deprecated) |
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Field
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: recipe `fieldRecipe`(`src/base-recipes.ts`), GestureSheetInput: [Optional presentation adapters](../../optional-adapters.md)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/입력 필드`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
라벨·도움말·오류를 가진 입력 칸에 쓴다. 한 줄 텍스트 입력은 `TextField`를 쓰고, `Field`는 HJM에 없는
|
|
14
|
+
제품 고유 컨트롤(예: 직접 만든 선택기)에 같은 라벨·도움말·오류 틀과 접근성 연결을 씌울 때 쓴다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 여러 줄 입력 | [TextArea](text-area.md) |
|
|
21
|
+
| 비밀번호·인증 코드·숫자 | [PasswordField](password-field.md), [OtpField](otp-field.md), [NumberField](number-field.md) |
|
|
22
|
+
| 검색어 입력 | [SearchField](search-field.md) |
|
|
23
|
+
| 목록에서 고르기·날짜 | [Select](select.md), [Combobox](combobox.md), [DatePicker](date-picker.md) |
|
|
24
|
+
| 입력 묶음의 submit·오류 포커스 | [Form](form.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `Field` | 기본 — 커스텀 컨트롤용 틀 | `@hjmds/react`, `/forms` | `@hjmds/react-native`, `/forms` |
|
|
31
|
+
| `TextField` | 동반 — 한 줄 텍스트 입력 | `@hjmds/react`, `/forms` | `@hjmds/react-native`, `/inputs` |
|
|
32
|
+
| `GestureSheetInput` | 확장 — GestureSheet 안의 키보드 추적 입력 | 없음 | `/sheet-gesture` |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { TextField, Field } from "@hjmds/react/forms";
|
|
39
|
+
|
|
40
|
+
<TextField
|
|
41
|
+
label={t("profile.nickname")}
|
|
42
|
+
description={t("profile.nicknameHint")}
|
|
43
|
+
error={nicknameError ? t(nicknameError) : undefined}
|
|
44
|
+
required
|
|
45
|
+
value={nickname}
|
|
46
|
+
onValueChange={setNickname}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<Field controlId="profile-color" label={t("profile.color")} error={colorError}>
|
|
50
|
+
{(control) => <ProductColorPicker {...control} value={color} onChange={setColor} />}
|
|
51
|
+
</Field>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Native
|
|
56
|
+
import { TextField } from "@hjmds/react-native/inputs";
|
|
57
|
+
import { Field } from "@hjmds/react-native/forms";
|
|
58
|
+
|
|
59
|
+
<TextField label={t("profile.nickname")} description={t("profile.nicknameHint")} required
|
|
60
|
+
value={nickname} onValueChange={setNickname} {...(nicknameError ? { error: t(nicknameError) } : {})} />
|
|
61
|
+
|
|
62
|
+
<Field label={t("profile.color")} {...(colorError ? { error: t(colorError) } : {})}>
|
|
63
|
+
{(control) => <ProductColorPicker {...control} value={color} onChange={setColor} />}
|
|
64
|
+
</Field>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 축과 기본값
|
|
68
|
+
|
|
69
|
+
| prop | 값 | 기본값 | 설명 |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| `variant`(`TextField`) | `surface` · `inset` | `surface` | — |
|
|
72
|
+
| `shape`(`TextField`) | `medium` · `large` · `full` | `medium` | — |
|
|
73
|
+
| `align`(`TextField`) | `start` · `center` | `start` | `center`는 닉네임·코드처럼 짧은 한 값 |
|
|
74
|
+
| `onValueChange`(`TextField`) | `(value: string) => void` | — | Web·Native 같은 이름·모양. Web은 DOM `onChange`(이벤트)도 함께 불린다. 공용 폼 코드는 `onValueChange`를 쓴다 |
|
|
75
|
+
| `children`(`Field` render-prop) | `(control: FieldControlProps) => ReactNode` 또는 `ReactNode` | — | Web `FieldControlProps`는 `{ id, required, disabled, "aria-invalid"?: true, "aria-describedby"? }`, Native는 `{ accessibilityLabel, accessibilityHint?, accessibilityState: { disabled } }`(hint는 오류 우선) |
|
|
76
|
+
| `busy`(Native `TextField`) | `boolean` | `false` | 편집을 막고 접근성 busy를 싣는다 |
|
|
77
|
+
|
|
78
|
+
- 상태 테두리는 recipe가 정한다: 기본 `borderControl`, 포커스 `contentBrand`, 오류 `danger`.
|
|
79
|
+
- 도움말은 오류가 생겨도 사라지지 않고 둘 다 보인다(Web은 `aria-describedby`로 둘 다 연결).
|
|
80
|
+
- 비활성(`disabled`)은 라벨과 컨트롤만 흐리고 도움말·오류는 원래 대비 그대로 둔다(`fieldRecipe.disabledScope`).
|
|
81
|
+
흐림 정도는 컴포넌트 recipe에 `states.disabledOpacity`가 있으면 그 값(SearchField·PasswordField·OtpField·NumberField·Select
|
|
82
|
+
0.5), 없으면 `fieldRecipe.disabledOpacity` 0.6(TextField·TextArea·Mentions·Field·NativeSelect·Combobox·DatePicker·TagsInput)이다.
|
|
83
|
+
2026-10-06 전에는 틀 전체가 흐려져 잠긴 이유를 적은 도움말까지 대비가 떨어졌다.
|
|
84
|
+
|
|
85
|
+
## 배치
|
|
86
|
+
|
|
87
|
+
| 항목 | 값 | 근거 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| 크기 | 컨트롤 최소 높이 44(`control.minTouchTarget`), 안쪽 여백 좌우 `spacing.md`(16)·위아래 `spacing.sm`(12), 테두리 1. 여러 줄 입력은 최소 80(`fieldRecipe.multilineMinHeight`)이고 `minVisibleLines`를 줘도 이 하한 아래로 내려가지 않는다(한 줄 시작은 MessageComposer 내부만). 폭은 부모를 채운다 | `base-recipes.ts` `fieldRecipe`, `styles.css` `.hjm-field__control` |
|
|
90
|
+
| 간격 | 라벨과 컨트롤 사이 `spacing.xs`(8). 컨트롤과 도움말·오류 사이는 Web `spacing.xs`(8), Native 내장 필드 6(`fieldRecipe.support.gap`)이다. 필드 사이 간격은 [Form](form.md) `density`가 정한다(`comfortable` `spacing.lg` 20). Form 없이 쌓을 때는 Stack `gap`으로 같은 값을 쓴다 | `styles.css` `.hjm-field`, `react-native/src/internal/field-frame.tsx` |
|
|
91
|
+
| 순서·정렬 | 위→아래 순서는 라벨 → 컨트롤 → 도움말 → 오류다. 필드 여러 개는 Form 안에 세로로 쌓는다 | `react-native/src/internal/field-frame.tsx` |
|
|
92
|
+
| 고정·스크롤 | — | — |
|
|
93
|
+
| 좁은 폭·큰 글자 | 큰 글자에서는 컨트롤 높이가 줄 높이에 따라 커진다. 라벨·도움말은 줄바꿈하며 자르지 않는다 | `base-recipes.ts` `fieldRecipe` |
|
|
94
|
+
|
|
95
|
+
## 꼭 지킬 것
|
|
96
|
+
|
|
97
|
+
- 라벨·도움말·오류·placeholder는 i18n 키로 넣는다. 오류 문장에 도움말을 다시 쓰지 않는다.
|
|
98
|
+
- 라벨을 숨기면 접근성 이름을 준다. Web `TextField`는 `label` 또는 `aria-label`, Native는 `label` 또는
|
|
99
|
+
`accessibilityLabel`이 없으면 `TypeError`를 던진다.
|
|
100
|
+
- 정렬·높이는 `align`·`variant`·`shape` 축으로 바꾼다. Native `TextField`는 `style`을 받지 않고 배치는 `layoutStyle`로만 한다.
|
|
101
|
+
- 커스텀 컨트롤에는 render-prop 값을 그대로 펼쳐 라벨 연결을 끊지 않는다. Web `Field`는 `controlId`가 필수다.
|
|
102
|
+
- 비활성인 이유는 도움말(`description`)에 적는다. 도움말·오류는 흐려지지 않으므로 제품에서 따로 흐리게 하거나 색을 바꾸지 않는다.
|
|
103
|
+
|
|
104
|
+
### GestureSheetInput (Native optional-extension)
|
|
105
|
+
|
|
106
|
+
`GestureSheet` 안에서 키보드를 따라 움직이는 입력이다. `BottomSheetTextInput`의 props를 그대로 받고
|
|
107
|
+
TextField와 같은 테두리·글꼴·placeholder 색을 입힌다. 라벨·도움말·오류 틀은 없으므로 접근성 이름을 직접 준다.
|
|
108
|
+
`/sheet-gesture`만 이 optional peer를 요구한다: `@gorhom/bottom-sheet` `5.2.14`, `react-native-reanimated`(이 파일이
|
|
109
|
+
직접 import), 그리고 문서 기준 Gesture Handler `2.32.0`·Worklets `0.10.1`과 `GestureHandlerRootView`·`GestureSheetProvider`
|
|
110
|
+
설정. 없으면 tsc는 통과하고 기기 번들에서 실패한다.
|
|
111
|
+
|
|
112
|
+
## 플랫폼 차이
|
|
113
|
+
|
|
114
|
+
| 항목 | Web | Native |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| 값 콜백 | `onValueChange`(+ DOM `onChange`) | `onValueChange` |
|
|
117
|
+
| `TextField` 앞뒤 슬롯 | `leading`, `trailing` | 없음 |
|
|
118
|
+
| `TextField` 진행 중 | 없음 | `busy`(편집 막음, 접근성 busy) |
|
|
119
|
+
| `Field` 라벨 | `label` ReactNode + `controlId` | `label` string, `controlId` 없음 |
|
|
120
|
+
| 비활성 커스텀 `Field` | 틀이 라벨과 자식 컨트롤을 함께 흐린다 | 라벨만 흐린다. 컨트롤은 감싸는 View 없이 틀의 직계 자식이라 `accessibilityState.disabled`를 보고 스스로 흐린다 |
|
|
121
|
+
| 배치 | `layoutStyle`(틀 `.hjm-field`) · `fieldClassName`(틀) · `className`·`style`(안쪽 `<input>`) | `layoutStyle`(`Field`·`TextField` 모두). `TextField`는 `style`을 받지 않는다 |
|
|
122
|
+
| 나머지 props·ref | `<input>` HTML 속성(`type`·`autoComplete`·`inputMode` 등)과 ref(`HTMLInputElement`) 전달 | RN `TextInput` props(`keyboardType`·`autoCapitalize`·`textContentType`·`returnKeyType`·`onSubmitEditing` 등)와 ref(`TextInput`) 전달. `style`·`onChangeText`는 받지 않는다 |
|
|
123
|
+
|
|
124
|
+
## 함정
|
|
125
|
+
|
|
126
|
+
- Web `TextField`의 `style`은 틀이 아니라 안쪽 `<input>`에 붙는다. 필드 전체의 바깥 여백·폭은 `layoutStyle`로 준다.
|
|
127
|
+
- Native `TextField`·`Field`의 `error`·`description`은 `string`이라 `exactOptionalPropertyTypes`에서 `undefined`를 받지
|
|
128
|
+
않는다(TS2375). 위 예처럼 조건부 spread로 넘긴다. Web은 `ReactNode`라 `undefined`를 그대로 넘겨도 된다.
|
|
129
|
+
- Web `Field`는 `controlId`가 필수이고, Native `Field`에는 `controlId`가 없다. 공용 코드에서 같은 props 객체를 넘기지 않는다.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# FilePicker
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [FilePicker](../../file-picker.md), 판정 `resolveFilePickerSelection`(`@hjmds/design-contracts/components/file-picker`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/파일 선택`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
사용자가 업로드할 로컬 파일을 고르게 할 때 쓴다. 받는 형식(`accept`), 최대 크기, 최대 개수를
|
|
14
|
+
descriptor로 선언하면 선택 결과가 `{ accepted, rejected }`로 함께 돌아온다. FilePicker는 "무엇을
|
|
15
|
+
골랐는가"까지만 소유한다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 업로드 진행·성공·실패·재시도 표시 | [UploadItem](upload-item.md) |
|
|
22
|
+
| 파일이 아닌 텍스트 값 입력 | [Field](field.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `FilePicker` | 기본 | `@hjmds/react`, `/file-picker`, `/forms` | `@hjmds/react-native`, `/file-picker`, `/inputs` |
|
|
29
|
+
|
|
30
|
+
결과 타입(`FilePickerSelectionResult`, `FilePickerCandidate`)은 `@hjmds/design-contracts/components/file-picker`에 있다.
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { FilePicker } from "@hjmds/react/file-picker";
|
|
37
|
+
|
|
38
|
+
<FilePicker
|
|
39
|
+
descriptor={{ mode: "multiple", accept: ["image/*", ".pdf"], maxSizeBytes: 10_000_000, maxCount: 5 }}
|
|
40
|
+
label={t("attach.label")}
|
|
41
|
+
buttonLabel={t("attach.choose")}
|
|
42
|
+
dropzoneLabel={t("attach.drop")}
|
|
43
|
+
existingCount={files.length}
|
|
44
|
+
onSelect={({ accepted, rejected }) => { addFiles(accepted); reportRejected(rejected); }}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Native
|
|
50
|
+
import { FilePicker } from "@hjmds/react-native/file-picker";
|
|
51
|
+
|
|
52
|
+
<FilePicker
|
|
53
|
+
descriptor={{ accept: ["application/pdf"], maxSizeBytes: 10_000_000 }}
|
|
54
|
+
label={t("attach.label")}
|
|
55
|
+
buttonLabel={t("attach.choose")}
|
|
56
|
+
onPick={pickWithDocumentPicker} // 제품 adapter: 취소면 null, 아니면 FilePickerCandidate[]
|
|
57
|
+
onPickError={reportPickError}
|
|
58
|
+
onSelect={({ accepted, rejected }) => { addFiles(accepted); reportRejected(rejected); }}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 축과 기본값
|
|
63
|
+
|
|
64
|
+
| prop | 값 | 기본값 | 설명 |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `descriptor.mode` | `single` · `multiple` | `single` | `maxCount`는 `multiple`에서만 쓴다. `single`에 주면 던진다 |
|
|
67
|
+
| `existingCount` | 0 이상 정수 | `0` | 여러 번 나눠 고르는 흐름은 이미 고른 개수를 넘겨야 `maxCount`가 누적으로 판정된다 |
|
|
68
|
+
| `descriptor` | `{ mode?, accept?: readonly string[], maxSizeBytes?, maxCount? }` | — | `accept`는 MIME 패턴(`image/*`) 또는 확장자(`.pdf`). 비우면 제한 없음 |
|
|
69
|
+
| `onSelect` | `(result: { accepted: readonly FilePickerCandidate[]; rejected: readonly FilePickerRejection[] }) => void` | 필수 | 받아들인 것과 거부한 것을 한 번에 준다 |
|
|
70
|
+
| Native `onPick` | `() => Promise<readonly FilePickerCandidate[] \| null>` | 필수 | 제품 adapter. 취소면 `null` |
|
|
71
|
+
| Native `onPickError` | `(error: unknown) => void` | 필수 | `onPick`이 reject하면 이쪽으로만 온다 |
|
|
72
|
+
| Web `getCandidateId` | `(file: File, index: number) => string` | `name:size:lastModified:index` | 후보 id 규칙 |
|
|
73
|
+
| `disabled` | `true` · `false` | `false` | — |
|
|
74
|
+
| `hint`, `error` | 문구 | — | 선택 사항이다 |
|
|
75
|
+
|
|
76
|
+
- `FilePickerCandidate`: `{ id, name, mimeType, sizeBytes }`(`mimeType`은 플랫폼이 못 정하면 `""`).
|
|
77
|
+
- `FilePickerRejection`: `{ file, reason: "unsupported-type", accept }` · `{ file, reason: "too-large", maxSizeBytes }` ·
|
|
78
|
+
`{ file, reason: "count-exceeded", maxCount }`. 거부 문장은 `reason`과 함께 오는 한계값으로 만든다.
|
|
79
|
+
|
|
80
|
+
## 배치
|
|
81
|
+
|
|
82
|
+
| 항목 | 값 | 근거 |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| 크기 | Web 진입부는 dropzone이다. 점선 테두리 1, radius `radius.lg`(16), 안쪽 여백 `spacing.xl`(24), 최소 높이 8rem(128). 폭은 부모를 채운다. 버튼(Native trigger 포함)은 최소 높이 44(`control.minTouchTarget`), 좌우 여백 `spacing.md`(16)다 | `file-picker.ts` `filePickerRecipe`, `styles.css` `.hjm-file-picker__dropzone` |
|
|
85
|
+
| 간격 | 항목 간격은 Web `spacing.xs`(8), Native 6이다. dropzone 안 문구와 버튼 사이 간격은 `spacing.sm`(12)이다 | `styles.css` `.hjm-file-picker`, `react-native/src/file-picker.tsx` |
|
|
86
|
+
| 순서·정렬 | 위→아래 순서는 라벨 → 진입부 → 도움말 → 오류다. dropzone 안에 문구와 버튼을 가운데 정렬로 쌓는다. Native trigger는 테두리 1·radius 12 상자로 시작 쪽에 붙고(`alignSelf: flex-start`) 내용 폭만큼만 차지한다. 고른 파일 목록은 FilePicker 아래에 [UploadItem](upload-item.md)으로 따로 쌓는다. FilePicker 안에 넣지 않는다 | `react/src/file-picker.tsx`, `react-native/src/file-picker.tsx` |
|
|
87
|
+
| 고정·스크롤 | — | — |
|
|
88
|
+
| 좁은 폭·큰 글자 | — | — |
|
|
89
|
+
|
|
90
|
+
## 꼭 지킬 것
|
|
91
|
+
|
|
92
|
+
- 라벨·버튼·dropzone 문구와 거부 문장은 제품이 i18n 키로 만든다. 거부 문장은 `reason`과 한계값으로
|
|
93
|
+
만들고 색으로만 알리지 않는다.
|
|
94
|
+
- `rejected`가 있어도 `accepted`는 그대로 쓴다. 한 장이 막혔다고 전체 선택을 버리지 않는다.
|
|
95
|
+
- FilePicker는 선택 목록을 쌓지 않는다. 고른 파일 목록·업로드 상태는 제품 상태와 UploadItem이 소유한다.
|
|
96
|
+
- Native 실제 OS document/image picker 연결(`onPick`)은 제품 책임이다. 패키지는 이를 구현하거나
|
|
97
|
+
기기에서 검증하지 않는다. 제품 릴리스 QA에서 확인한다.
|
|
98
|
+
|
|
99
|
+
## 플랫폼 차이
|
|
100
|
+
|
|
101
|
+
| 항목 | Web | Native |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| 파일 진입 | 숨긴 `<input type="file">` + 보이는 버튼 + dropzone | 제품 adapter `onPick`(비동기) |
|
|
104
|
+
| 필수 prop 차이 | `dropzoneLabel` | `onPick`, `onPickError` |
|
|
105
|
+
| 드래그 앤 드롭 | 있음(버튼과 항상 함께) | 없음 |
|
|
106
|
+
| 진행 중 잠금 | 없음 | `onPick` 대기 중 버튼 `busy`·비활성 |
|
|
107
|
+
| `label`·`hint`·`error` 타입 | `ReactNode` | `string` |
|
|
108
|
+
| 후보 id | `getCandidateId`(기본 `name:size:lastModified:index`) | adapter가 `id`를 채움 |
|
|
109
|
+
| 배치 | `layoutStyle`(루트), `className`과 div 속성 | `layoutStyle`. `style`은 deprecated(개발 모드 1회 경고, 다음 major 제거) — `layoutStyle` 또는 recipe |
|
|
110
|
+
|
|
111
|
+
## 함정
|
|
112
|
+
|
|
113
|
+
- Native `onPick`이 reject하면 `onSelect`는 불리지 않고 `onPickError`로만 전달된다. 빈 함수로 두면 피커 실패가 화면에 남지 않는다.
|
|
114
|
+
- 브라우저 `accept`와 OS 피커 필터는 힌트일 뿐이다. 거부 판정은 반드시 `onSelect` 결과로 처리한다.
|