@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,93 @@
|
|
|
1
|
+
# Menubar
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Menubar](../../menubar.md), `src/menubar.ts`(`menubarRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/탐색/메뉴 막대`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
데스크톱 Web 앱 상단에 항상 같은 자리에 있는 가로 메뉴 막대(파일·편집·보기)에 쓴다.
|
|
14
|
+
막대 전체가 tab stop 하나이고, 한 번에 메뉴 하나만 열리며, 열린 상태에서 ←/→로 옆 메뉴로 넘어간다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 버튼 하나에 붙는 action 목록 | [Menu](menu.md) |
|
|
21
|
+
| 포인터 위치에서 여는 메뉴 | [ContextMenu](context-menu.md) |
|
|
22
|
+
| 화면(페이지)을 바꾸는 상단 탐색 | [Tabs](tabs.md), [TopBar](top-bar.md) |
|
|
23
|
+
| 모바일·Native 앱 | 없음. 앱 바는 OS 소유이고 Native renderer가 없다 |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `Menubar` | 기본 | `@hjmds/react`, `/menubar` | — |
|
|
30
|
+
|
|
31
|
+
## 최소 사용 예
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Web
|
|
35
|
+
import { Menubar } from "@hjmds/react/menubar";
|
|
36
|
+
|
|
37
|
+
<Menubar
|
|
38
|
+
descriptor={{
|
|
39
|
+
accessibilityLabel: t("editor.menubar"),
|
|
40
|
+
menus: [
|
|
41
|
+
{ id: "file", label: t("menu.file"), items: [
|
|
42
|
+
{ id: "save", label: t("menu.save"), textValue: t("menu.save"), shortcut: "⌘S" },
|
|
43
|
+
] },
|
|
44
|
+
{ id: "edit", label: t("menu.edit"), items: [
|
|
45
|
+
{ id: "undo", label: t("menu.undo"), textValue: t("menu.undo") },
|
|
46
|
+
] },
|
|
47
|
+
],
|
|
48
|
+
}}
|
|
49
|
+
onAction={(itemId, menuId) => run(menuId, itemId)}
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Native 예는 없다(renderer 없음).
|
|
54
|
+
|
|
55
|
+
## 축과 기본값
|
|
56
|
+
|
|
57
|
+
| prop | 값 | 기본값 | 설명 |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `descriptor` | `{ accessibilityLabel: string; menus: readonly { id, label: string, items, disabled? }[] }` | 필수 | 타입 `MenubarDescriptor`(`@hjmds/design-contracts/components/menubar`) |
|
|
60
|
+
| 항목 | `{ id, label: string, textValue: string, description?, shortcut?, tone?, disabled? }`(`MenuItemDescriptor`) | — | `textValue` 필수. `shortcut`은 표시 문구일 뿐 키를 등록하지 않는다 |
|
|
61
|
+
| `onAction` | `(id: Key, menuId: MenuKey) => void` | 필수 | 항목 id가 먼저, 메뉴 id가 두 번째 |
|
|
62
|
+
| `openMenuId` · `onOpenMenuIdChange` | `MenuKey \| null` · `(id: MenuKey \| null) => void` | — | 제어형 열린 메뉴 |
|
|
63
|
+
| `defaultOpenMenuId` | `MenuKey \| null` | `null`(닫힘) | 비제어형 열린 메뉴 |
|
|
64
|
+
| 메뉴 `disabled` | `boolean` | `false` | 키보드 이동에서 건너뛴다 |
|
|
65
|
+
| `className` · `layoutStyle` | 문자열 · 배치 전용 style 객체 | — | 막대 루트 배치. 색·높이 변수는 덮이지 않는다 |
|
|
66
|
+
|
|
67
|
+
## 배치
|
|
68
|
+
|
|
69
|
+
| 항목 | 값 | 근거 |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| 크기 | 막대 높이 최소 44(`control.minTouchTarget`). 라벨 높이 44 이상, radius `radius.sm` 8. 패널 폭 `13.75rem`~`min(24rem, 90vw)`. 항목 높이 44 이상 | `menubarRecipe`, `.hjm-menubar*` |
|
|
72
|
+
| 간격 | 막대 좌우 `spacing.xs` 8, 라벨 사이 `spacing.xxs` 4, 라벨 좌우 `spacing.sm` 12. 패널 안쪽 4, radius `radius.md` 12. 항목 위아래 8 · 좌우 12, 단축키와 12 | `menubarRecipe`, `.hjm-menubar__panel`·`__item` |
|
|
73
|
+
| 순서·정렬 | 메뉴는 데스크톱 관례(파일 → 편집 → 보기 → … → 도움말). 패널은 해당 라벨 바로 아래 시작 쪽에 붙는다. 단축키 문구는 항목 끝(`space-between`) | `.hjm-menubar__panel` |
|
|
74
|
+
| 고정·스크롤 | 화면 맨 위(앱 제목·TopBar 아래나 같은 줄 시작 쪽). sticky 여부는 놓는 레이아웃이 정한다. 패널은 Menu와 같은 portal·fixed 배치(`layer.dropdown` 400)로 공간이 부족하면 뒤집고 화면 안으로 민다. 모달 안에서는 소유 모달 + 1이다 | `useAnchoredPopup`, `.hjm-menubar__panel` |
|
|
75
|
+
| 좁은 폭·큰 글자 | 폭이 모자라면 라벨이 다음 줄로 감긴다(`flex-wrap`). 모바일 폭은 대상이 아니다 | `.hjm-menubar` |
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
┌ 화면 (Web, 데스크톱) ─────────────────────────────────────┐
|
|
79
|
+
│ [파일] [편집] [보기] ← 막대 ≥44 │
|
|
80
|
+
│ └┬───────────────────┐ │
|
|
81
|
+
│ │ 저장 ⌘S │ ← 패널: 라벨 바로 아래, 시작 정렬 │
|
|
82
|
+
│ │ 다른 이름… ⇧⌘S │ 13.75rem ~ 24rem │
|
|
83
|
+
│ └───────────────────┘ │
|
|
84
|
+
│ 본문(스크롤) │
|
|
85
|
+
└───────────────────────────────────────────────────────────┘
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 꼭 지킬 것
|
|
89
|
+
|
|
90
|
+
- `descriptor.accessibilityLabel`, 각 메뉴 `id`·`label`은 비어 있으면 안 되고, 메뉴마다 항목이 하나 이상 있어야 한다. 어기면 던진다.
|
|
91
|
+
- 항목 `textValue`는 계약 타입상 필수다. 지역화한 문구를 그대로 넣는다.
|
|
92
|
+
- 메뉴 구성과 단축키 문구는 제품 소유다. 단축키 자체의 키 바인딩은 Menubar가 등록하지 않으므로 제품이 따로 연결한다.
|
|
93
|
+
- `className`·`layoutStyle`은 배치에만 쓴다. 색·높이를 덮지 않는다.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# MessageComposer
|
|
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
|
+
채팅·DM·댓글 입력창에 쓴다. 1~5줄로 자라는 TextArea와 전송 버튼, 선택적으로 사진 첨부 미리보기,
|
|
14
|
+
답장 대상 표시, 첨부 버튼을 하나로 조합한다. 값은 완전 제어형이고 HJM은 초안을 지우지 않는다.
|
|
15
|
+
|
|
16
|
+
용도별 조합은 [댓글 작성](../compositions/purpose-input-comment.md)과 [메시지 작성](../compositions/purpose-input-message.md)을 먼저 선택한다. 입력·전송 엔진은 같고 답글·첨부·문구만 목적에 맞게 연결한다.
|
|
17
|
+
|
|
18
|
+
## 쓰지 않을 때
|
|
19
|
+
|
|
20
|
+
| 상황 | 대신 쓸 것 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| 일반 폼의 여러 줄 입력 | [TextArea](text-area.md) |
|
|
23
|
+
| @멘션 자동완성 입력 | [Mentions](mentions.md) |
|
|
24
|
+
| 채팅 화면 전체 틀(타임라인 + 작성창 자리) | [ChatScreen](chat-screen.md)의 `composer` 슬롯에 이 컴포넌트를 넣는다 |
|
|
25
|
+
| 댓글 화면 전체 | [CommentThreadScreen](comment-thread-screen.md)의 `composer` 슬롯 |
|
|
26
|
+
| 키보드 위에 붙이기 | [KeyboardDock](keyboard-dock.md) 등 keyboard 계열과 합성 |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `MessageComposer` | supplemental, 루트 barrel에 없음 | `/screens` | `/screens` |
|
|
33
|
+
|
|
34
|
+
`@hjmds/react/screens`, `@hjmds/react-native/screens`로만 import한다. 추가 optional peer는 없다.
|
|
35
|
+
|
|
36
|
+
## 최소 사용 예
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Web
|
|
40
|
+
import { MessageComposer } from "@hjmds/react/screens";
|
|
41
|
+
|
|
42
|
+
<MessageComposer
|
|
43
|
+
label={t("chat.input")}
|
|
44
|
+
sendLabel={t("chat.send")}
|
|
45
|
+
value={draft}
|
|
46
|
+
onValueChange={setDraft}
|
|
47
|
+
pending={sending}
|
|
48
|
+
onSend={async (text) => { const ok = await send(text, photos); if (ok) { setDraft(""); setPhotos([]); } }}
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Native
|
|
54
|
+
import { MessageComposer } from "@hjmds/react-native/screens";
|
|
55
|
+
import { Image } from "react-native";
|
|
56
|
+
|
|
57
|
+
<MessageComposer
|
|
58
|
+
label={t("chat.input")}
|
|
59
|
+
sendLabel={t("chat.send")}
|
|
60
|
+
value={draft}
|
|
61
|
+
onValueChange={setDraft}
|
|
62
|
+
pending={sending}
|
|
63
|
+
sendIcon={<ArrowUpIcon />}
|
|
64
|
+
sendPresentation="circle"
|
|
65
|
+
attachmentAction={{ label: t("chat.attachPhoto"), icon: <PhotoIcon />, onPress: openPhotoSource }}
|
|
66
|
+
attachments={photos.map((p) => ({ id: p.id, preview: <Image source={{ uri: p.uri }} />, removeLabel: t("chat.removePhoto") }))}
|
|
67
|
+
onRemoveAttachment={removePhoto}
|
|
68
|
+
onSend={sendDraft}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 축과 기본값
|
|
73
|
+
|
|
74
|
+
| prop | 값 | 기본값 | 설명 |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `value` | `string` | 필수 | 완전 제어형. HJM은 초안을 지우지 않는다 |
|
|
77
|
+
| `label` | `string` | 필수 | placeholder 겸 접근성 이름 |
|
|
78
|
+
| `sendLabel` | `string` | 필수 | 전송 버튼 문구·접근성 이름 |
|
|
79
|
+
| `onValueChange` | `(value: string) => void` | 필수 | 입력 변경 |
|
|
80
|
+
| `onSend` | `(value: string) => void` | 필수 | 현재 문자열만 넘긴다 |
|
|
81
|
+
| `disabled` · `pending` | `boolean` | `false` | 둘 중 하나면 입력·첨부·삭제·답장 취소가 모두 잠긴다. `pending`은 전송 버튼 loading |
|
|
82
|
+
| `sendDisabled` | `boolean` | `false` | 전송만 막고 편집은 열어 둔다 |
|
|
83
|
+
| `additionalContent` | `boolean` | `false` | 글·첨부 밖에 보낼 내용이 있으면 `true`. 전송 가능 판정에 더한다 |
|
|
84
|
+
| `maxLength` | `number` | 없음 | 입력 최대 글자 수 |
|
|
85
|
+
| `leadingAction` | `ReactNode` | 없음 | 입력 테두리 **안** 글자 앞 시작 쪽 도구 버튼. 자라는 입력의 세로 가운데에 맞춘다(Web `.hjm-field__leading` `align-self: center`, Native `alignSelf: "center"`). 행동이라 흐린 affix 색을 쓰지 않는다 |
|
|
86
|
+
| `inputRef` | Web `Ref<HTMLTextAreaElement>` · Native `Ref<TextInput>` | 없음 | 답글 선택 뒤 포커스 유지 |
|
|
87
|
+
| `context` | `ReactNode` | 없음 | 답장 대상 위 임의 슬롯 |
|
|
88
|
+
| `replyTo` | `{ author, excerpt, cancelLabel, onCancel() }` | 없음 | 입력창 위 답장 대상과 취소 버튼 |
|
|
89
|
+
| `sendIcon` | `ReactNode` | 없음 | 주면 아이콘 모드: 빈 입력에서는 입력창 안에 `attachmentAction`, 내용이 있거나 `pending`이면 전송 아이콘. 없으면 입력창 옆 텍스트 `Button` |
|
|
90
|
+
| `sendPresentation` | `"inline"` · `"circle"` | `"inline"` | `circle`은 primary 원형 `IconButton size="small"`(댓글·DM 레퍼런스). `sendIcon`이 있을 때만 의미가 있다 |
|
|
91
|
+
| `attachmentAction` | `{ label, icon, onPress(), disabled? }` | 없음 | 첨부 버튼(ghost `IconButton`) |
|
|
92
|
+
| `attachments` | `readonly { id, removeLabel, preview }[]` | `[]` | 첨부 미리보기. 있으면 `onRemoveAttachment` 필수 |
|
|
93
|
+
| `onRemoveAttachment` | `(id: string) => void` | 없음 | 첨부 제거 |
|
|
94
|
+
| Web `layoutStyle` | `HjmCompositionStyleProp` | 없음 | 작성창 루트 배치(margin·width·flex 등). 미게시(1.12.1 이후) |
|
|
95
|
+
|
|
96
|
+
전송 가능 조건: `disabled`·`pending`·`sendDisabled`가 아니고, 글(공백 제외)·첨부·`additionalContent` 중 하나가 있을 때(`canSubmitMessage`).
|
|
97
|
+
|
|
98
|
+
## 배치
|
|
99
|
+
|
|
100
|
+
| 항목 | 값 | 근거 |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| 크기 | 입력 `composerMinLines` 1 ~ `composerMaxLines` 5줄(넘으면 입력 안에서 스크롤), `shape="large"`; 첨부 썸네일 `attachmentSize` 80(radius `radius.md`); 제거 버튼은 `IconButton size="small"` 36 + 사방 hit 4(실제 타깃 44), 그 안 보이는 원 `attachmentRemoveSize` 24; circle 전송은 `IconButton size="small"` 36 | `screen-patterns.ts`, `icon-button-recipe.ts` `sizes.small`, Web `styles.css`, Native `MessageComposer` |
|
|
103
|
+
| 간격 | 세로 묶음(context·답장 대상·첨부·입력 줄) 사이와 입력–텍스트 전송 버튼 사이 `screenPatternRecipe.itemGap`(`spacing.sm` 12, 두 플랫폼. 2026-10-06까지 Web은 8, 1.12.1 이후 미게시); 답장 대상 줄·첨부 사이 `spacing.sm` 12; circle 전송은 입력 글자와 `spacing.sm` 12, 오른쪽 끝 8 | `screenPatternRecipe.itemGap`, Web `.hjm-message-composer`·`__row`(`--hjm-message-composer-gap`), Native `Stack gap="sm"`·`itemGap` |
|
|
104
|
+
| 순서·정렬 | `context` → 답장 대상(작성자·발췌 → 취소) → 첨부 썸네일(→ 첨부 버튼) → 입력·전송 한 줄, 전송은 입력 아래쪽(`flex-end`)에 맞춘다. 입력 줄 안은 `leadingAction` → 글자 → (아이콘 모드) 전송/첨부 | 렌더 순서, Web `forms.tsx` `hjm-field__leading` |
|
|
105
|
+
| 고정·스크롤 | 자체 고정 없음: 화면 composer 슬롯(footer)에 넣는다; 첨부는 가로 스크롤; 키보드 adapter는 host 하나만 | ChatScreen·CommentThreadScreen `composer` |
|
|
106
|
+
| 좁은 폭·큰 글자 | 입력이 남은 폭을 채우고(`flex: 1`, 최소 폭 0) 전송 버튼은 줄지 않는다; 큰 글자에서도 5줄 상한 뒤 입력 안 스크롤로 기록을 가리지 않는다 | Web `.hjm-message-composer__row`, Native `layoutStyle={{ flex: 1 }}` |
|
|
107
|
+
|
|
108
|
+
## 꼭 지킬 것
|
|
109
|
+
|
|
110
|
+
- 문구(`label`은 placeholder 겸 접근성 이름, `sendLabel`, `removeLabel`, `cancelLabel`)는 모두 i18n 키로 넣는다.
|
|
111
|
+
- `onSend(value)`는 현재 문자열만 넘긴다. 첨부 목록은 제품 상태에서 읽고, **서버 성공 뒤에만** 제품이 글·첨부·답장 대상을 지운다.
|
|
112
|
+
- 첨부 `id`는 비어 있지 않고 유일해야 하며 `removeLabel`이 필요하다. 첨부가 있으면 `onRemoveAttachment`가 필수다. 어기면 던진다.
|
|
113
|
+
- 사진 권한·선택기·업로드·개수 제한은 제품 소유다. 출처 선택 UI는 [PhotoSourceSheet](photo-source-sheet.md)를 쓴다.
|
|
114
|
+
- Enter는 줄바꿈이다(IME 조합 보호). Enter 전송을 덧붙이지 않는다.
|
|
115
|
+
- Web 배치는 `layoutStyle`로 한다. Native는 스타일 통로가 없어 감싸는 레이아웃에서 배치한다.
|
|
116
|
+
|
|
117
|
+
## 플랫폼 차이
|
|
118
|
+
|
|
119
|
+
| 항목 | Web | Native |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `inputRef` 타입 | `HTMLTextAreaElement` | `TextInput` |
|
|
122
|
+
| 첨부 목록 | CSS 클래스 `hjm-message-composer__attachments` | 가로 ScrollView |
|
|
123
|
+
| 이벤트 이름 | `attachmentAction.onPress`(내부에서 onClick으로 연결) | `onPress` |
|
|
124
|
+
| 배치 prop | `layoutStyle`(루트) | 없음 |
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ModerationScreen
|
|
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
|
+
선택적인 차단 버튼과 그 확인 대화상자를 [ScreenLayout](screen-layout.md) 위에 조합한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 차단·삭제 확인만 필요 | [AlertDialog](alert-dialog.md) |
|
|
21
|
+
| 신고 진입 메뉴(⋯) | [Menu](menu.md) |
|
|
22
|
+
| 자유 입력 위주의 문의 폼 | [Form](form.md), [EditorScreen](editor-screen.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `ModerationScreen` | supplemental, 루트 barrel에 없음 | `/screen-flows` | `/screen-flows` |
|
|
29
|
+
|
|
30
|
+
`@hjmds/react/screen-flows`, `@hjmds/react-native/screen-flows`로만 import한다. 추가 optional peer는 없다.
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { ModerationScreen } from "@hjmds/react/screen-flows";
|
|
37
|
+
|
|
38
|
+
<ModerationScreen
|
|
39
|
+
title={t("report.title")}
|
|
40
|
+
reasonLabel={t("report.reason")}
|
|
41
|
+
reasons={[
|
|
42
|
+
{ value: "spam", label: t("report.reason.spam") },
|
|
43
|
+
{ value: "abuse", label: t("report.reason.abuse") },
|
|
44
|
+
]}
|
|
45
|
+
reason={reason}
|
|
46
|
+
onReasonChange={setReason}
|
|
47
|
+
submit={{ label: t("report.submit"), onAction: submitReport, pending: reporting }}
|
|
48
|
+
block={{
|
|
49
|
+
action: { label: t("report.block"), onAction: () => {} },
|
|
50
|
+
confirmation: {
|
|
51
|
+
mode: "confirm", tone: "danger",
|
|
52
|
+
title: t("block.confirm.title"), description: t("block.confirm.body"),
|
|
53
|
+
confirmLabel: t("block.confirm"), cancelLabel: t("common.cancel"),
|
|
54
|
+
onConfirm: blockUser, fallbackErrorMessage: t("block.error"),
|
|
55
|
+
},
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// Native — props는 Web과 같다
|
|
62
|
+
import { ModerationScreen } from "@hjmds/react-native/screen-flows";
|
|
63
|
+
|
|
64
|
+
<ModerationScreen
|
|
65
|
+
title={t("report.title")}
|
|
66
|
+
reasonLabel={t("report.reason")}
|
|
67
|
+
reasons={[
|
|
68
|
+
{ value: "spam", label: t("report.reason.spam") },
|
|
69
|
+
{ value: "abuse", label: t("report.reason.abuse") },
|
|
70
|
+
]}
|
|
71
|
+
reason={reason}
|
|
72
|
+
onReasonChange={setReason}
|
|
73
|
+
submit={{ label: t("report.submit"), onAction: submitReport, pending: reporting }}
|
|
74
|
+
block={{
|
|
75
|
+
action: { label: t("report.block"), onAction: () => {} },
|
|
76
|
+
confirmation: {
|
|
77
|
+
mode: "confirm", tone: "danger",
|
|
78
|
+
title: t("block.confirm.title"), description: t("block.confirm.body"),
|
|
79
|
+
confirmLabel: t("block.confirm"), cancelLabel: t("common.cancel"),
|
|
80
|
+
onConfirm: blockUser, fallbackErrorMessage: t("block.error"),
|
|
81
|
+
},
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 제품이 공급할 것
|
|
87
|
+
|
|
88
|
+
| 슬롯·prop | 내용 |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `reasons`, `reasonLabel` | 지역화한 사유 목록과 그룹 이름. 기본으로 세로 RadioGroup을 그린다 |
|
|
91
|
+
| `reason`, `onReasonChange` | 선택된 사유(제어형, 없으면 `null`) |
|
|
92
|
+
| `reasonPicker` | 단계형 사유 목록 등 다른 선택 UI. 주면 기본 RadioGroup 대신 그린다 |
|
|
93
|
+
| `children` | 상세 설명 입력 등 추가 내용 |
|
|
94
|
+
| `submit` | 신고 행동 `{ label, onAction, disabled?, pending? }` |
|
|
95
|
+
| `block` | 차단 버튼 행동과 `AlertDialog` 확인 요청(`mode: "confirm"`) |
|
|
96
|
+
| ScreenLayout props | `title`, `description`, `header`, `leading`, `actions`, `notice`, `state`, `stateAction` 등(`footer` 제외) |
|
|
97
|
+
|
|
98
|
+
## 배치
|
|
99
|
+
|
|
100
|
+
| 항목 | 값 | 근거 |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| 크기 | ScreenLayout 폭(최대 720); 사유는 세로 `RadioGroup`, 신고·차단은 `Button` 기본 크기 | `ModerationScreen`, `screen-flows.tsx` `Action` |
|
|
103
|
+
| 간격 | 화면 padding `spacing.md` 16; 본문 사유 선택–`children` `spacing.lg` 20; footer 신고–차단 `spacing.sm` 12 | Web·Native `ModerationScreen` `Stack gap="lg"`·`gap="sm"` |
|
|
104
|
+
| 순서·정렬 | 헤더(제목) → 본문(사유 → 추가 설명 `children`) → footer(신고 primary → 차단 ghost) | 렌더 순서 |
|
|
105
|
+
| 고정·스크롤 | 헤더·footer 고정, 본문 화면 스크롤; 차단 확인은 AlertDialog 오버레이 | `ScreenLayout`, `AlertDialog` |
|
|
106
|
+
| 좁은 폭·큰 글자 | 사유 문구는 줄바꿈되고 항목이 세로로 늘어난다; footer 버튼은 세로로 쌓여 좁은 폭에서도 나란히 줄지 않는다 | `RadioGroup orientation="vertical"`, `Stack` |
|
|
107
|
+
|
|
108
|
+
## 꼭 지킬 것
|
|
109
|
+
|
|
110
|
+
- 신고 버튼은 `reason`이 `reasons` 안의 값이 아니거나 `state`가 `ready`가 아니면 자동으로 막힌다. `reasonPicker`를 쓸 때도 `reasons`에 유효한 값을 넣어야 버튼이 열린다.
|
|
111
|
+
- 차단 버튼의 `block.action.onAction`은 호출되지 않는다. 버튼은 확인 대화상자만 열고, 실제 차단은 `confirmation.onConfirm`에서 한다.
|
|
112
|
+
- 서버 신고·권한·차단 mutation, 완료 후 이동은 제품 소유다. 사유 목록과 정책 문구도 제품이 정한다.
|
|
113
|
+
- 확인 대화상자의 성공 후 닫힘·실패 문구는 [AlertDialog](alert-dialog.md) 계약을 따른다.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Notice
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: `src/component-recipes.ts`(`noticeRecipe`), BottomInfo와의 경계 [BottomInfo](../../bottom-info.md), Toast와의 경계 [Toast](../../toast.md)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/상태와 알림/안내 메시지`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
화면 흐름 안 **제자리에 남아 있는** 상태 알림에 쓴다. 저장 실패, 오프라인, 권한 제한,
|
|
14
|
+
결제 수단 만료처럼 사용자가 읽고 필요하면 행동(`action`)할 때까지 사라지면 안 되는 내용이 여기에 속한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 무시해도 안전하고 잠시 뒤 사라지는 짧은 결과 알림(“저장했어요”) | [Toast](toast.md) |
|
|
21
|
+
| 반드시 응답해야 진행되는 확인 | [AlertDialog](alert-dialog.md) |
|
|
22
|
+
| 주 행동 아래 늘 있는 조건 안내(약관 동의 등) | [BottomInfo](bottom-info.md) |
|
|
23
|
+
| 화면 전체가 빈 상태·오류 상태 | [EmptyState](empty-state.md), [Result](result.md) |
|
|
24
|
+
| 입력 하나의 오류 | [Field](field.md)의 `error` |
|
|
25
|
+
|
|
26
|
+
선택 기준: 사라져도 되는가(Toast) / 남아야 하는가(Notice) / 응답이 필수인가(AlertDialog).
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `Notice` | 기본 | `@hjmds/react`, `/feedback` | `@hjmds/react-native`, `/feedback` |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { Button } from "@hjmds/react/actions";
|
|
39
|
+
import { Notice } from "@hjmds/react/feedback";
|
|
40
|
+
|
|
41
|
+
<Notice
|
|
42
|
+
tone="warning"
|
|
43
|
+
title={t("sync.offline.title")}
|
|
44
|
+
description={t("sync.offline.body")}
|
|
45
|
+
action={<Button tone="secondary" size="small" onClick={retry}>{t("common.retry")}</Button>}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Native
|
|
51
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
52
|
+
import { Notice } from "@hjmds/react-native/feedback";
|
|
53
|
+
|
|
54
|
+
<Notice
|
|
55
|
+
tone="warning"
|
|
56
|
+
announcement="polite"
|
|
57
|
+
title={t("sync.offline.title")}
|
|
58
|
+
description={t("sync.offline.body")}
|
|
59
|
+
action={<Button tone="secondary" size="small" onPress={retry}>{t("common.retry")}</Button>}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 축과 기본값
|
|
64
|
+
|
|
65
|
+
| prop | 값 | 기본값 | 설명 |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `title` | Web `ReactNode` · Native `string` | 필수 | 현지화 |
|
|
68
|
+
| `description` | Web `ReactNode` · Native `string` | — | 현지화 |
|
|
69
|
+
| `tone` | `info` · `success` · `warning` · `attention` · `danger` | `info` | 도메인 상태는 제품 어댑터에서 tone으로 매핑한다 |
|
|
70
|
+
| `action` | 노드 | — | 보조 행동 하나(`Button` `tone="secondary"` `size="small"`) |
|
|
71
|
+
| `icon` | 노드 | — | 장식이다(Web은 `aria-hidden`) |
|
|
72
|
+
| `renderIcon`(Native) | `(props: { tone, color: string, size: number }) => ReactNode` | — | tone 색과 recipe 크기를 받아 그린다 |
|
|
73
|
+
| `announcement`(Native) | `none` · `polite` · `assertive` | `none` | 새로 나타나는 상태만 발표한다(아래 플랫폼 차이) |
|
|
74
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 바깥 여백·폭만 |
|
|
75
|
+
|
|
76
|
+
## 배치
|
|
77
|
+
|
|
78
|
+
| 항목 | 값 | 근거 |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| 크기 | 본문 폭을 꽉 채운다. 아이콘 20(`glyph.sm`), 테두리 1(`stroke.default`), radius `radius.md` 12 | `noticeRecipe` |
|
|
81
|
+
| 간격 | 안쪽 `spacing.md` 16, 아이콘·문구·행동 사이 `spacing.sm` 12, 제목·설명 사이 `spacing.xxs` 4. 이웃 블록과는 `layout.contentGap` 16 | `noticeRecipe`, `.hjm-notice*` |
|
|
82
|
+
| 순서·정렬 | 아이콘 — 제목·설명 — 행동. 행동은 하나, `Button` `size="small"` `tone="secondary"`(또는 `ghost`), `primary` 금지. Web은 행동이 같은 줄 끝, Native는 행동이 문구 줄 **아래** | `.hjm-notice__action`, `react-native/src/feedback.tsx` |
|
|
83
|
+
| 고정·스크롤 | 본문 흐름 안. 먼저 읽어야 하는 경고·입력 오류는 영향을 받는 내용 바로 위(화면 상태는 제목 아래, 폼 검증은 제출 버튼 위). 제출 후 성공·실패 결과는 구성 지침이 지정한 행동 인접 위치에 둔다. 떠 있거나 고정되지 않고 함께 스크롤되며 여러 개 쌓지 않는다 | — |
|
|
84
|
+
| 좁은 폭·큰 글자 | Web은 문구 칸이 60% 아래로 줄면 행동이 다음 줄로 감긴다. 제목·설명은 줄바꿈된다 | `.hjm-notice__content`(`flex: 1 1 60%`) |
|
|
85
|
+
|
|
86
|
+
## 꼭 지킬 것
|
|
87
|
+
|
|
88
|
+
- 제목·설명·행동 문구는 i18n 키로 넣는다. 아이콘은 제품 소유 자산이고 색은 tone이 정한다.
|
|
89
|
+
- 배치는 `layoutStyle`로만 한다. 색·padding·radius를 `style`/`className`으로 덮지 않는다. Native `style`은 deprecated(개발 모드 경고, 다음 major 제거)다.
|
|
90
|
+
- 같은 화면에 같은 내용의 Notice와 Toast를 동시에 띄우지 않는다.
|
|
91
|
+
|
|
92
|
+
## 플랫폼 차이
|
|
93
|
+
|
|
94
|
+
| 항목 | Web | Native |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| 발표 | 항상 live region. `danger`는 `role="alert"`+assertive, 나머지는 `role="status"`+polite | `announcement`: `none`(기본) · `polite` · `assertive`. 지정해야 발표한다 |
|
|
97
|
+
| title/description 타입 | `ReactNode` | `string` |
|
|
98
|
+
| 아이콘 렌더 함수 | 없음 | `renderIcon` |
|
|
99
|
+
| 배치 | `className`, `layoutStyle`(HTML `style`도 받음) | `layoutStyle`(`style`은 deprecated) |
|
|
100
|
+
|
|
101
|
+
## 함정
|
|
102
|
+
|
|
103
|
+
- Native는 기본이 `announcement="none"`이라, 새로 생긴 오류 Notice를 화면에 넣기만 하면 스크린 리더가 알리지 않는다. 새로 나타나는 상태에는 `polite`/`assertive`를 준다.
|
|
104
|
+
- Web은 반대로 항상 발표되므로, 화면 진입 때부터 늘 있는 안내를 Notice로 두면 매번 읽힌다. 그런 조건 안내는 BottomInfo다.
|
|
105
|
+
|
|
106
|
+
- 2026-10-06 독립 재구현에서 입력 전 경고 위치와 시간 선택의 확정 후 결과 위치가 충돌했다. 시간 선택의 결과는 행동 아래, 입력 시트의 저장 실패는 본문 입력 위로 각 구성에 명시한다. Notice 자체가 결과 위치를 강제하지 않는다.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# NotificationInboxScreen
|
|
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
|
+
알림함 화면 전체 틀에 쓴다. [ScreenLayout](screen-layout.md) 위에 필터 슬롯을 `notice` 영역에 고정해,
|
|
14
|
+
로딩·빈 상태·오류로 본문이 바뀌어도 필터는 남게 한다. 본문 행은 [NotificationItem](notification-item.md)을 쓴다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 알림 한 행 | [NotificationItem](notification-item.md) |
|
|
21
|
+
| 상단 바의 알림 진입 아이콘·개수 | `NotificationBell`([IconButton](icon-button.md) 확장, `/notification-bell`) |
|
|
22
|
+
| 필터 없는 일반 목록 화면 | [ScreenLayout](screen-layout.md), [ListDetailScreen](list-detail-screen.md) |
|
|
23
|
+
| 일시적인 결과 알림 | [Toast](toast.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `NotificationInboxScreen` | supplemental, 루트 barrel에 없음 | `/screens` | `/screens` |
|
|
30
|
+
|
|
31
|
+
`@hjmds/react/screens`, `@hjmds/react-native/screens`로만 import한다. 추가 optional peer는 없다.
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { NotificationInboxScreen, NotificationItem } from "@hjmds/react/screens";
|
|
38
|
+
|
|
39
|
+
<NotificationInboxScreen
|
|
40
|
+
title={t("inbox.title")}
|
|
41
|
+
filters={filterControl}
|
|
42
|
+
state={items.length ? { kind: "ready" } : { kind: "empty", title: t("inbox.empty") }}
|
|
43
|
+
>
|
|
44
|
+
{items.map((n) => <NotificationItem key={n.id} {...toItemProps(n)} />)}
|
|
45
|
+
</NotificationInboxScreen>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Native
|
|
50
|
+
import { FlatList } from "react-native";
|
|
51
|
+
import { NotificationInboxScreen } from "@hjmds/react-native/screens";
|
|
52
|
+
|
|
53
|
+
<NotificationInboxScreen
|
|
54
|
+
title={t("inbox.title")}
|
|
55
|
+
filters={filterControl}
|
|
56
|
+
scroll="content"
|
|
57
|
+
state={query.isPending ? { kind: "loading", title: t("inbox.loading") } : { kind: "ready" }}
|
|
58
|
+
>
|
|
59
|
+
<FlatList data={items} renderItem={renderNotification} />
|
|
60
|
+
</NotificationInboxScreen>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 제품이 공급할 것
|
|
64
|
+
|
|
65
|
+
| 슬롯·prop | 내용 |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `title`, `description` | 지역화 제목 |
|
|
68
|
+
| `filters` | 필터 control(SegmentedControl·Chip 등). 상태가 바뀌어도 유지된다 |
|
|
69
|
+
| `notice` | 새로고침 실패 같은 안내. `filters` 위에 함께 놓인다 |
|
|
70
|
+
| `state` | `ready` · `loading` · `empty` · `error` · `restricted`. ready 외에는 지역화 `title` 필수 |
|
|
71
|
+
| `stateAction` | 재시도·로그인 같은 실제 행동 |
|
|
72
|
+
| `children` | 알림 목록(데이터·페이지 합치기·더 보기는 제품) |
|
|
73
|
+
| `header`, `leading`, `actions` | 기존 내비게이션 헤더, 뒤로 가기, “모두 읽음” 같은 행동 |
|
|
74
|
+
|
|
75
|
+
## 배치
|
|
76
|
+
|
|
77
|
+
| 항목 | 값 | 근거 |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| 크기 | ScreenLayout 폭(최대 720) 안에서 알림 행이 폭을 채운다 | `ScreenLayout`, `NotificationItem`(ListRow) |
|
|
80
|
+
| 간격 | 화면 padding `spacing.md` 16(notice 영역은 좌우만); `notice`–`filters` `spacing.sm` 12; 행 사이 간격은 목록(제품) 소유 | Web·Native `NotificationInboxScreen` `Stack gap="sm"` |
|
|
81
|
+
| 순서·정렬 | 헤더(`leading` → 제목 → `actions`) → `notice` → `filters` → 알림 목록 | 렌더 순서 |
|
|
82
|
+
| 고정·스크롤 | `notice`·`filters`는 헤더 아래 고정되고 상태 교체 중에도 남는다; 목록은 기본 화면 스크롤(`scroll="screen"`), 가상화 목록이면 `scroll="content"` | `ScreenLayout` `notice`·`scroll` |
|
|
83
|
+
| 좁은 폭·큰 글자 | 제목 열 최소 폭 120 × 글자 배율, 모자라면 `actions`(모두 읽음 등)가 다음 줄로 내려간다; 필터 줄바꿈·가로 스크롤은 `filters` 소유 | `screenPatternRecipe.headerMinWidth` |
|
|
84
|
+
|
|
85
|
+
## 꼭 지킬 것
|
|
86
|
+
|
|
87
|
+
- 읽음 처리·계정 scope·요청 취소·라우팅·focus 복구는 제품 소유다. 화면 표시만으로 읽음 처리하지 않는다.
|
|
88
|
+
- 새로고침 실패는 `state="error"`가 아니라 `ready` + `notice`로 알린다. 오류로 바꾸면 목록이 내려간다.
|
|
89
|
+
- FlatList 같은 가상화 목록은 `scroll="content"`로 넣는다. 스크롤 컨테이너를 이중으로 만들지 않는다.
|
|
90
|
+
- 이미 `<main>` 안이면 Web은 `as="section"`, 라우트가 여백을 이미 주면 `contentInset="none"`.
|
|
91
|
+
|
|
92
|
+
## 플랫폼 차이
|
|
93
|
+
|
|
94
|
+
| 항목 | Web | Native |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| 배치·식별 | `layoutStyle`, `className`, `as` | `layoutStyle`, `testID` |
|
|
97
|
+
| 스크롤 | 없음 | `scrollRef`, `scrollProps`(`refreshControl` 등) |
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# NotificationItem
|
|
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
|
+
알림함의 알림 한 행에 쓴다. [ListRow](list-row.md)에 읽음 여부(제목 굵기)와
|
|
14
|
+
지역화한 “상태 · 시각” 줄을 더한 조합이다. 탭 동작·링크는 ListRow 그대로다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 알림이 아닌 일반 목록 행 | [ListRow](list-row.md) |
|
|
21
|
+
| 알림함 화면 전체 틀 | [NotificationInboxScreen](notification-inbox-screen.md) |
|
|
22
|
+
| 화면 위에 잠시 뜨는 알림 | [Toast](toast.md) |
|
|
23
|
+
| 화면 안에 남는 상태 안내 | [Notice](notice.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `NotificationItem` | supplemental, 루트 barrel에 없음 | `/screens` | `/screens` |
|
|
30
|
+
|
|
31
|
+
`@hjmds/react/screens`, `@hjmds/react-native/screens`로만 import한다. 추가 optional peer는 없다.
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { NotificationItem } from "@hjmds/react/screens";
|
|
38
|
+
|
|
39
|
+
<NotificationItem
|
|
40
|
+
read={n.read}
|
|
41
|
+
statusLabel={n.read ? t("inbox.read") : t("inbox.unread")}
|
|
42
|
+
timestamp={formatRelative(n.createdAt)}
|
|
43
|
+
title={n.title}
|
|
44
|
+
description={n.body}
|
|
45
|
+
leading={senderAvatar}
|
|
46
|
+
href={n.url}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Native
|
|
52
|
+
import { NotificationItem } from "@hjmds/react-native/screens";
|
|
53
|
+
|
|
54
|
+
<NotificationItem
|
|
55
|
+
read={n.read}
|
|
56
|
+
statusLabel={n.read ? t("inbox.read") : t("inbox.unread")}
|
|
57
|
+
timestamp={formatRelative(n.createdAt)}
|
|
58
|
+
title={n.title}
|
|
59
|
+
description={n.body}
|
|
60
|
+
onPress={() => openNotification(n)}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 제품이 공급할 것
|
|
65
|
+
|
|
66
|
+
| prop | 내용 |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `read` | 읽음 여부(필수). 안 읽음이면 제목이 굵어진다 |
|
|
69
|
+
| `statusLabel` | 지역화 읽음 상태 문구(필수). 굵기만으로 상태를 전하지 않도록 글로도 표시한다 |
|
|
70
|
+
| `timestamp` | 제품이 포맷한 상대·절대 시각 문자열(필수) |
|
|
71
|
+
| `title`, `description` | 알림 내용 |
|
|
72
|
+
| 나머지 ListRow props | `leading`·`trailing`·`density`·`disabled` 등, 이동은 `href`/`onClick`(Web), `onPress`(Native) |
|
|
73
|
+
|
|
74
|
+
## 배치
|
|
75
|
+
|
|
76
|
+
| 항목 | 값 | 근거 |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 크기 | ListRow 크기·density와 내용 높이를 따른다; 모서리 radius 0(Web) | Web `.hjm-notification-item`, `ListRow` |
|
|
79
|
+
| 간격 | ListRow 내부 간격; Web은 `description`–상태·시각 줄 `spacing.xxs` 4, Native는 같은 description 문자열 안 줄바꿈; 행을 카드 padding으로 다시 감싸지 않는다 | Web `Stack gap="xxs"`, Native `description` join |
|
|
80
|
+
| 순서·정렬 | leading → 제목(안 읽음은 굵게) → `description` → `statusLabel · timestamp` → trailing | Web·Native `NotificationItem` |
|
|
81
|
+
| 고정·스크롤 | 목록이 스크롤; 행 자체는 고정하지 않는다 | `NotificationInboxScreen` |
|
|
82
|
+
| 좁은 폭·큰 글자 | 제목·설명·상태 줄이 줄바꿈되며 행 높이가 늘어난다; 읽음 여부는 굵기만이 아니라 `statusLabel` 문구로도 전한다 | `NotificationItem` |
|
|
83
|
+
|
|
84
|
+
## 꼭 지킬 것
|
|
85
|
+
|
|
86
|
+
- 표시·탭·스크롤이 읽음 처리를 하지 않는다. 읽음 mutation은 제품이 `onPress`/`onClick` 또는 화면 정책에서 실행하고 `read`를 갱신한다.
|
|
87
|
+
- `selected`는 받지 않는다(타입에서 제외). 선택 목록이 필요하면 ListRow를 쓴다.
|
|
88
|
+
- 시각 포맷·상대시간은 제품 i18n에서 만든다.
|
|
89
|
+
|
|
90
|
+
## 플랫폼 차이
|
|
91
|
+
|
|
92
|
+
| 항목 | Web | Native |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `title`, `description` 타입 | `ReactNode` | `string` |
|
|
95
|
+
| 상태·시각 줄 | 설명 아래 caption `Text` | `description`과 줄바꿈(`\n`)으로 합친 한 문자열 |
|
|
96
|
+
| 이동 | `href`, `onClick` | `onPress` |
|
|
97
|
+
| 추가 슬롯 | `className`, `layoutStyle` | `titleMetadata`, `trailingAction`, `trailingText`, `layoutStyle` |
|
|
98
|
+
| 제목 굵기 조정 | `Text emphasis` | 렌더러 비공개 입력(`hjmTitleEmphasis`)으로 안 읽음 700(`fontWeight.bold`)·읽음 400(`fontWeight.regular`). deprecated `ListRow.titleStyle`을 쓰지 않으므로 HJM이 `titleStyle` 경고를 내지 않는다. `NotificationItemProps`에는 `titleStyle`이 없다(미게시(1.12.1 이후)) |
|