@hjmds/design-contracts 1.12.1 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +26 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +31 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +20 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +378 -0
- package/docs/sheet.md +21 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +13 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +107 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +221 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +146 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +157 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +279 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# ColorPicker
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [ColorPicker — Web sRGB 색상 입력](../../color-picker.md), recipe `colorPickerRecipe`(`src/color-picker.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/색상 선택기`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
사용자가 콘텐츠 색(라벨 색, 태그 색, 테마 편집기의 사용자 값 등)을 sRGB HEX로 고르는 폼 입력에 쓴다.
|
|
14
|
+
브라우저 색상 선택기, HEX 텍스트 입력, 선택적 불투명도 슬라이더, 프리셋 견본을 한 fieldset으로 묶는다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 정해진 몇 가지 색 중 하나만 고름 | [RadioGroup](radio-group.md), [SegmentedControl](segmented-control.md) |
|
|
21
|
+
| 숫자 하나를 범위에서 고름 | [Slider](slider.md) |
|
|
22
|
+
| 제품 브랜드·테마 색을 화면마다 바꾸기 | 컴포넌트가 아니다. 제품 테마 토큰(`brandPalette`)으로 한다 |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `ColorPicker` | 기본 | `/color-picker` | 없음 |
|
|
29
|
+
| `ColorPickerLabels`(타입) | 보조(문구 묶음) | `/color-picker` | 없음 |
|
|
30
|
+
|
|
31
|
+
루트 barrel에는 없다. React Native 구현은 없다.
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { ColorPicker } from "@hjmds/react/color-picker";
|
|
38
|
+
|
|
39
|
+
<ColorPicker
|
|
40
|
+
label={t("label.color")}
|
|
41
|
+
labels={{
|
|
42
|
+
color: t("color.choose"), hex: t("color.hex"),
|
|
43
|
+
opacity: t("color.opacity"), invalid: t("color.invalid"),
|
|
44
|
+
}}
|
|
45
|
+
value={color}
|
|
46
|
+
onValueChange={setColor}
|
|
47
|
+
presets={["#b94627", "#338844"]}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Native: 없음. 제품의 색 선택 화면이나 플랫폼 피커를 쓴다.
|
|
52
|
+
|
|
53
|
+
## 축과 기본값
|
|
54
|
+
|
|
55
|
+
| prop | 값 | 기본값 | 설명 |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| `value` + `onValueChange` | 소문자 `#rrggbb`(`alpha`면 `#rrggbbaa`) + `(value: string) => void` | 필수(controlled 전용) | 정규화한 값이 바뀔 때만 부른다 |
|
|
58
|
+
| `label` | `string` | 필수 | fieldset legend |
|
|
59
|
+
| `labels` | `{ color: string; hex: string; opacity: string; invalid: string }` | 필수 | 견본 입력·HEX 입력·불투명도·오류 문구 |
|
|
60
|
+
| `alpha` | `boolean` | `false` | 켜면 0–100% 불투명도 슬라이더가 생긴다. 3·6자리 HEX를 치면 현재 불투명도를 유지한다 |
|
|
61
|
+
| `disabled` | `boolean` | `false` | fieldset으로 모든 입력과 프리셋을 막는다 |
|
|
62
|
+
| `presets` | `readonly string[]`(HEX) | `[]` | 정규화 후 중복을 없앤다. 선택된 견본은 `aria-pressed` |
|
|
63
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 `fieldset` 배치 |
|
|
64
|
+
| HEX 입력 | — | — | Enter·blur에 확정, Escape는 마지막 `value`로 되돌린다. 잘못된 입력은 `labels.invalid`를 alert로 보이고 외부 값은 바꾸지 않는다 |
|
|
65
|
+
|
|
66
|
+
## 배치
|
|
67
|
+
|
|
68
|
+
| 항목 | 값 | 근거 |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| 크기 | 부모 폭을 채우는 fieldset(테두리 1px, `radius.md` 12). 입력·프리셋 버튼 최소 높이 44(`minTargetSize`), 색 견본 입력 폭 3rem(48), HEX 입력 기준 폭 10rem(160)에서 늘어난다. 미리보기 띠 높이 1.5rem(24) | `colorPickerRecipe.minTargetSize`, `.hjm-color-picker*` |
|
|
71
|
+
| 간격 | 안쪽 1rem(16, `spacing.md`와 같은 값), 색 견본↔HEX 0.75rem(12), 불투명도·미리보기·프리셋 위 0.75rem(12), 프리셋 사이 0.5rem(8). CSS가 spacing 변수 대신 rem 값을 쓴다 | `.hjm-color-picker*` |
|
|
72
|
+
| 순서·정렬 | 위→아래 [legend] → [색 견본 입력][HEX 입력] 한 줄 → [오류(alert)] → [불투명도(`alpha`일 때)] → [미리보기] → [프리셋 줄]. 설정 폼 안에 블록으로 둔다 | `react/src/color-picker.tsx` |
|
|
73
|
+
| 고정·스크롤 | 고정 영역이 없다 | — |
|
|
74
|
+
| 좁은 폭·큰 글자 | 견본·HEX 줄과 프리셋 줄이 줄바꿈된다(`flex-wrap: wrap`). 문구는 `overflow-wrap: anywhere` | `.hjm-color-picker__row`, `.hjm-color-picker__presets` |
|
|
75
|
+
|
|
76
|
+
## 꼭 지킬 것
|
|
77
|
+
|
|
78
|
+
- `label`과 `labels`의 네 문구는 모두 비어 있지 않아야 한다. 하나라도 비면 렌더 중 `TypeError`가 난다.
|
|
79
|
+
- `value`와 `presets`는 유효한 HEX여야 한다. `alpha={false}`인데 불투명하지 않은 8자리 값을 주면 투명도를 버리지 않고
|
|
80
|
+
`TypeError`를 던진다. 저장된 값을 넘기기 전에 제품 어댑터에서 형식을 맞춘다.
|
|
81
|
+
- 고른 색을 본문·배경에 쓸 때의 대비 검사는 제품이 한다. 견본 색은 콘텐츠 데이터이며 HJM 토큰이 아니다.
|
|
82
|
+
- 배치는 `layoutStyle`(바깥 `fieldset`)로 한다. `className`·`style` prop은 없다.
|
|
83
|
+
|
|
84
|
+
## 함정
|
|
85
|
+
|
|
86
|
+
- 브라우저 색상 팝업은 RGB만 고른다. 팝업 모양과 동작은 OS·브라우저 소유이며 HJM 검증 대상이 아니다.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: recipe `comboboxRecipe`(`src/component-recipes.ts`), behavior `combobox`
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/검색형 선택`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
주어진 목록에서 하나를 고르는데 목록이 길어 입력으로 좁혀야 할 때 쓴다. 나라·도시·카테고리 고르기가
|
|
14
|
+
여기에 속한다. 입력창이 곧 검색어이고, 확정된 값은 목록 항목 하나다. Native는 서버 검색 결과
|
|
15
|
+
(`filtering="external"`)도 받는다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 항목이 짧아 입력 없이 고름 | [Select](select.md) |
|
|
22
|
+
| 목록 밖 값을 만들거나 여러 개를 고름 | [TagsInput](tags-input.md) ([경계](../../tags-input.md)) |
|
|
23
|
+
| 결과가 화면 전체를 차지하는 검색 | [SearchField](search-field.md) |
|
|
24
|
+
| 명령 실행 팔레트(Web) | [CommandPalette](command-palette.md) |
|
|
25
|
+
| 본문 중간의 `@` 언급 | [Mentions](mentions.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `Combobox` | 기본 | `@hjmds/react`, `/forms` | `@hjmds/react-native`, `/forms` |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { Combobox } from "@hjmds/react/forms";
|
|
38
|
+
|
|
39
|
+
<Combobox
|
|
40
|
+
label={t("profile.city")}
|
|
41
|
+
items={cities.map((c) => ({ value: c.id, label: c.name }))}
|
|
42
|
+
value={cityId}
|
|
43
|
+
onValueChange={setCityId}
|
|
44
|
+
emptyMessage={t("profile.city.empty")}
|
|
45
|
+
loadingMessage={t("common.loading")}
|
|
46
|
+
selectionRequiredMessage={t("profile.city.required")}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Native
|
|
52
|
+
import { Combobox } from "@hjmds/react-native/forms";
|
|
53
|
+
|
|
54
|
+
<Combobox
|
|
55
|
+
label={t("profile.city")}
|
|
56
|
+
items={cities.map((c) => ({ id: c.id, label: c.name, textValue: c.name }))}
|
|
57
|
+
selectedKey={cityId}
|
|
58
|
+
onSelectionChange={setCityId}
|
|
59
|
+
emptyMessage={t("profile.city.empty")}
|
|
60
|
+
loadingMessage={t("common.loading")}
|
|
61
|
+
clearLabel={t("common.clear")}
|
|
62
|
+
dismissLabel={t("common.close")}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 축과 기본값
|
|
67
|
+
|
|
68
|
+
| prop | 값 | 기본값 | 설명 |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| Web `items` | `readonly { value: string; label: string; keywords?: readonly string[]; disabled?: boolean }[]` | 필수 | 라벨과 `keywords`로 부분 일치 필터링한다 |
|
|
71
|
+
| Web `value` · `defaultValue` + `onValueChange` | `string` + `(value: string) => void` | `""` | 확정 값. 입력을 고치면 `""`로 지워진다 |
|
|
72
|
+
| Native `items` · `sections` · `source` | `readonly { id: Key; label: string; textValue: string; description?: string; disabled?: boolean }[]` · 구획 목록 · `{ items } \| { sections }` | — | 정확히 하나만 준다 |
|
|
73
|
+
| Native `selectedKey` · `defaultSelectedKey` + `onSelectionChange` | `Key \| null` + `(key: Key \| null) => void` | `null` | 확정 값. 목록 밖 키는 `selectedItem` 스냅샷이 필요하다 |
|
|
74
|
+
| Native `onCommit` · `onCommitAfterDismiss` | `(key: Key \| null, reason: "selection" \| "clear") => void` · `(key: Key, reason: "selection") => void \| Promise<void>` | — | 시트가 닫힌 뒤 할 일은 `onCommitAfterDismiss`에서 한다 |
|
|
75
|
+
| `inputValue` · `defaultInputValue` + `onInputValueChange` | `string` + `(value: string) => void` | 선택된 항목의 라벨 | 입력어를 따로 제어할 수 있다 |
|
|
76
|
+
| `open` · `defaultOpen` + `onOpenChange` | `boolean` + `(open: boolean, reason) => void` | `defaultOpen` `false` | reason: Web `"focus" \| "input" \| "keyboard" \| "selection" \| "escape" \| "blur"`, Native `"trigger" \| "keyboard" \| "selection" \| "escape" \| "outside" \| "blur" \| "programmatic"` |
|
|
77
|
+
| `size` | `medium` · `large` | `medium` | — |
|
|
78
|
+
| `density` | `comfortable` · `compact` | `comfortable` | — |
|
|
79
|
+
| `openOnFocus` | `boolean` | `true` | — |
|
|
80
|
+
| Native `filtering` | `"local"`(`label`·`textValue`) · `"external"` | `"local"` | `external`이면 제품이 `items`를 걸러 넘긴다 |
|
|
81
|
+
| Native `asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error"; message: string }` | — | 결과 시트의 상태 문구. `error`면 `onRetry: () => void`·`retryLabel`로 다시 시도 버튼이 생긴다 |
|
|
82
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 필드 바깥 배치 |
|
|
83
|
+
|
|
84
|
+
## 배치
|
|
85
|
+
|
|
86
|
+
| 항목 | 값 | 근거 |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| 크기 | 입력 높이 `medium` 44(`fieldFrameContract`) · `large` 52(`control.buttonHeight.large`), 폭은 폼 열을 채운다. Web 목록 최대 높이 22.5rem(360, recipe `popover.maxHeight`), 선택지 최소 높이 `compact` 44 · `comfortable` 56(3.5rem). Native 시트 최대 높이 화면의 75% | `selectRecipe.sizes`·`popover`, `.hjm-combobox__listbox`·`__option`, `react-native/src/forms.tsx` |
|
|
89
|
+
| 간격 | 라벨·설명·오류는 [Field](field.md) 규칙(`formSupportContract.gap` `spacing.xs` 8). Web 목록 안쪽 `spacing.xs` 8(`comboboxRecipe.popover.padding` = Select와 같은 표면. 2026-10-06까지 4, 1.12.1 이후 미게시), 선택지 안쪽 위아래 `spacing.xs` 8 · 좌우 `spacing.sm` 12. Native 시트 안쪽 `spacing.md` 16, 아래는 `spacing.md` + 하단 안전 영역, 요소 사이 `spacing.sm` 12 | `.hjm-combobox__option`, `react-native/src/forms.tsx` |
|
|
90
|
+
| 순서·정렬 | 폼 안에서 다른 입력과 같은 열에 둔다. Web은 입력 바로 아래(공간이 없으면 위, `data-placement`)에 목록이 붙는다. Native는 화면 아래에서 시트가 올라오고 [제목·닫기] → [상태 문구 또는 선택지 목록] 순서다 | `react/src/advanced-forms.tsx`(`AnchoredPortal`), `CollectionSheetHeader` |
|
|
91
|
+
| 고정·스크롤 | 목록은 그 안에서 스크롤한다. Web 목록은 `position: fixed`(z-index `layer.dropdown` 400)로 떠서 화면 스크롤과 무관하다. Native 시트는 배경막(`backdrop.modal`)과 함께 모달로 뜬다 | `.hjm-combobox__listbox`, `react-native/src/forms.tsx` |
|
|
92
|
+
| 좁은 폭·큰 글자 | 선택지 문구는 줄바꿈되고(`overflow-wrap: anywhere`) 높이가 늘어난다. 좁은 폭에서도 Web 목록은 입력에 붙는다 | `.hjm-combobox__option` |
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
Web Native
|
|
96
|
+
┌──────────────────────────┐ ┌──────────────────────────┐
|
|
97
|
+
│ 라벨 │ │ 라벨 │
|
|
98
|
+
│ [ 입력어 _____________ ▾ ]│ │ [ 입력어 _____________ ▾ ]│
|
|
99
|
+
│ ┌──────────────────────┐ │ │ ▒▒▒▒ 배경막 ▒▒▒▒▒▒▒▒▒▒▒▒ │
|
|
100
|
+
│ │ 선택지 1 (44/56) │ │ ← fixed │ ┌──────────────────────┐ │
|
|
101
|
+
│ │ 선택지 2 │ │ 스크롤 │ │ 제목 [닫기]│ │
|
|
102
|
+
│ └──────────────────────┘ │ │ │ 선택지 목록(스크롤) │ │ ← 최대 75%
|
|
103
|
+
│ 설명·오류 │ │ │ ░ 하단 안전 영역 ░ │ │
|
|
104
|
+
└──────────────────────────┘ └─┴──────────────────────┴─┘
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 꼭 지킬 것
|
|
108
|
+
|
|
109
|
+
- 상태 문구는 필수이고 i18n 키로 넣는다. Web은 `emptyMessage`·`loadingMessage`·`selectionRequiredMessage`,
|
|
110
|
+
Native는 `emptyMessage`·`loadingMessage`·`clearLabel`·`dismissLabel`. Native는 빈 문자열이면 `TypeError`다.
|
|
111
|
+
- 선택 값은 목록에 있는 항목이어야 한다. Web은 없는 값이나 `disabled` 항목이면 `RangeError`, Native는
|
|
112
|
+
목록에 없는 `selectedKey`에 `selectedItem` 스냅샷이 없으면 `RangeError`다.
|
|
113
|
+
- Native는 `items`·`sections`·`source` 중 정확히 하나만 준다.
|
|
114
|
+
- 입력을 고치면 확정 값이 지워진다(Web은 `""`, Native는 결과 sheet에서 다시 골라야 확정). 확정 값을 서버로
|
|
115
|
+
보낼 때는 입력어가 아니라 `value`/`selectedKey`를 쓴다.
|
|
116
|
+
- 배치는 `layoutStyle`(필드 바깥)로 한다. Web `style`·`className`은 input에, `fieldClassName`은 필드 바깥에 붙는다.
|
|
117
|
+
Native `style`은 deprecated — `layoutStyle` 또는 `density`를 쓴다.
|
|
118
|
+
|
|
119
|
+
## 플랫폼 차이
|
|
120
|
+
|
|
121
|
+
| 항목 | Web | Native |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| 항목 형태 | `{ value, label, keywords?, disabled? }` | `{ id, label, textValue, description?, disabled? }`, `sections` 가능 |
|
|
124
|
+
| 선택 값 | `value`·`onValueChange(string)`, 빈 값은 `""` | `selectedKey`·`onSelectionChange(key \| null)` |
|
|
125
|
+
| 결과 표시 | 입력 아래 listbox(포털, `align`, `portalContainer`) | `Modal` 결과 sheet(`sheetTitle`) |
|
|
126
|
+
| 비동기 결과 | `loading`만 | `asyncState`, `queryValue`/`resultQuery`, `minimumQueryLength`, `onRetry` |
|
|
127
|
+
| 확정 콜백 | `onValueChange` | `onCommit(key, reason)`, sheet가 닫힌 뒤 `onCommitAfterDismiss` |
|
|
128
|
+
| 폼 제출 | `name`이면 hidden input에 값 | 없음 |
|
|
129
|
+
| 항목 leading | 없음 | `renderLeading(item, props)` |
|
|
130
|
+
|
|
131
|
+
## 함정
|
|
132
|
+
|
|
133
|
+
- Native의 `onSelectionChange`·`onCommit`은 결과 `Modal`이 닫히기 전에 불린다. 닫힌 뒤에 해야 하는
|
|
134
|
+
후속 작업은 `onCommitAfterDismiss`로 받는다(Modal이 닫힌 다음 실행된다). 선택 뒤 iOS가 입력에 초점을
|
|
135
|
+
되돌려 키보드·결과가 다시 열리던 문제는 renderer가 막는다(2026-09-30 감사, 소스 주석).
|
|
136
|
+
- Web은 `style`이 보이는 필드가 아니라 안쪽 input에 붙는다. 배치용 margin·width를 `style`로 주면 필드 틀과 어긋나므로
|
|
137
|
+
`layoutStyle`을 쓴다.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# CommandPalette
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [CommandPalette contract](../../command-palette.md), recipe `commandPaletteRecipe`(`src/command-palette.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/오버레이/명령 검색`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
⌘K 스타일로 앱 전체의 **행동**을 검색해 실행하는 모달에 쓴다. 결과는 값이 아니라 행동이며, 실행하면
|
|
14
|
+
팔레트는 항상 닫힌다. 최근 항목·명령·검색 결과를 `sections`로 한 목록에 섞을 수 있다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 검색해서 값을 고르고 필드에 남김 | [Combobox](combobox.md) |
|
|
21
|
+
| 버튼에 붙은 행동 목록 | [Menu](menu.md) |
|
|
22
|
+
| 우클릭·길게 누르기 메뉴 | [ContextMenu](context-menu.md) |
|
|
23
|
+
| 화면 안의 검색 입력 | [SearchField](search-field.md) |
|
|
24
|
+
| 확인·입력이 필요한 모달 | [Dialog](dialog.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `CommandPalette` | 기본 | `@hjmds/react`, `/command-palette` | 없음 |
|
|
31
|
+
|
|
32
|
+
Native renderer는 없다(계약이 Web 전용 모달로 정의한다).
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { CommandPalette } from "@hjmds/react/command-palette";
|
|
39
|
+
|
|
40
|
+
<CommandPalette
|
|
41
|
+
descriptor={{
|
|
42
|
+
accessibilityLabel: t("palette.label"),
|
|
43
|
+
searchPlaceholder: t("palette.placeholder"),
|
|
44
|
+
emptyMessage: t("palette.empty"),
|
|
45
|
+
closeLabel: t("common.close"),
|
|
46
|
+
}}
|
|
47
|
+
source={paletteSource}
|
|
48
|
+
query={query}
|
|
49
|
+
onQueryChange={setQuery}
|
|
50
|
+
onActivate={runCommand}
|
|
51
|
+
onActivateAfterDismiss={(id) => { if (id === "new-post") openComposerDialog(); }}
|
|
52
|
+
open={open}
|
|
53
|
+
onOpenChange={(next) => { setOpen(next); if (!next) setQuery(""); }}
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`paletteSource`는 `{ sections: [{ id: "recent", label: t("palette.recent"), items: recentItems }, …] }`처럼 만들고
|
|
58
|
+
`useMemo`로 고정한다. 기본(`queryState` 없음)은 renderer가 `query`로 항목의 `label`·`textValue`를 부분 일치로 거른다.
|
|
59
|
+
항목은 `{ id, label, textValue, description?, shortcut?, disabled?, tone? }`이다. 항목의 label·description·shortcut과
|
|
60
|
+
섹션 label은 제품 i18n에서 만든다.
|
|
61
|
+
|
|
62
|
+
Native: 없음.
|
|
63
|
+
|
|
64
|
+
## 축과 기본값
|
|
65
|
+
|
|
66
|
+
| prop | 값 | 기본값 | 설명 |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `descriptor` | `{ accessibilityLabel: string; searchPlaceholder: string; emptyMessage?: string; closeLabel?: string }` | 필수 | 앞의 둘은 비면 `TypeError`. `emptyMessage`는 보이는 결과가 없을 때 한 번 알린다. `closeLabel`을 주면 검색 입력 옆에 닫기 버튼이 생긴다(`reason: "close-action"`) |
|
|
69
|
+
| `source` | `{ items }` 또는 `{ sections: { id; label?; accessibilityLabel?; items }[] }` | 필수 | 섹션은 `label`·`accessibilityLabel` 중 하나가 필수. `accessibilityLabel`이 있으면 그룹 이름으로 먼저 쓴다 |
|
|
70
|
+
| `query` + `onQueryChange` | `string` + `(query: string) => void` | 필수 | 검색어는 항상 제품이 들고 있다 |
|
|
71
|
+
| `onActivate` · `onActivateAfterDismiss` | `(itemId: Key, reason: "pointer" \| "keyboard") => void` | `onActivate` 필수 | 실행하면 팔레트가 닫힌다. 다른 오버레이를 여는 명령은 `onActivateAfterDismiss`에서 연다 |
|
|
72
|
+
| `queryState` | `{ filtering?: "local"; asyncState? }` · `{ filtering: "external"; asyncState; queryValue: string; resultQuery?: string }` | 로컬 필터링 | `external`이면 renderer가 거르지 않는다. `resultQuery`가 `queryValue`와 다르면 결과는 보이되 실행되지 않는다(늦게 온 응답 보호) |
|
|
73
|
+
| `queryState.asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error"; message: string }` | `idle` | 목록 위에 한 번 알린다(`error`는 alert, 나머지는 status). `descriptor.emptyMessage`보다 우선한다 |
|
|
74
|
+
| `open` · `defaultOpen` + `onOpenChange` | `boolean` + `(open: boolean, details: { reason }) => void` | `defaultOpen` `false` | reason: `"trigger" \| "close-action" \| "outside" \| "escape" \| "activation" \| "programmatic"` |
|
|
75
|
+
| `trigger` | element | — | 주면 그 요소가 팔레트를 열고(`reason: "trigger"`), 닫힌 뒤 포커스 복귀 대상이 된다 |
|
|
76
|
+
| `dismissPolicy` | `{ dismissible?, outsideDismiss?, escapeDismiss? }`(`boolean`) | 모두 `true` | 실행(`activation`)으로 닫히는 것은 어떤 정책으로도 막을 수 없다 |
|
|
77
|
+
| `renderLeading` | `(itemId: Key) => ReactNode` | — | 행 앞 아이콘을 넣는다 |
|
|
78
|
+
| `className` · `portalContainer` | `string` · `HTMLElement` | — | `layoutStyle`은 없다(위치를 recipe가 고정하는 제외 대상) |
|
|
79
|
+
|
|
80
|
+
## 배치
|
|
81
|
+
|
|
82
|
+
| 항목 | 값 | 근거 |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| 크기 | 폭 `min(100%, 35rem)`(최대 560), 최대 높이 420. 검색 입력 최소 높이 44(`control.fieldHeight`), 항목 최소 높이 44(`control.minTouchTarget`). 모서리 `radius.lg` 16 | `commandPaletteRecipe.content`, `.hjm-command-palette*`, `react/src/command-palette.tsx` |
|
|
85
|
+
| 간격 | 화면 가장자리와 `spacing.md` 16(오버레이 padding), 위에서 10vh 띄운다. 검색 입력 좌우 `spacing.md` 16, 목록 안쪽 `spacing.xxs` 4, 항목 좌우 `spacing.sm` 12 · 아이콘↔문구 `spacing.xs` 8, 섹션 이름 위아래 `spacing.xs` 8 · 좌우 `spacing.sm` 12, 상태 문구 `spacing.md` 16 | `.hjm-command-palette-positioner`, `.hjm-command-palette__*` |
|
|
86
|
+
| 순서·정렬 | 화면 위쪽 가운데. 위→아래 [검색 입력(아래 경계선) · 닫기 버튼(`closeLabel`이 있을 때 끝 쪽)] → [상태 문구] → [섹션 이름 → 항목들]. 항목은 [leading] → [label · description] → [shortcut(끝 쪽)] | `commandPaletteRecipe.slots`, `.hjm-command-palette__copy`·`__shortcut` |
|
|
87
|
+
| 고정·스크롤 | 배경막(`backdrop.modal`)이 화면을 덮는 모달이다. 검색 입력은 위에 고정되고 목록만 스크롤한다(`overscroll-behavior: contain`) | `.hjm-command-palette__search`(`flex: 0 0 auto`), `.hjm-command-palette__viewport` |
|
|
88
|
+
| 좁은 폭·큰 글자 | 폭은 화면 − 32까지 줄어든다. 항목 문구는 줄바꿈된다(`flex-wrap`, `overflow-wrap: anywhere`) | `.hjm-overlay`, `.hjm-command-palette__copy` |
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
┌──────────────────────────────────────┐
|
|
92
|
+
│ ▒▒▒▒▒▒▒▒▒ 배경막 ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │
|
|
93
|
+
│ ↕ 10vh │
|
|
94
|
+
│ ┌──────────────────────────────┐ │
|
|
95
|
+
│ │ 🔍 명령 검색… [×] │ │ ← 검색(고정), × 는 closeLabel
|
|
96
|
+
│ ├──────────────────────────────┤ │
|
|
97
|
+
│ │ 섹션 이름 │ │
|
|
98
|
+
│ │ (아이콘) 명령 이름 설명 ⌘K │ │ ← 항목 44
|
|
99
|
+
│ │ … (스크롤) │ │ ← 최대 높이 420
|
|
100
|
+
│ └──────────────────────────────┘ │
|
|
101
|
+
│ 최대 폭 560 │
|
|
102
|
+
└──────────────────────────────────────┘
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 꼭 지킬 것
|
|
106
|
+
|
|
107
|
+
- `descriptor`의 두 문구는 필수이고 빈 문자열이면 `TypeError`가 난다.
|
|
108
|
+
- 기본은 renderer가 `query`로 `source`를 거른다(`label`·`textValue` 부분 일치, 대소문자 무시). 서버 검색·퍼지 정렬처럼
|
|
109
|
+
제품이 이미 거른 결과는 `queryState={{ filtering: "external", asyncState, queryValue, resultQuery }}`로 넘긴다.
|
|
110
|
+
- 결과 없음 문구는 `descriptor.emptyMessage`로 준다. 없으면 빈 목록에 아무 안내도 없다. 로딩·실패는
|
|
111
|
+
`queryState.asyncState`로 알린다.
|
|
112
|
+
- 마우스 사용자가 닫을 수 있도록 `descriptor.closeLabel`을 준다. 없으면 Escape·바깥 누름·실행으로만 닫힌다.
|
|
113
|
+
- 전역 단축키(⌘K)와 그 범위는 제품이 정하고 바인딩한다. HJM은 키를 듣지 않는다.
|
|
114
|
+
- 다른 오버레이를 여는 명령은 `onActivateAfterDismiss`에서 연다. `onActivate`에서 열면 두 모달이 겹친다.
|
|
115
|
+
- 배치 prop은 `className`뿐이다(`layoutStyle` 제외 대상). 색·크기를 덮지 않는다.
|
|
116
|
+
|
|
117
|
+
## 함정
|
|
118
|
+
|
|
119
|
+
- 현재 renderer는 `query`·`source`·`queryState`의 참조가 바뀔 때마다 활성 행을 첫 활성 항목으로 되돌린다(`useEffect` 의존성).
|
|
120
|
+
`source`나 `queryState`를 JSX 안에서 객체 리터럴로 만들면 렌더마다 참조가 바뀌어 화살표 키·마우스로 옮긴 활성 행이
|
|
121
|
+
바로 첫 행으로 돌아간다. 둘 다 `useMemo`로 고정한다.
|
|
122
|
+
- 닫을 때 `query`를 지우는 것은 제품 몫이다. 비우지 않으면 다음에 열 때 이전 검색어가 남는다.
|
|
123
|
+
- `filtering: "external"`인데 `resultQuery`를 갱신하지 않으면 결과가 보이기만 하고 Enter·클릭이 먹지 않는다(`aria-disabled`).
|
|
124
|
+
- 현재 Web 스토리는 제품 쪽에서 `label.includes(query)`로 직접 거르고 결과가 없을 때 `asyncState` `empty`로 안내하며
|
|
125
|
+
`closeLabel`이 없다. 새 코드는 로컬 필터링 기본값과 `emptyMessage`·`closeLabel`을 쓴다.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# CommentThreadScreen
|
|
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
|
+
하단 작성창을 controlled로 합성한다. 별도 보조 기능(supplemental)이라 `/screen-flows` subpath로만 import 한다.
|
|
15
|
+
서버 정렬·권한·전송·성공 후 초안 정리는 제품 소유다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 1:1·그룹 대화 타임라인 | [ChatScreen](chat-screen.md) + [ChatMessage](chat-message.md) |
|
|
22
|
+
| 작성창만 필요 | [MessageComposer](message-composer.md) |
|
|
23
|
+
| 일반 목록/상세 화면 | [ListDetailScreen](list-detail-screen.md) |
|
|
24
|
+
| 신고·차단 흐름 | [ModerationScreen](moderation-screen.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `CommentThreadScreen` | 화면 조합 | `/screen-flows` | `/screen-flows` |
|
|
31
|
+
| `CommentThreadItem`(타입) | 댓글 한 개의 데이터 | `/screen-flows` | `/screen-flows` |
|
|
32
|
+
|
|
33
|
+
루트 barrel에는 없다. optional native peer를 요구하지 않는다.
|
|
34
|
+
|
|
35
|
+
## 최소 사용 예
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Web
|
|
39
|
+
import { CommentThreadScreen } from "@hjmds/react/screen-flows";
|
|
40
|
+
import { MessageComposer } from "@hjmds/react/screens";
|
|
41
|
+
|
|
42
|
+
<CommentThreadScreen
|
|
43
|
+
title={t("comments.title")}
|
|
44
|
+
items={comments.map(toCommentItem)}
|
|
45
|
+
expandedIds={expanded}
|
|
46
|
+
onExpandedChange={toggleExpanded}
|
|
47
|
+
onLike={like}
|
|
48
|
+
onReply={id => setReplyTo(id)}
|
|
49
|
+
replyLabel={t("comments.reply")}
|
|
50
|
+
repliesLabel={(count, open) => t(open ? "comments.hideReplies" : "comments.showReplies", { count })}
|
|
51
|
+
composer={<MessageComposer value={draft} label={t("comments.input")} sendLabel={t("comments.send")}
|
|
52
|
+
sendIcon={<ArrowUpIcon />} sendPresentation="circle" onValueChange={setDraft} onSend={send} />}
|
|
53
|
+
threadFooter={hasMore ? <LoadMoreButton /> : null}
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// Native — props·콜백 이름은 Web과 같다
|
|
59
|
+
import { CommentThreadScreen } from "@hjmds/react-native/screen-flows";
|
|
60
|
+
import { MessageComposer } from "@hjmds/react-native/screens";
|
|
61
|
+
|
|
62
|
+
<CommentThreadScreen
|
|
63
|
+
title={t("comments.title")}
|
|
64
|
+
items={comments.map(toCommentItem)}
|
|
65
|
+
expandedIds={expanded}
|
|
66
|
+
onExpandedChange={toggleExpanded}
|
|
67
|
+
onLike={like}
|
|
68
|
+
onReply={id => setReplyTo(id)}
|
|
69
|
+
replyLabel={t("comments.reply")}
|
|
70
|
+
repliesLabel={(count, open) => t(open ? "comments.hideReplies" : "comments.showReplies", { count })}
|
|
71
|
+
composer={<MessageComposer value={draft} label={t("comments.input")} sendLabel={t("comments.send")}
|
|
72
|
+
sendIcon={<ArrowUpIcon />} sendPresentation="circle" onValueChange={setDraft} onSend={send} />}
|
|
73
|
+
threadFooter={hasMore ? <LoadMoreButton /> : null}
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 제품이 공급하는 것
|
|
78
|
+
|
|
79
|
+
| prop | 내용 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `title`(필수), `header`·`leading`·`actions`, `state`·`stateAction`, `notice` | `ScreenLayout`과 같다([ScreenLayout](screen-layout.md)) |
|
|
82
|
+
| `items`(필수) | 서버 순서대로 정렬된 `CommentThreadItem[]`. 아래 표 |
|
|
83
|
+
| `expandedIds`·`onExpandedChange(id)` | 답글을 펼친 최상위 id 목록과 토글 |
|
|
84
|
+
| `onLike(id)`·`onReply(id)` | 기본 좋아요 버튼·답글 버튼의 콜백 |
|
|
85
|
+
| `replyLabel`·`repliesLabel(count, expanded)` | 지역화 문구. 복수형·펼침 상태는 제품 i18n이 만든다. 펼침 버튼 이름이 이 문구이므로 `expanded`에 따라 "답글 3개 보기"/"답글 숨기기"처럼 상태가 드러나게 만든다 |
|
|
86
|
+
| `composer` | 작성창(보통 `MessageComposer`의 `replyTo`·`sendIcon`). `ready`·`empty`일 때만 보인다 |
|
|
87
|
+
| `threadFooter` | 더 보기·커서 페이지네이션 |
|
|
88
|
+
|
|
89
|
+
`CommentThreadItem`: `id`, `parentId`(`null`이면 최상위), `author`, `body`(노드), `timeLabel`, `likeCountLabel`,
|
|
90
|
+
`likeIcon`, `likeLabel`은 필수. 선택은 `bodyText`(작성자와 본문을 한 줄 흐름으로), `avatar`, `likeAction`(기본
|
|
91
|
+
좋아요 버튼 교체, `null`이면 생략), `actions`(신고·수정 등), `canReply`(기본: 최상위만 true), `replyDisabled`.
|
|
92
|
+
|
|
93
|
+
## 배치
|
|
94
|
+
|
|
95
|
+
| 항목 | 값 | 근거 |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| 크기 | ScreenLayout 폭(최대 720); 본문 열이 남은 폭을 채우고(`flex: 1`, 최소 폭 0) 오른쪽 끝에 좋아요 `IconButton` 하나; 답글·펼침 버튼은 `Button size="small"` ghost | Web·Native `CommentThreadScreen` |
|
|
98
|
+
| 간격 | 화면 padding `spacing.md` 16; 최상위 댓글 묶음 사이 `spacing.lg` 20; 댓글 행–답글 묶음 `spacing.xs` 8; 답글 묶음 들여쓰기 `sectionGap`(`spacing.xl`) 24, 펼침 버튼·답글 사이 `spacing.md` 16; 행 안 아바타–본문–좋아요 `spacing.sm` 12, 본문 줄 사이 `spacing.xxs` 4, 시각·좋아요 수·답글 버튼 사이 `spacing.sm` 12 | `screen-flows.tsx` `Stack gap`, `screenPatternRecipe.sectionGap` |
|
|
99
|
+
| 순서·정렬 | 최상위 댓글(아바타 → 작성자·본문 → 시각·좋아요 수·답글 → `actions` → 좋아요) → 답글 펼침 버튼 → 펼친 답글 → … → `threadFooter` → composer(footer) | 렌더 순서 |
|
|
100
|
+
| 고정·스크롤 | 헤더·composer 고정, 댓글은 본문 화면 스크롤(`scroll` 기본 `"screen"`); composer는 `ready`·`empty`에서만 | `ScreenLayout`, `screen-flows.tsx` |
|
|
101
|
+
| 좁은 폭·큰 글자 | 시각·좋아요 수·답글 버튼 줄과 작성자 줄은 줄바꿈(`flexWrap: "wrap"`); 답글은 한 단계만 들여써 좁은 폭에서도 본문 폭을 지킨다 | `screen-flows.tsx` |
|
|
102
|
+
|
|
103
|
+
## 꼭 지킬 것
|
|
104
|
+
|
|
105
|
+
- id는 비어 있지 않고 유일해야 하며, 답글의 `parentId`는 `items` 안의 **최상위** 댓글이어야 한다. 아니면 렌더 중
|
|
106
|
+
`TypeError`가 난다. 답글의 답글은 표현하지 않으므로 제품이 최상위로 평탄화한다.
|
|
107
|
+
- 모든 문구·시각·좋아요 수 라벨은 제품 i18n에서 만든다. `likeCountLabel`이 빈 문자열이면 그리지 않는다.
|
|
108
|
+
- 좋아요 저장·길게 누르기·이모지 선택, 답글 권한(`canReply`/`replyDisabled`)은 제품 데이터로 정한다.
|
|
109
|
+
- 기본 `scroll`은 `ScreenLayout`과 같은 `screen`이다. 댓글을 가상화 목록으로 그리면 직접 `scroll="content"`를 준다.
|
|
110
|
+
|
|
111
|
+
## 플랫폼 차이
|
|
112
|
+
|
|
113
|
+
| 항목 | Web | Native |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| 배치 | `layoutStyle`, `className` | `layoutStyle` |
|
|
116
|
+
| 새로고침·키보드 스크롤 | 없음 | `scrollProps`(`refreshControl`, `keyboardDismissMode` 등), `scrollRef` |
|
|
117
|
+
| 답글 펼침 상태 | 펼침 버튼이 `aria-expanded`를 노출한다(미게시(1.12.1 이후)) | 펼침 버튼이 `accessibilityState.expanded`를 노출한다(미게시(1.12.1 이후)) |
|
|
118
|
+
|
|
119
|
+
## 함정
|
|
120
|
+
|
|
121
|
+
- 펼침 상태는 두 플랫폼 모두 버튼의 expanded 상태로 전한다(2026-10-06 Web `aria-expanded` 추가). 그래도 `repliesLabel(count, expanded)`가
|
|
122
|
+
버튼 이름이므로 문구에서도 펼침 여부가 드러나게 만든다.
|
|
123
|
+
|
|
124
|
+
- 작성창 노출 조건이 ChatScreen과 다르다. ChatScreen은 `ready`에서만, CommentThreadScreen은 `ready`·`empty`에서 보인다.
|
|
125
|
+
`loading`·`error`·`restricted`에서는 둘 다 숨는다.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Container
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Container contract](../../container.md), recipe `containerRecipe`(`src/container.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/레이아웃/컨테이너`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
화면 본문의 최대 폭과 좌우(논리 방향) 여백을 맞출 때 쓴다. 글 읽기 화면은 `reading`,
|
|
14
|
+
일반 제품 화면은 `content`, 지도·갤러리처럼 의도적으로 가득 채우는 영역은 `full`을 고른다.
|
|
15
|
+
가운데 정렬은 항상 inline 축 기준이라 RTL에서도 그대로 동작한다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 화면 전체 틀(상단 바·스크롤·하단 행동) | [Layout](layout.md) |
|
|
22
|
+
| 자식 사이 간격·정렬 | [Stack](stack.md) |
|
|
23
|
+
| 반응형 열 배치 | [Grid](grid.md) |
|
|
24
|
+
| 배경·테두리가 있는 영역 | [Surface](surface.md), [Card](card.md) |
|
|
25
|
+
| 비율이 고정된 미디어 틀 | [AspectRatio](aspect-ratio.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `Container` | 기본 | `@hjmds/react`, `/layout` | `@hjmds/react-native`, `/primitives` |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { Container } from "@hjmds/react/layout";
|
|
38
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
39
|
+
|
|
40
|
+
const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
|
|
41
|
+
|
|
42
|
+
<Container size="reading" gutter={gutter}>
|
|
43
|
+
<article>{body}</article>
|
|
44
|
+
</Container>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// Native
|
|
49
|
+
import { ScrollView, useWindowDimensions } from "react-native";
|
|
50
|
+
import { Container } from "@hjmds/react-native/primitives";
|
|
51
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
52
|
+
|
|
53
|
+
const { width } = useWindowDimensions();
|
|
54
|
+
const gutter = resolveWindowClass(width) === "compact" ? "compact" : "regular";
|
|
55
|
+
|
|
56
|
+
<ScrollView>
|
|
57
|
+
<Container size="content" gutter={gutter}>{children}</Container>
|
|
58
|
+
</ScrollView>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Native는 `ScrollView`가 세로 스크롤을, 그 안의 Container가 좌우 여백을 맡는다. `ScrollView`에 좌우 padding을 직접 주지 않는다.
|
|
62
|
+
|
|
63
|
+
## 축과 기본값
|
|
64
|
+
|
|
65
|
+
| prop | 값 | 기본값 | 설명 |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `size` | `"reading"`(최대 720) · `"content"`(최대 1200) · `"full"`(최대 폭 없음) | `"content"` | — |
|
|
68
|
+
| `gutter` | `"none"`(0) · `"compact"`(16) · `"regular"`(20) · `"spacious"`(24) | `"regular"` | 구간 객체(`{ compact: … }`)는 받지 않는다. 폭 600 미만(`resolveWindowClass` `compact`)은 `"compact"`, 그 이상은 `"regular"`를 골라 넘긴다 |
|
|
69
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 |
|
|
70
|
+
| 허용 값 검사 | — | — | 허용하지 않는 값은 `resolveContainerDescriptor`가 `TypeError`로 막는다 |
|
|
71
|
+
|
|
72
|
+
## 배치
|
|
73
|
+
|
|
74
|
+
| 항목 | 값 | 근거 |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| 크기 | 부모 폭을 채우되 최대 폭 `reading` 720(`layout.readingMaxWidth`) · `content` 1200(`layout.contentMaxWidth`) · `full` 제한 없음. 높이는 내용이 정한다 | `containerRecipe.maxWidths` |
|
|
77
|
+
| 간격 | 좌우 gutter `none` 0 · `compact` `spacing.md` 16 · `regular` `spacing.lg` 20 · `spacious` `spacing.xl` 24. 위아래 여백·자식 사이 간격은 없다([Stack](stack.md)이 정한다) | `containerRecipe.gutters` |
|
|
78
|
+
| 순서·정렬 | 페이지 본문·섹션 바깥에서 가로 가운데 정렬(inline 축, RTL에서도 같다). 글 위주 화면은 `reading`, 목록·대시보드는 `content` | `containerRecipe.alignment` |
|
|
79
|
+
| 고정·스크롤 | 고정 영역도 스크롤도 만들지 않는다. 스크롤은 화면이 소유한다 | `.hjm-container` |
|
|
80
|
+
| 좁은 폭·큰 글자 | 최대 폭보다 좁으면 부모 폭을 그대로 쓰고 gutter만 남는다. 좁은 폭에서는 `compact` gutter를 고려한다 | `resolveContainerDescriptor` |
|
|
81
|
+
|
|
82
|
+
## 꼭 지킬 것
|
|
83
|
+
|
|
84
|
+
- 최대 폭과 여백은 `size`·`gutter`로만 고른다. 임의 px `maxWidth`나 좌우 padding을 제품마다
|
|
85
|
+
새로 정하지 않는다(배제 이유는 [계약](../../container.md#배제한-축)).
|
|
86
|
+
- 바깥 배치(margin·flex)는 `layoutStyle`로 한다. `style`로 `maxWidth`·`padding`을 덮으면
|
|
87
|
+
계약 폭이 사라진다(두 renderer 모두 `style`이 recipe 값 뒤에 합쳐진다). Native `style`은 deprecated —
|
|
88
|
+
`layoutStyle` 또는 `size`·`gutter`를 쓴다.
|
|
89
|
+
- Container는 배경·테두리·스크롤을 갖지 않는다. 필요하면 바깥 컴포넌트가 맡는다.
|
|
90
|
+
|
|
91
|
+
## 플랫폼 차이
|
|
92
|
+
|
|
93
|
+
| 항목 | Web | Native |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| 폭 | `max-inline-size` | `maxWidth` + `width: "100%"` |
|
|
96
|
+
| 여백 | `padding-inline` | `paddingHorizontal` |
|
|
97
|
+
| 가운데 정렬 | `hjm-container` 스타일 | `alignSelf: "center"` |
|
|
98
|
+
| root | `<div>`(ref 전달) | `View` |
|