@hjmds/design-contracts 1.12.0 → 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,120 @@
|
|
|
1
|
+
# SearchField
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: `src/component-recipes.ts`(`searchFieldRecipe`). 별도 계약 문서는 없다
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/검색 입력`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
목록·화면 안에서 검색어를 입력받을 때 쓴다. 검색 아이콘, 값이 있을 때의 지우기 버튼,
|
|
14
|
+
조회 중 진행 표시가 기본으로 들어 있다. 입력값 상태만 소유하고 조회·결과는 제품이 맡는다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 입력하며 후보 중 하나를 선택해 값으로 확정 | [Combobox](combobox.md) |
|
|
21
|
+
| 앱 전체 명령·이동 검색(Web) | [CommandPalette](command-palette.md) |
|
|
22
|
+
| 검색이 아닌 일반 텍스트 입력 | [Field](field.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `SearchField` | 기본 | `@hjmds/react`, `/forms` | `@hjmds/react-native`, `/inputs` |
|
|
29
|
+
|
|
30
|
+
## 최소 사용 예
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Web
|
|
34
|
+
import { SearchField } from "@hjmds/react/forms";
|
|
35
|
+
|
|
36
|
+
<SearchField
|
|
37
|
+
aria-label={t("feed.search.label")}
|
|
38
|
+
placeholder={t("feed.search.placeholder")}
|
|
39
|
+
clearLabel={t("common.clearSearch")}
|
|
40
|
+
value={query}
|
|
41
|
+
onValueChange={setQuery}
|
|
42
|
+
loading={isFetching}
|
|
43
|
+
/>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Native
|
|
48
|
+
import { SearchField } from "@hjmds/react-native/inputs";
|
|
49
|
+
|
|
50
|
+
<SearchField
|
|
51
|
+
accessibilityLabel={t("feed.search.label")}
|
|
52
|
+
placeholder={t("feed.search.placeholder")}
|
|
53
|
+
clearLabel={t("common.clearSearch")}
|
|
54
|
+
busyLabel={t("common.searching")}
|
|
55
|
+
value={query}
|
|
56
|
+
onValueChange={setQuery}
|
|
57
|
+
busy={isFetching}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 축과 기본값
|
|
62
|
+
|
|
63
|
+
| prop | 값 | 기본값 | 설명 |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| `size` | `medium` · `large` | `medium` | 최소 높이 44 · 52 |
|
|
66
|
+
| `variant` | `surface` · `inset` | — | — |
|
|
67
|
+
| `shape` | `medium` · `large` · `full` | `medium` | — |
|
|
68
|
+
| `align` | `start` · `center` | — | — |
|
|
69
|
+
| `value` · `defaultValue` | 문자열 | `""` | — |
|
|
70
|
+
| `onValueChange` | `(value: string) => void` | — | 입력마다. 지우기를 누르면 `""`로 부른 뒤 `onClear`를 부른다. Web은 원시 `onChange`도 받는다 |
|
|
71
|
+
| `onClear` | `() => void` | — | 지우기 버튼을 누른 뒤 |
|
|
72
|
+
| `loading`(Web) / `busy`(Native) | `boolean` | `false` | 진행 중에는 지우기 대신 진행 표시가 뒤에 선다 |
|
|
73
|
+
| `clearLabel` | 문자열 | — | 필수, 지우기 버튼 접근성 이름. Native는 `busyLabel`도 필수 |
|
|
74
|
+
| `renderSearchIcon`(Web) · `renderLeading`(Native) | `(props) => ReactNode` — Web `{ color: "currentColor", size }`, Native `{ color, size, disabled }` | 기본 돋보기 | 제품 아이콘 adapter |
|
|
75
|
+
| `renderClearIcon` · `renderLoadingIndicator`(Web) · `renderBusyIndicator`(Native) | 위와 같은 모양 | 기본 아이콘·스피너 | — |
|
|
76
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 필드 전체 배치. Web `style`은 안쪽 input에 붙는다 |
|
|
77
|
+
| `label` / `aria-label`(Web) / `accessibilityLabel`(Native) | 문자열 | — | 하나는 필수. 없으면 렌더 중 `TypeError` |
|
|
78
|
+
|
|
79
|
+
## 배치
|
|
80
|
+
|
|
81
|
+
| 항목 | 값 | 근거 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| 크기 | 최소 높이 `medium` 44 · `large` 52. 지우기 버튼 `medium` 지름 36 + hitSlop 4, `large` 44(Web은 36 원에 `::after` 4px로 터치 44를 만든다) | `searchFieldRecipe.sizes`, `control.minTouchTarget`, `control.buttonHeight.large`, `.hjm-search-field__clear` |
|
|
84
|
+
| 간격 | 필드 안 좌우 `medium` `spacing.sm` 12 · `large` `spacing.md` 16, 안쪽 요소 사이 `medium` `spacing.xs` 8 · `large` `spacing.sm` 12(두 플랫폼). 아래 결과 목록과 `layout.contentGap` 16. 좌우는 화면 `layout.pagePadding`(`compact` 16 · `regular` 20)을 따른다 | `searchFieldRecipe.sizes`, `layout` |
|
|
85
|
+
| 순서·정렬 | 돋보기 → 입력 → 지우기/진행 표시. 목록·결과 바로 위에 폭을 꽉 채워 둔다. 필터 칩·정렬 버튼은 같은 줄이 아니라 아래 줄에 둔다 | `searchFieldRecipe.slots` |
|
|
86
|
+
| 고정·스크롤 | 컴포넌트 자체는 고정되지 않는다. 스크롤 중에도 보여야 하면 스크롤 영역 밖(목록 위 고정 영역)에 둔다 | — |
|
|
87
|
+
| 좁은 폭·큰 글자 | 높이는 최소값이라 큰 글자에서 늘어난다. 좁은 폭에서도 한 줄 전체 폭을 쓴다 | `minHeight`(`searchFieldRecipe.sizes`) |
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
┌──────────────────────────────┐
|
|
91
|
+
│ TopBar │ ← 고정
|
|
92
|
+
├──────────────────────────────┤
|
|
93
|
+
│ ┌──────────────────────────┐ │
|
|
94
|
+
│ │ 🔍 검색어 (×) │ │ 44 (medium), 좌우 pagePadding
|
|
95
|
+
│ └──────────────────────────┘ │
|
|
96
|
+
│ gap layout.contentGap 16 │
|
|
97
|
+
│ 결과 행 │ ↕ 스크롤
|
|
98
|
+
│ 결과 행 │
|
|
99
|
+
└──────────────────────────────┘
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 꼭 지킬 것
|
|
103
|
+
|
|
104
|
+
- 모든 문구(`placeholder`, `clearLabel`, `busyLabel`)는 i18n 키로 넣는다.
|
|
105
|
+
- 아이콘은 제품 adapter로 그린다(`renderSearchIcon`/`renderLeading`, `renderClearIcon`). 크기·색은 렌더 함수가
|
|
106
|
+
받은 값을 쓴다. 지우기 버튼·스피너를 `trailing`으로 따로 만들지 않는다. `trailing`은 지우기/진행이
|
|
107
|
+
없을 때만 보인다.
|
|
108
|
+
- 디바운스·요청 취소는 제품 소유다. `onValueChange`마다 바로 요청하지 않는다.
|
|
109
|
+
- 배치는 `layoutStyle`로 한다(Native는 `style`이 타입에서 빠져 있다). Web `fieldClassName`은 필드 틀에, `style`은 안쪽
|
|
110
|
+
input에 붙는다. 색·높이·radius를 덮지 않는다.
|
|
111
|
+
|
|
112
|
+
## 플랫폼 차이
|
|
113
|
+
|
|
114
|
+
| 항목 | Web | Native |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| 진행 상태 prop | `loading`(+`renderLoadingIndicator`) | `busy`(+`renderBusyIndicator`, `busyLabel` 필수) |
|
|
117
|
+
| 검색 아이콘 | `renderSearchIcon`, 또는 `leading` | `renderLeading`, 또는 `leading` |
|
|
118
|
+
| 진행 중 입력 | 입력 가능(`aria-busy`) | 입력 가능(`accessibilityState.busy`). 2026-10-06까지 Native는 `busy`인 동안 입력을 무시했다(1.12.1 이후 미게시) |
|
|
119
|
+
| 입력 요소 | `<input type="search">`, 원시 `onChange`도 전달 | `TextInput` |
|
|
120
|
+
| 배치 | `layoutStyle`·`fieldClassName`(필드 틀), `style`은 안쪽 input | `layoutStyle` |
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# SearchScreen
|
|
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`; 기존 개별 지침을 새 규격으로 통합. `onSubmit`·`filtersOverflow`는 2026-10-06 사용자 요청("검색과 필터 있는 앱 서비스 따라해")의 검색 화면 개편에서 추가. 같은 날 사용자 위임 결정(권장안)으로 미리보기에만 있던 두 단계 검색 로직(`committedQuery`·`recentQueries`·`suggestedQueries`·`suggestions`·`resultSummary`·`appliedFilters`·`filterSheet`)과 `queryLabelVisibility`를, utilverse 적용 조사로 `searching`·`searchingLabel`을 추가([계약 결정 표](../../screen-patterns.md#searchscreen-두-단계-검색)). 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
|
|
9
|
+
- 스토리북: `배포/화면/검색/검색 결과와 필터`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
검색어 입력, 필터, 최근 검색, 결과 목록을 갖춘 검색 화면 전체에 쓴다. 입력 debounce(기본 300ms)와
|
|
14
|
+
이전 요청 취소(`AbortSignal`)를 공통으로 처리한다. 검색 API, 오류 처리, 필터·정렬, 늦은 응답 무시는 제품 소유다.
|
|
15
|
+
입력 중 제안과 확정 후 결과를 나누는 두 단계 검색은 `committedQuery`+`onSubmit`으로 켠다. 그러면 단계 전환, 모든 확정 경로,
|
|
16
|
+
최근·추천 검색어와 제안의 배치, 결과 개수·정렬, 적용 필터 칩, 초안/적용 필터 시트, 0건 원인별 복구, 지운 뒤 포커스, 개수 낭독을
|
|
17
|
+
SearchScreen이 맡고 제품은 데이터·요청·문구·아이콘만 넘긴다. 한 줄 가로 스크롤 필터 칩 줄은 `filtersOverflow="scroll"`로 만든다.
|
|
18
|
+
화면 전체 배치는 [검색 화면 지침](../screens/common-search.md)을 따른다.
|
|
19
|
+
|
|
20
|
+
## 쓰지 않을 때
|
|
21
|
+
|
|
22
|
+
| 상황 | 대신 쓸 것 |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| 다른 화면 안의 검색 입력 하나 | [SearchField](search-field.md) |
|
|
25
|
+
| 입력하며 값 하나를 고름 | [Combobox](combobox.md) |
|
|
26
|
+
| Web 명령·이동 검색 | [CommandPalette](command-palette.md) |
|
|
27
|
+
| 검색 없는 목록·상세 | [ListDetailScreen](list-detail-screen.md) |
|
|
28
|
+
|
|
29
|
+
## 공개 이름과 import
|
|
30
|
+
|
|
31
|
+
| 이름 | 역할 | Web | Native |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| `SearchScreen` | supplemental. root에서는 내보내지 않는다 | `/screen-flows` | `/screen-flows` |
|
|
34
|
+
|
|
35
|
+
## 최소 사용 예
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Web
|
|
39
|
+
import { SearchScreen } from "@hjmds/react/screen-flows";
|
|
40
|
+
|
|
41
|
+
<SearchScreen queryClearLabel={t("common.clearSearch")}
|
|
42
|
+
title={t("search.title")}
|
|
43
|
+
queryLabel={t("search.query")}
|
|
44
|
+
query={query}
|
|
45
|
+
onQueryChange={setQuery}
|
|
46
|
+
onSearch={(value, { signal }) => {
|
|
47
|
+
if (!value.trim()) return setResults([]);
|
|
48
|
+
void searchApi(value, { signal })
|
|
49
|
+
.then((result) => { if (!signal.aborted) setResults(result); })
|
|
50
|
+
.catch((error) => { if (!signal.aborted) setError(error); });
|
|
51
|
+
}}
|
|
52
|
+
filters={<ProductFilters />}
|
|
53
|
+
recentSearches={<RecentSearches onPick={setQuery} />}
|
|
54
|
+
>
|
|
55
|
+
<ProductResults items={results} />
|
|
56
|
+
</SearchScreen>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// Native — clear·busy가 있는 SearchField를 입력 slot으로
|
|
61
|
+
import { SearchScreen } from "@hjmds/react-native/screen-flows";
|
|
62
|
+
import { SearchField } from "@hjmds/react-native/inputs";
|
|
63
|
+
|
|
64
|
+
<SearchScreen queryClearLabel={t("common.clearSearch")}
|
|
65
|
+
title={t("search.title")}
|
|
66
|
+
queryLabel={t("search.query")}
|
|
67
|
+
query={query}
|
|
68
|
+
onQueryChange={setQuery}
|
|
69
|
+
onSearch={runSearch}
|
|
70
|
+
queryField={<SearchField label={t("search.query")} clearLabel={t("common.clearSearch")}
|
|
71
|
+
busyLabel={t("common.searching")} value={query} onValueChange={setQuery} busy={isFetching} />}
|
|
72
|
+
>
|
|
73
|
+
<ProductResults items={results} />
|
|
74
|
+
</SearchScreen>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 두 단계 검색(최소 연결)
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// Web
|
|
81
|
+
import { SearchScreen } from "@hjmds/react/screen-flows";
|
|
82
|
+
|
|
83
|
+
<SearchScreen queryClearLabel={t("common.clearSearch")}
|
|
84
|
+
title={t("search.title")}
|
|
85
|
+
queryLabel={t("search.query")}
|
|
86
|
+
queryLabelVisibility="hidden"
|
|
87
|
+
query={query}
|
|
88
|
+
onQueryChange={setQuery}
|
|
89
|
+
onSearch={(value, { signal }) => loadSuggestions(value, signal)} // 입력 중: 제안
|
|
90
|
+
committedQuery={committed}
|
|
91
|
+
onSubmit={(value) => { setCommitted(value); saveRecent(value); }} // 모든 확정: 결과·최근 검색
|
|
92
|
+
suggestions={{
|
|
93
|
+
items: suggestions,
|
|
94
|
+
commitLabel: (q) => t("search.commitRow", { q }),
|
|
95
|
+
countLabel: (n) => t("search.suggestionCount", { n }),
|
|
96
|
+
}}
|
|
97
|
+
resultSummary={{
|
|
98
|
+
count: isFetching ? null : results.length,
|
|
99
|
+
countLabel: (n) => t("search.resultCount", { n }),
|
|
100
|
+
loadingLabel: t("search.loading"),
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<ResultList items={results} />
|
|
104
|
+
</SearchScreen>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// Native
|
|
109
|
+
import { SearchScreen } from "@hjmds/react-native/screen-flows";
|
|
110
|
+
|
|
111
|
+
<SearchScreen queryClearLabel={t("common.clearSearch")}
|
|
112
|
+
title={t("search.title")}
|
|
113
|
+
queryLabel={t("search.query")}
|
|
114
|
+
queryLabelVisibility="hidden"
|
|
115
|
+
query={query}
|
|
116
|
+
onQueryChange={setQuery}
|
|
117
|
+
onSearch={(value, { signal }) => loadSuggestions(value, signal)} // 입력 중: 제안
|
|
118
|
+
committedQuery={committed}
|
|
119
|
+
onSubmit={(value) => { setCommitted(value); saveRecent(value); }} // 모든 확정: 결과·최근 검색
|
|
120
|
+
suggestions={{
|
|
121
|
+
items: suggestions,
|
|
122
|
+
commitLabel: (q) => t("search.commitRow", { q }),
|
|
123
|
+
countLabel: (n) => t("search.suggestionCount", { n }),
|
|
124
|
+
}}
|
|
125
|
+
resultSummary={{
|
|
126
|
+
count: isFetching ? null : results.length,
|
|
127
|
+
countLabel: (n) => t("search.resultCount", { n }),
|
|
128
|
+
loadingLabel: t("search.loading"),
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<ResultList items={results} />
|
|
132
|
+
</SearchScreen>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
최근·추천 검색어, 적용 필터, 필터 시트, 정렬까지 모두 연결한 예는 [검색 화면 지침의 코드 골격](../screens/common-search.md#코드-골격)에 있다.
|
|
136
|
+
|
|
137
|
+
## 축과 기본값
|
|
138
|
+
|
|
139
|
+
| prop | 값 | 기본값 | 설명 |
|
|
140
|
+
| --- | --- | --- | --- |
|
|
141
|
+
| `query` | `string` | 필수 | 제어형 검색어 |
|
|
142
|
+
| `queryLabel` | `string` | 필수 | 기본 입력의 label |
|
|
143
|
+
| `onQueryChange` | `(value: string) => void` | 필수 | 입력·지우기(`""`) |
|
|
144
|
+
| `onSearch` | `(query: string, context: { signal: AbortSignal }) => void` | 필수 | `debounceMs` 뒤 호출. 값이 바뀌면 이전 타이머를 지우고 이전 신호를 abort한다 |
|
|
145
|
+
| `queryClearLabel` | `string` | `queryField`가 없으면 필수 | 기본 입력의 지우기 버튼 이름. 비어 있으면 `TypeError` |
|
|
146
|
+
| `queryField` | `ReactNode` | 없음 | 기본 `SearchField` 대신 그릴 입력. 지우기 동작을 직접 제공한다 |
|
|
147
|
+
| `debounceMs` | `number` | `300` | 0 미만은 0 |
|
|
148
|
+
| `filters` | `ReactNode` | 없음 | 검색 입력 아래, 상단에 고정. 두 단계 검색에서는 결과 단계에만, 검색어 때문인 0건에는 숨긴다 |
|
|
149
|
+
| `onSubmit` | `(query: string) => void` | 없음 | 확정 신호. 기본 입력의 Web Enter(`enterKeyHint="search"`, IME 조합 중 Enter는 무시)·Native 키보드 검색 키(`returnKeyType="search"`, `onSubmitEditing`)와 SearchScreen이 그린 확정 행·제안·최근·추천 검색어가 모두 여기로 온다. 앞뒤 공백을 지운 값이고 공백만이면 부르지 않는다. 고른 값이 입력과 다르면 먼저 `onQueryChange`. 대기 중인 `onSearch` debounce는 그대로 둔다. `queryField`를 주면 그 입력의 키는 연결하지 않는다 |
|
|
150
|
+
| `committedQuery` | `string` | 없음 | 결과를 보여 줄 확정 검색어. 주면 두 단계 검색(`onSubmit` 필수, 타입). 단계: 검색어 공백 = 검색 전, 확정값과 다름 = 입력 중, 같음 = 결과(앞뒤 공백 무시). `children`은 결과 단계에만 그린다 |
|
|
151
|
+
| `queryLabelVisibility` | `visible` · `hidden` | `visible` | `hidden`이면 보이는 label 없이 `queryLabel`을 접근성 이름(Web `aria-label`, Native `accessibilityLabel`)과 placeholder로 쓴다 |
|
|
152
|
+
| `searching` · `searchingLabel` | `boolean` · `string` | 없음 | 기본 입력의 진행 표시(Web `loading`, Native `busy`+`busyLabel`). 함께 주거나 함께 생략(타입). 켜져 있으면 `searchingLabel`을 낭독한다 |
|
|
153
|
+
| `recentQueries` | `{ items, title, clearAllLabel, onClearAll(), removeLabel(query), onRemove(query), icon?, removeIcon?, maxVisible? }` | 없음 | 검색 전 본문 맨 위. 행 = 확정, 행 끝 × = `onRemove`, 제목 끝 = `onClearAll`. 기본 5행(`searchScreenRecipe.recentVisible`) |
|
|
154
|
+
| `suggestedQueries` | `{ title, items: string[] }` | 없음 | 검색 전(최근 검색 아래)과 검색어 때문인 0건 아래 Chip 줄. 누르면 확정 |
|
|
155
|
+
| `suggestions` | `{ items: { query, match?: { start, end } }[], commitLabel(query), countLabel(count), icon?, maxVisible? }` | 없음 | 입력 중 본문: 첫 행 `commitLabel`(그대로 확정) + 제안 최대 6행. `match` 범위는 굵게(Web). 개수를 `countLabel`로 낭독 |
|
|
156
|
+
| `resultSummary` | `{ count: number \| null, countLabel(count), loadingLabel, sort?, notice?, empty?: { title, description? } }` | 없음 | 결과 머리. `count = null`이면 개수 자리 Skeleton + `children` 대신 로딩 4행. `notice`는 개수 위(이전 결과 유지 오류). `count = 0`이면 개수 대신 `empty`로 EmptyState |
|
|
157
|
+
| `resultSummary.sort` | `{ label, triggerLabel, value, options: { id, label }[], onChange(id), icon? }` (Native는 `dismissLabel` 추가) | 없음 | 결과 머리 끝 Menu(single). 바꾸면 본문을 맨 위로 |
|
|
158
|
+
| `appliedFilters` | `{ items: { key, label }[], removeLabel(label), onRemove(key), clearAllLabel, onClearAll(), removeIcon? }` | 없음 | 결과 머리 아래 × 칩 + `모두 해제`. 개수는 trigger 이름과 0건 원인 판정에도 쓴다 |
|
|
159
|
+
| `filterSheet` | `{ open, onOpenChange(open), title, value, onApply(next), count(draft): number \| null, reset(draft), isDefault(draft), renderContent(draft, setDraft), labels: { close, reset, apply(count) }, size?, trigger? }` | 없음 | 열 때 `value`를 초안으로 복사, 닫히면 초안을 버림, 주 행동에서만 `onApply`. `count = 0`이면 주 행동 비활성, `isDefault`면 초기화 비활성. `trigger`를 주면 칩 줄 맨 앞에 `label(적용 개수)` Chip |
|
|
160
|
+
| `filtersOverflow` | `wrap` · `scroll` | `wrap` | `scroll`이면 `filters`를 한 줄 가로 스크롤 영역에 넣고 화면 좌우 여백(`spacing.md` 16)만큼 가장자리까지 넓힌다. 첫 칩은 검색 입력과 같은 시작선에 놓인다 |
|
|
161
|
+
| `recentSearches` | `ReactNode` | 없음 | `query`가 공백일 때만 본문 맨 위에 보인다 |
|
|
162
|
+
| `children` | `ReactNode` | 필수 | 결과 |
|
|
163
|
+
| 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | `notice`는 필터 아래에 붙는다 |
|
|
164
|
+
|
|
165
|
+
## 배치
|
|
166
|
+
|
|
167
|
+
| 항목 | 값 | 근거 |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| 크기 | ScreenLayout 폭(최대 720); 검색 입력은 padding 안 전체 폭 | `ScreenLayout`, `SearchField` |
|
|
170
|
+
| 간격 | 화면 padding `spacing.md` 16(notice 영역은 좌우만); 검색 입력·`filters`·`notice` 사이 `spacing.sm` 12; 본문 최근 검색–결과 `spacing.lg` 20 | Web·Native `SearchScreen` `Stack gap="sm"`·`gap="lg"` |
|
|
171
|
+
| 순서·정렬 | 헤더(제목) → 검색 입력 → [`filterSheet.trigger`, `filters`] → `notice` → 본문. 본문은 검색 전: `recentSearches` → `recentQueries` → `suggestedQueries`(사이 `spacing.xl` 24) / 입력 중: 확정 행 → 제안 / 결과: `resultSummary.notice` → 개수·정렬 → 적용 필터(사이 `spacing.sm` 12) → `children`(위 `spacing.lg` 20) | 렌더 순서, `searchScreenRecipe` |
|
|
172
|
+
| 고정·스크롤 | 검색 입력·필터는 notice 자리에 고정, 본문만 스크롤; `state`가 바뀌어도 입력·필터는 남는다 | `ScreenLayout` `notice` |
|
|
173
|
+
| 좁은 폭·큰 글자 | 입력은 폭을 줄여 맞춘다. `filtersOverflow="wrap"`은 필터가 줄바꿈되어 고정 영역이 줄 수만큼 커진다. `scroll`은 큰 글자에서도 한 줄(Web 2배 글자 실측: 칩 줄 52, 1배 44)로 고정 영역을 묶는다 | `SearchScreen`, `.hjm-search-screen__filters` |
|
|
174
|
+
|
|
175
|
+
## 꼭 지킬 것
|
|
176
|
+
|
|
177
|
+
- 검색어가 있으면 오른쪽 ×로 전체를 지우고 입력 포커스를 유지한다. 기본 입력의 `queryClearLabel`은 제품 i18n 문구다. `queryField`를 교체할 때도 동일한 지우기 동작을 제공한다(2026-10-06 사용자 요청). 지우기는 `onQueryChange("")`를 거쳐 기존 debounce·요청 취소 경로로 전달된다.
|
|
178
|
+
|
|
179
|
+
- `onSearch`는 첫 렌더 뒤와 빈 `query`에도 불린다. 빈 검색어 처리(요청 생략)는 제품이 한다.
|
|
180
|
+
- 응답을 반영하기 전에 `signal.aborted`를 확인한다. 취소는 신호만 줄 뿐 늦은 응답을 막지 않는다.
|
|
181
|
+
- 실패와 0건을 구분한다. 오류는 이전 결과를 남기고 본문 위 `Notice tone="danger"` + 다시 시도, 0건은 `EmptyState`로 그린다.
|
|
182
|
+
- 문구(`title`, `queryLabel`, 상태 제목)는 i18n 키로 넣는다.
|
|
183
|
+
- 최근 검색은 `onSubmit`에서만 저장한다. SearchScreen은 모든 확정(키·제안·최근·추천)을 `onSubmit`으로 보내고 debounce된
|
|
184
|
+
`onSearch`는 보내지 않는다. `onSearch`에서 저장하면 "산", "산책"이 따로 쌓인다(2026-10-06 개편 전 공통 검색 예제의 결함).
|
|
185
|
+
- 두 단계 검색에서 `children`에는 결과 목록만 넣는다. 제안·로딩 행·0건 EmptyState·개수·정렬을 다시 그리면 SearchScreen이 그린 것과 겹친다.
|
|
186
|
+
- 적용 조건은 `appliedFilters`로 넘긴다. 0건 원인(필터면 칩 줄 유지 + `모두 해제`, 검색어면 칩 줄 숨김 + 추천 검색어)을 이 개수로 판정한다.
|
|
187
|
+
- 시트 `count`와 결과 개수는 같은 계산에서 나와야 한다. 다르면 "N개 결과 보기"와 적용 뒤 개수가 어긋난다.
|
|
188
|
+
- 칩이 여러 개인 필터 줄은 `filtersOverflow="scroll"`로 한 줄에 둔다. 제품이 CSS·ScrollView로 가로 스크롤을 따로 만들지 않는다
|
|
189
|
+
(가장자리 여백·포커스 링 잘림·RTL이 제품마다 갈린다).
|
|
190
|
+
- 검색 로딩·0건·오류는 `state`로 본문을 바꾸지 않고 `children` 안에서 그린다(입력·필터·이전 결과를 지키기 위해서다. 2026-10-06 개편 전
|
|
191
|
+
예제는 `state="error"`로 이전 결과까지 지웠다). `state`는 화면 자체를 쓸 수 없는 경우(로그인 필요 등)에만 쓴다.
|
|
192
|
+
|
|
193
|
+
## 플랫폼 차이
|
|
194
|
+
|
|
195
|
+
| 항목 | Web | Native |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| `queryField` slot | 넘기면 기본 SearchField 대신 그린다 | 넘기면 기본 SearchField 대신 그린다 |
|
|
198
|
+
| 기본 입력 | `SearchField` + `queryLabel`·`queryClearLabel` | `SearchField` + `queryLabel`·`queryClearLabel`, `busyLabel`도 `queryLabel` |
|
|
199
|
+
| `onSubmit` | Enter keydown. `isComposing`·keyCode 229(IME 조합)는 무시 | `onSubmitEditing`. 한 줄 입력의 기본 blur-on-submit으로 키보드가 닫힌다 |
|
|
200
|
+
| `queryLabelVisibility="hidden"` | `aria-label` + `placeholder` | `accessibilityLabel` + `placeholder` |
|
|
201
|
+
| `searching` | `loading`: 입력 가능, `aria-busy`, 숨긴 `role="status"`로 `searchingLabel` | `busy`+`busyLabel`: 입력 가능, `announceForAccessibilityWithOptions(queue)` |
|
|
202
|
+
| 개수 낭독 | 숨긴 `role="status"` 하나 | `announceForAccessibilityWithOptions(…, { queue: true })`, 문구가 바뀔 때만 |
|
|
203
|
+
| 지운 뒤 포커스 | 적용 칩·최근 검색 ×: 같은 자리 다음 항목 → 없으면 trigger(최근 검색은 입력) | 옮기지 않는다(낭독 커서 유지) |
|
|
204
|
+
| 제안 강조 | `match` 범위 굵게 | ListRow 제목이 문자열이라 강조 없음 |
|
|
205
|
+
| 정렬 | Menu 목록, 바꾸면 `.hjm-screen__body` 맨 위 | Menu에 `sort.dismissLabel` 필요, 바꾸면 본문 ScrollView 맨 위(`scrollRef`는 그대로 전달) |
|
|
206
|
+
| 필터 시트 | 창 폭 ≥ 960(expanded)이면 옆 시트(`placement="end"`), footer [초기화][주] | 아래 시트 `scrollable`, footer 세로 fullWidth 주 먼저 |
|
|
207
|
+
| `filtersOverflow="scroll"` | `div.hjm-search-screen__filters`(overflow-x auto, 스크롤바 숨김, 위아래로 포커스 링 두께만큼 여백). Tab 이동 시 브라우저가 칩을 보이게 스크롤한다 | 가로 `ScrollView`(`keyboardShouldPersistTaps="handled"`, 표시기 숨김), `contentInset="none"`이면 넓히지 않는다 |
|
|
208
|
+
|
|
209
|
+
## 함정
|
|
210
|
+
|
|
211
|
+
- 기본값(`queryLabelVisibility="visible"`)은 label을 보이게 그려 고정 영역을 1배 약 22, 2배 약 40 더 쓴다(2026-10-06 실측). 검색만 하는 화면은 `hidden`을 쓴다.
|
|
212
|
+
- 1.12.1 이하 Native에서는 `searching`인 동안 SearchField `busy`가 입력을 무시해 그동안 친 글자가 사라졌다. 다음 릴리스부터 두 플랫폼 모두 입력을 받으므로 입력 중 제안 조회에도 켤 수 있다. 1.12.1 이하에 머무는 Native 제품은 확정 결과 요청에만 켠다.
|
|
213
|
+
- Native 필터 시트는 닫힘 애니메이션이 끝난 뒤에 다시 열린다(Sheet 계약). 닫자마자 `open`을 켜도 바로 보이지 않을 수 있다.
|
|
214
|
+
- Web `filters` 안에 `Stack wrap`을 넣어도 `scroll`에서는 한 줄이다(자식 폭을 `max-content`로 잡는다).
|
|
215
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Section
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: `src/component-recipes.ts`(`sectionRecipe`, Native renderer가 바인딩). 별도 계약 문서는 없다
|
|
9
|
+
- 스토리북: `배포/컴포넌트/레이아웃/섹션`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
화면 안의 내용 묶음에 제목·설명·머리 행동(“모두 보기”, “편집”)을 붙일 때 쓴다.
|
|
14
|
+
큰 글자에서는 머리 행동이 제목 아래로 내려가 겹치지 않는다. 배경·테두리가 없는 의미 구획이다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 배경·테두리가 있는 떠 있는 묶음 | [Card](card.md), [Surface](surface.md) |
|
|
21
|
+
| 접고 펼치는 구획 | [Accordion](accordion.md), [Collapsible](collapsible.md) |
|
|
22
|
+
| 제목 텍스트만 필요 | [Heading](heading.md) |
|
|
23
|
+
| 단순 세로 간격 | [Stack](stack.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `Section` | 기본 | `@hjmds/react`, `/layout` | `@hjmds/react-native`, `/primitives` |
|
|
30
|
+
|
|
31
|
+
## 최소 사용 예
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Web
|
|
35
|
+
import { Link } from "@hjmds/react/actions";
|
|
36
|
+
import { Section } from "@hjmds/react/layout";
|
|
37
|
+
|
|
38
|
+
<Section
|
|
39
|
+
title={t("home.recent.title")}
|
|
40
|
+
action={<Link href="/recent">{t("common.seeAll")}</Link>}
|
|
41
|
+
>
|
|
42
|
+
<RecentList />
|
|
43
|
+
</Section>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Native — 섹션 사이 간격은 감싼 Stack gap(`layout.sectionGap` 24)이 기본. 단독 배치만 layoutStyle
|
|
48
|
+
import { spacing } from "@hjmds/design-contracts/foundations";
|
|
49
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
50
|
+
import { Section } from "@hjmds/react-native/primitives";
|
|
51
|
+
|
|
52
|
+
<Section
|
|
53
|
+
title={t("home.recent.title")}
|
|
54
|
+
description={t("home.recent.description")}
|
|
55
|
+
action={<Button tone="link" onPress={openRecent}>{t("common.seeAll")}</Button>}
|
|
56
|
+
layoutStyle={{ marginTop: spacing.xl }}
|
|
57
|
+
>
|
|
58
|
+
<RecentList />
|
|
59
|
+
</Section>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 축과 기본값
|
|
63
|
+
|
|
64
|
+
| prop | 값 | 기본값 | 설명 |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `title` · `description` | Web `ReactNode` · Native `string` | — | 현지화 |
|
|
67
|
+
| `action` | 노드 하나 | — | 머리 끝 행동(링크·`tone="link"` 버튼) |
|
|
68
|
+
| `title`·`description`·`action` 모두 없음 | — | — | 머리 영역을 그리지 않는다 |
|
|
69
|
+
| `children` | ReactNode | 필수 | |
|
|
70
|
+
| `headingLevel`(Web) | `2` ~ `6` | `2` | 문서 구조에 맞춰 고른다. Native 제목은 `accessibilityRole="header"` |
|
|
71
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 섹션 바깥 배치 |
|
|
72
|
+
| `headerStyle` · `copyStyle` · `actionStyle` · `contentStyle`(Native) | 배치 전용 style 객체 | — | slot 배치(색·글자는 받지 않는 타입) |
|
|
73
|
+
|
|
74
|
+
콜백 prop은 없다.
|
|
75
|
+
|
|
76
|
+
## 배치
|
|
77
|
+
|
|
78
|
+
| 항목 | 값 | 근거 |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| 크기 | 자체 배경·테두리·여백이 없다. 폭은 부모를 따른다 | `.hjm-section` |
|
|
81
|
+
| 글자 | 제목 `title` 18/26 bold, 설명 `caption` 11/16(두 플랫폼). 2026-10-06까지 Web은 둘 다 본문 14를 물려받았다(1.12.1 이후 미게시) | `sectionRecipe.title`·`description`, `.hjm-section__title`·`__description` |
|
|
82
|
+
| 간격 | 머리와 본문 사이 `spacing.xs` 8, 글자 묶음과 행동 사이 `spacing.sm` 12, 제목과 설명 사이 `spacing.xxs` 4. Section끼리의 간격은 Section이 주지 않는다. 감싸는 [Stack](stack.md)이나 `layoutStyle`로 `layout.sectionGap`(`spacing.xl` 24)을 둔다 | `sectionRecipe.gap`·`headerGap`·`copyGap`, `layout.sectionGap` |
|
|
83
|
+
| 순서·정렬 | 머리 행동은 끝(LTR 오른쪽)에 하나, 글자 묶음과 세로 가운데 정렬. 보통 [Link](link.md) 또는 `tone="link"` [Button](button.md) | `.hjm-section__header`, Native `Section` |
|
|
84
|
+
| 고정·스크롤 | 고정되지 않는다. 화면 본문 스크롤 안에 둔다 | — |
|
|
85
|
+
| 좁은 폭·큰 글자 | 머리 행이 세로로 쌓이고 행동이 글자 아래로 간다. Web은 폭 600 미만(`breakpoint.medium`, media query), Native는 글자 크기 160% 이상 | `.hjm-section__header` @media, `largeTextThreshold` 1.6 |
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
최근 본 장소 [전체 보기] ← 머리: 제목 title 18, gap spacing.sm 12
|
|
89
|
+
자주 가는 곳이에요 ← 설명: caption 11, gap spacing.xxs 4
|
|
90
|
+
gap spacing.xs 8
|
|
91
|
+
┌──────────── 본문 ────────────┐
|
|
92
|
+
└──────────────────────────────┘
|
|
93
|
+
gap layout.sectionGap 24 (Stack이 줌)
|
|
94
|
+
다음 Section …
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 꼭 지킬 것
|
|
98
|
+
|
|
99
|
+
- 제목·설명은 i18n 문구로 넣는다. Native는 `string`만 받는다.
|
|
100
|
+
- 배치는 `layoutStyle`로 한다. Native slot 배치는 `headerStyle`·`copyStyle`·`actionStyle`·`contentStyle`(모두 배치 전용 타입)이다.
|
|
101
|
+
Native `style`은 deprecated(개발 모드 경고, 다음 major 제거)다.
|
|
102
|
+
- 머리 행동은 하나로 둔다. 여러 행동은 본문 안이나 [Menu](menu.md)로 옮긴다.
|
|
103
|
+
|
|
104
|
+
## 플랫폼 차이
|
|
105
|
+
|
|
106
|
+
| 항목 | Web | Native |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `title`·`description` 타입 | `ReactNode` | `string` |
|
|
109
|
+
| heading 수준 | `headingLevel` | 없음(header role 고정) |
|
|
110
|
+
| 루트 | `<section>` | `View` |
|
|
111
|
+
| 배치 | `className`, `layoutStyle`(HTML `style`도 받음) | `layoutStyle`, slot `*Style`(`style`은 deprecated) |
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SegmentedControl
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [ToggleGroup 계약](../../toggle-group.md)(경계), `src/component-recipes.ts`(`segmentedControlRecipe`). 2026-10-06 사용자 승인으로 실험 `실험/컴포넌트/입력/카테고리 필터`를 `알약 모양`·`비활성` 스토리로 합쳐 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/버튼형 선택`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
2~4개의 짧은 보기 중 **항상 하나가 선택된** 전환에 쓴다. 같은 화면의 목록·기간·보기 방식을
|
|
14
|
+
바꾸는 필터(오늘/이번 주/이번 달, 목록/지도)가 전형이다. 선택하면 곧바로 화면이 바뀐다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 선택 없음이 가능하거나, 항목마다 설명이 필요한 폼 질문 | [RadioGroup](radio-group.md) |
|
|
21
|
+
| 선택지가 많거나 라벨이 길다 | [Select](select.md) |
|
|
22
|
+
| 여러 개를 동시에 켠다 | [ToggleGroup](toggle-group.md) |
|
|
23
|
+
| 서로 다른 콘텐츠 패널 사이를 이동 | [Tabs](tabs.md) |
|
|
24
|
+
| 단일 행동 | [Button](button.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `SegmentedControl` | 기본 | `@hjmds/react`, `/selection` | `@hjmds/react-native`, `/inputs` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { SegmentedControl } from "@hjmds/react/selection";
|
|
37
|
+
|
|
38
|
+
<SegmentedControl
|
|
39
|
+
label={t("stats.range.label")}
|
|
40
|
+
items={[
|
|
41
|
+
{ value: "week", label: t("stats.range.week") },
|
|
42
|
+
{ value: "month", label: t("stats.range.month") },
|
|
43
|
+
]}
|
|
44
|
+
value={range}
|
|
45
|
+
onValueChange={setRange}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Native
|
|
51
|
+
import { SegmentedControl } from "@hjmds/react-native/inputs";
|
|
52
|
+
|
|
53
|
+
<SegmentedControl
|
|
54
|
+
label={t("stats.range.label")}
|
|
55
|
+
items={[
|
|
56
|
+
{ value: "week", label: t("stats.range.week") },
|
|
57
|
+
{ value: "month", label: t("stats.range.month") },
|
|
58
|
+
]}
|
|
59
|
+
value={range}
|
|
60
|
+
onValueChange={setRange}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 축과 기본값
|
|
65
|
+
|
|
66
|
+
| prop | 값 | 기본값 | 설명 |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `items` | Web `readonly { value: string; label: ReactNode; disabled? }[]` · Native `readonly { value: Value; label: string; disabled?; leading?; renderLeading? }[]` | 필수 | 값이 비거나 중복이면 던진다 |
|
|
69
|
+
| `presentation` | `connected` · `pills` | `connected` | 연결된 보기 전환 / 독립된 카테고리 필터. `pills`는 미게시(1.12.1 이후) |
|
|
70
|
+
| `size` | `small` · `medium` | `medium` | 항목 최소 높이 36(+hitSlop 4) · 44 |
|
|
71
|
+
| `value` · `defaultValue` | 항목 값 | 첫 번째 활성 항목 | 선택 해제 상태는 없다 |
|
|
72
|
+
| `onValueChange` | Web `(value: string) => void` · Native `(value: Value) => void` | — | 다른 항목을 고를 때. `null`은 오지 않는다 |
|
|
73
|
+
| `label` | `string` | 필수 | 화면에 보이지 않는 접근성 이름. 보이는 제목은 바깥에 둔다 |
|
|
74
|
+
| `disabled` | `boolean` | `false` | 전체 비활성(Web은 fieldset 속성) |
|
|
75
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 바깥 배치 |
|
|
76
|
+
|
|
77
|
+
Native는 글자 크기 160% 이상(`stackAtFontScale`)에서 항목을 세로로 쌓는다.
|
|
78
|
+
|
|
79
|
+
## 배치
|
|
80
|
+
|
|
81
|
+
| 항목 | 값 | 근거 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| 크기 | 항목 최소 높이 `medium` 44 · `small` 36 + hitSlop 4. 항목 좌우 여백 `medium` `spacing.md` 16 · `small` `spacing.sm` 12(두 플랫폼. Web `small`은 `::after`로 위아래 4를 넓힌다) | `segmentedControlRecipe.sizes`, `.hjm-segmented__item` |
|
|
84
|
+
| 간격 | 트랙 안쪽 여백·항목 간격 `container.padding`·`gap` `spacing.xxs` 4. 트랙 모서리 `radius.lg` 16(테두리 1). 선택 항목 모서리 `radius.md` 12, 선택 테두리 2(`stroke.strong`). 아래 내용과 `layout.contentGap` 16 | `segmentedControlRecipe.container`·`item`, `.hjm-segmented__items` |
|
|
85
|
+
| 순서·정렬 | 항목은 같은 폭으로 나뉘고(Native `flex: 1`, Web `flex: 1 1 0`) 트랙은 부모 폭을 채운다. 전환할 내용 바로 위, 화면 좌우 여백(`layout.pagePadding`) 안 | Native `SegmentedControl`, `.hjm-segmented__item` |
|
|
86
|
+
| 고정·스크롤 | 고정되지 않는다. Web 항목은 글자보다 좁아지지 않아(`min-inline-size: max-content`) 넘치면 트랙이 가로 스크롤된다. Native는 스크롤하지 않는다 | `.hjm-segmented__items`(overflow-x) |
|
|
87
|
+
| 좁은 폭·큰 글자 | 항목을 세로로 쌓는다. Native는 글자 크기 160% 이상, Web은 provider `data-large-text` 또는 폭 11em 이하 | `segmentedControlRecipe.adaptive`, `largeTextThreshold` 1.6, `.hjm-segmented` @media |
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
┌───────────────────────────────┐ 트랙: radius.lg 16, padding 4 (Native)
|
|
91
|
+
│ ┌─────────┐ │
|
|
92
|
+
│ │ 주간 │ 월간 연간 │ 44 (medium), 같은 폭
|
|
93
|
+
│ └─────────┘ │ 선택 = focus 색 테두리
|
|
94
|
+
└───────────────────────────────┘
|
|
95
|
+
gap layout.contentGap 16
|
|
96
|
+
[ 전환되는 내용 ]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 꼭 지킬 것
|
|
100
|
+
|
|
101
|
+
- 라벨은 짧은 i18n 문구로 둔다. 한글 한 글자씩 줄바꿈될 만큼 길면 Select나 RadioGroup으로 바꾼다.
|
|
102
|
+
- 활성 항목이 하나도 없거나 `value`가 비거나 중복이면 렌더 중 오류를 던진다. Native는 controlled
|
|
103
|
+
`value`가 비활성 항목이어도 `RangeError`다.
|
|
104
|
+
- 옛 Native `options` prop은 제거됐다. 넘기면 `TypeError`다([이관표](../../migration-native-legacy-removal.md)).
|
|
105
|
+
- 연결형의 선택 표시는 focus 색 테두리(ring), `pills`는 본문색 채움과 반전 글자다. 제품에서 선택 배경을 직접 덮지 않는다. 배치는 `layoutStyle`로 하고, Native `style`은
|
|
106
|
+
deprecated(개발 모드 경고, 다음 major 제거)다.
|
|
107
|
+
|
|
108
|
+
## 플랫폼 차이
|
|
109
|
+
|
|
110
|
+
| 항목 | Web | Native |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| 항목 `label` 타입 | `ReactNode` | `string` |
|
|
113
|
+
| 항목 아이콘 | 없음 | `leading`, `renderLeading({ selected, disabled, color, size })` |
|
|
114
|
+
| 전체 비활성 | fieldset `disabled` 속성 | `disabled` |
|
|
115
|
+
| `name` | 있음(기본 자동 생성) | 없음 |
|
|
116
|
+
| 키보드 | 네이티브 radio 화살표 이동 | 해당 없음 |
|
|
117
|
+
|
|
118
|
+
### 카테고리 필터 표현(알약 모양)
|
|
119
|
+
|
|
120
|
+
2026-10-06 사용자가 여러 화면의 전체/장소/일상 선택을 함께 개편하도록 요청했다.
|
|
121
|
+
[Material의 필터 칩](https://developer.android.com/develop/ui/compose/components/chip)처럼 콘텐츠 범위를 선택하는 작은 표면에서 착안했다.
|
|
122
|
+
기존 단일 선택 계약을 재사용하고 버튼마다 별도 selected 상태를 조립하는 대안은 제외했다.
|
|
123
|
+
|
|
124
|
+
- `presentation="pills"`: 카테고리·작성자·기간처럼 같은 목록의 범위를 좁힐 때 쓴다.
|
|
125
|
+
- 터치 영역은 크기 옵션과 관계없이 최소 `control.minTouchTarget` 44, 안쪽 표면은 위아래 `spacing.xs` 8만큼 들어간다. 눈에 보이는 모양을 작게 해도 터치 영역을 줄이지 않는다.
|
|
126
|
+
- 좌우 `spacing.md` 16, 항목 사이 `spacing.xs` 8, 모서리 `radius.full`. 비선택은 `surfaceAlt`, 선택은 `content.body`/`canvas` 반전이다. 색은 제품 provider에서 따라온다.
|
|
127
|
+
- 항목은 내용 폭이며 좁아지면 줄바꿈한다. 2배 글자는 기존 접근성 규칙대로 세로로 쌓으며, 라벨 높이만큼 항목도 늘어난다.
|
|
128
|
+
- 그룹 이름은 `label`, 하나의 선택은 `value`/`onValueChange`. 복수 조건은 ToggleGroup, 화면 이동은 Tabs/Navigation을 쓴다.
|
|
129
|
+
- 예제의 전체 선택값도 실제 항목으로 넣는다. 선택된 항목을 다시 눌러도 선택을 해제하지 않는다.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { SegmentedControl } from "@hjmds/react/selection";
|
|
133
|
+
<SegmentedControl label={t("category.label")} presentation="pills"
|
|
134
|
+
items={categories} value={category} onValueChange={setCategory} />
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Native는 `@hjmds/react-native/inputs`에서 같은 prop을 사용한다. `presentation`은 게시 버전 1.12.1에 없다(미게시, 1.12.1 이후). 게시·소비 앱 반영 전에는 연결형을 쓴다.
|
|
138
|
+
스토리는 `버튼형 선택`의 `알약 모양`·`비활성`이다. 2026-10-06 사용자 승인으로 실험 `입력/카테고리 필터` 항목을 배포하면서 별도 공개 API가 아니라 이 표현이라 그 스토리로 합쳤다([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). Storybook 배포는 npm 게시가 아니다.
|