@hjmds/design-contracts 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +25 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +17 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +5 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +376 -0
- package/docs/sheet.md +12 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +7 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +104 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +215 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +138 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +151 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +274 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# ContentTransition
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: contract `src/content-transition.ts`(`resolveContentTransition`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/시각 효과/내용 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
같은 자리의 내용이 상태에 따라 바뀔 때(필터 결과 패널, 단계별 본문) 새 내용이 짧게 나타나도록 감싼다.
|
|
14
|
+
`stateKey`가 바뀔 때만 움직이고, 화면에는 현재 내용 하나만 남는다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 바뀌는 것이 텍스트 한 줄 | [TextTransition](text-transition.md) |
|
|
21
|
+
| 화면 사이 이동 | [SharedTransitionScreen](shared-transition-screen.md) |
|
|
22
|
+
| 내용이 아직 로딩 중 | [Skeleton](skeleton.md) |
|
|
23
|
+
| 내용을 펼치고 접기 | [Collapsible](collapsible.md) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `ContentTransition` | 보조 — 별도 보조 기능(supplemental) | `/content-transition` | `/content-transition` |
|
|
30
|
+
| `TextTransition` | 동반 — 텍스트 전용, [별도 지침](text-transition.md) | `/content-transition` | `/content-transition` |
|
|
31
|
+
|
|
32
|
+
root에서 export되지 않고 granular subpath로만 import 된다. Web은 optional peer `framer-motion`이 필요하다.
|
|
33
|
+
Native는 React Native `Animated`만 써서 추가 peer가 없다.
|
|
34
|
+
|
|
35
|
+
## 최소 사용 예
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Web
|
|
39
|
+
import { ContentTransition } from "@hjmds/react/content-transition";
|
|
40
|
+
import { useRef } from "react";
|
|
41
|
+
|
|
42
|
+
// 상태 → 키 상수 표. 키를 템플릿 문자열로 만들지 않는다.
|
|
43
|
+
const titleKey = { all: "results.all.title", unread: "results.unread.title" } as const;
|
|
44
|
+
|
|
45
|
+
function Results({ filter }: { filter: keyof typeof titleKey }) {
|
|
46
|
+
const heading = useRef<HTMLHeadingElement>(null);
|
|
47
|
+
return (
|
|
48
|
+
<ContentTransition stateKey={filter} preset="rise" focusTarget={heading}>
|
|
49
|
+
<h2 ref={heading} tabIndex={-1}>{t(titleKey[filter])}</h2>
|
|
50
|
+
<ResultList filter={filter} />
|
|
51
|
+
</ContentTransition>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// Native
|
|
58
|
+
import { ContentTransition } from "@hjmds/react-native/content-transition";
|
|
59
|
+
|
|
60
|
+
<ContentTransition stateKey={step} preset="slide">
|
|
61
|
+
<StepBody step={step} />
|
|
62
|
+
</ContentTransition>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 축과 기본값
|
|
66
|
+
|
|
67
|
+
| prop | 값 | 기본값 | 설명 |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `preset` | `fade` · `rise` · `slide` · `scale` | `fade` | `rise`는 아래 12에서, `slide`는 가로 16(RTL이면 반대), `scale`은 0.96에서 시작 |
|
|
70
|
+
| `motion` | `system` · `none` | `system` | `system`은 reduced motion을 따르고 `none`은 항상 즉시 교체 |
|
|
71
|
+
| `stateKey` | `string` | — (필수) | 바뀔 때만 새 내용이 나타난다 |
|
|
72
|
+
| Web `focusTarget` | `RefObject<HTMLElement \| null>` | — | 바뀌기 전 포커스가 안에 있었으면 전환 뒤 이 요소로 옮긴다 |
|
|
73
|
+
| Web `layoutStyle` | 배치 전용 style | — | 바깥 고정 wrapper에 붙는다(키가 바뀌는 안쪽 패널이 아님) |
|
|
74
|
+
|
|
75
|
+
콜백 prop은 없다. `TextTransition`은 `text: string`을 `stateKey`로 쓴다.
|
|
76
|
+
|
|
77
|
+
- 첫 렌더는 움직이지 않는다. 시간은 `motion.normal`, 곡선은 `easing.enter` 토큰이다.
|
|
78
|
+
|
|
79
|
+
## 배치
|
|
80
|
+
|
|
81
|
+
| 항목 | 값 | 근거 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| 크기 | 자체 크기·여백이 없다. Web은 `div` 두 겹(블록), Native는 `Animated.View` 하나로 감싼다 | `packages/react/src/content-transition.tsx`, `packages/react-native/src/content-transition.tsx` |
|
|
84
|
+
| 간격 | 자체 간격이 없다. 위아래 간격은 감싸는 [Stack](stack.md) 등이 정한다. 움직임 폭(세로 12·가로 16·0.96배)만큼 래퍼 밖으로 잠깐 밀려 나오므로 바로 옆 요소와 간격을 둔다 | `src/content-transition.ts` |
|
|
85
|
+
| 순서·정렬 | 바뀌는 영역 하나만 감싼다(결과 패널, 단계 본문). 필터 막대·탭·제목처럼 그대로 남는 부분은 바깥에 둔다 | — |
|
|
86
|
+
| 고정·스크롤 | Native 래퍼에는 `flex`가 없어 남은 높이를 채우지 않는다. 화면 높이를 채워야 하는 내용이면 바깥 View가 높이를 정한다 | `packages/react-native/src/content-transition.tsx` |
|
|
87
|
+
| 좁은 폭·큰 글자 | — | — |
|
|
88
|
+
|
|
89
|
+
## 꼭 지킬 것
|
|
90
|
+
|
|
91
|
+
- `stateKey`는 내용의 의미가 바뀔 때만 바꾼다. 매 렌더 새 값을 주면 계속 다시 나타난다.
|
|
92
|
+
- 사라지는 내용의 복사본을 남기지 않는 계약이다. 교차 페이드를 직접 만들려고 두 겹을 겹치지 않는다.
|
|
93
|
+
- Web 배치는 `layoutStyle`(바깥 wrapper)로 한다. Native는 배치 prop(`style`·`layoutStyle`)이 없어 바깥 View가 배치한다.
|
|
94
|
+
|
|
95
|
+
## 플랫폼 차이
|
|
96
|
+
|
|
97
|
+
| 항목 | Web | Native |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| 포커스 복원 | `focusTarget`: 바뀌기 전 포커스가 안에 있었으면 그 요소로 옮긴다 | 없음 |
|
|
100
|
+
| 앱이 백그라운드로 감 | 해당 없음 | 진행 중 전환을 멈추고 바로 표시 |
|
|
101
|
+
| 배치 prop | `layoutStyle`(바깥 wrapper) | 없음 |
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# ContextMenu
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [ContextMenu](../../context-menu.md), [선택 어댑터](../../optional-adapters.md), recipe `menuRecipe`
|
|
9
|
+
- 스토리북: `배포/컴포넌트/탐색/상황별 메뉴`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
Web에서 제품이 소유한 영역(카드·목록 행·캔버스)의 우클릭·길게 누르기·Shift+F10에 명령 목록을
|
|
14
|
+
띄울 때 쓴다. 같은 명령은 화면의 다른 경로(버튼·Menu)에도 있어야 한다. Native canonical 구현은 없고,
|
|
15
|
+
OS 길게 누르기 메뉴가 필요하면 선택 어댑터 `NativeContextMenu`를 쓴다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 버튼을 눌러 여는 명령 목록 | [Menu](menu.md) |
|
|
22
|
+
| 앱 상단 메뉴 막대(Web) | [Menubar](menubar.md) |
|
|
23
|
+
| 목록 행을 밀어 나오는 행동 | [SwipeActions](swipe-actions.md) |
|
|
24
|
+
| 텍스트 선택·링크·이미지 위의 브라우저 기본 메뉴 대체 | 쓰지 않는다([계약](../../context-menu.md)) |
|
|
25
|
+
| Native에서 추가 의존성 없이 행동 목록 | [Menu](menu.md), [Sheet](sheet.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `ContextMenu` | 기본 — Web 전용 | `@hjmds/react`, `/context-menu` | 없음 |
|
|
32
|
+
| `NativeContextMenu` | 확장 — OS 길게 누르기 메뉴 선택 어댑터 | 없음 | `/context-menu-native` |
|
|
33
|
+
|
|
34
|
+
`NativeContextMenu`는 root에서 재노출되지 않는다. `@hjmds/react-native/context-menu-native`로만 import한다.
|
|
35
|
+
|
|
36
|
+
## 최소 사용 예
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Web
|
|
40
|
+
import { ContextMenu } from "@hjmds/react/context-menu";
|
|
41
|
+
|
|
42
|
+
<ContextMenu
|
|
43
|
+
accessibilityLabel={t("post.actions")}
|
|
44
|
+
items={[
|
|
45
|
+
{ id: "share", label: t("post.share"), textValue: t("post.share") },
|
|
46
|
+
{ id: "delete", label: t("post.delete"), textValue: t("post.delete"), tone: "danger" },
|
|
47
|
+
]}
|
|
48
|
+
onAction={(id) => runPostAction(post, id)}
|
|
49
|
+
>
|
|
50
|
+
<PostCard post={post} />
|
|
51
|
+
</ContextMenu>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Native (선택 어댑터)
|
|
56
|
+
import { NativeContextMenu } from "@hjmds/react-native/context-menu-native";
|
|
57
|
+
import { Pressable } from "react-native";
|
|
58
|
+
|
|
59
|
+
<NativeContextMenu
|
|
60
|
+
items={[
|
|
61
|
+
{ id: "share", label: t("post.share") },
|
|
62
|
+
{ id: "delete", label: t("post.delete"), tone: "danger" },
|
|
63
|
+
]}
|
|
64
|
+
onAction={(id) => runPostAction(post, id)}
|
|
65
|
+
>
|
|
66
|
+
<Pressable accessibilityRole="button" accessibilityLabel={t("post.open")}>
|
|
67
|
+
<PostCardBody post={post} />
|
|
68
|
+
</Pressable>
|
|
69
|
+
</NativeContextMenu>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 축과 기본값
|
|
73
|
+
|
|
74
|
+
| prop | 값 | 기본값 | 설명 |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| Web `items` | `{ id, label, textValue, shortcut?, disabled?, tone?: "default" \| "danger" }[]` | — (필수) | `MenuItemDescriptor`. `description`은 그리지 않는다 |
|
|
77
|
+
| Native `items` | `{ id: string, label: string, disabled?, tone?: "default" \| "danger" }[]` | — (필수) | 비거나 id 중복·빈 라벨이면 `TypeError` |
|
|
78
|
+
| `onAction` | Web `(id: Key) => void` · Native `(id: string) => void` | — (필수) | 고른 항목 id. 메뉴는 닫힌다 |
|
|
79
|
+
| Web `accessibilityLabel` | `string` | — (필수) | 메뉴 이름 |
|
|
80
|
+
| Native `onOpenChange` | `(open: boolean) => void` | — | OS 메뉴가 열리고 닫힐 때 |
|
|
81
|
+
| Web `layoutStyle` | 배치 전용 style | — | 영역 래퍼(흐름 안)에 붙는다. 떠 있는 메뉴는 배치하지 않는다 |
|
|
82
|
+
| Web `className` | `string` | — | 영역 래퍼 |
|
|
83
|
+
|
|
84
|
+
## 배치
|
|
85
|
+
|
|
86
|
+
| 항목 | 값 | 근거 |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| 크기 | 메뉴(Web): 최대 폭 `min(24rem, 90vw)`, 최대 높이 뷰포트 높이 − 2×`spacing.md`(16). 항목 최소 높이 `control.minTouchTarget` 44. 감싼 영역 래퍼(`.hjm-context-menu-host`)는 `min-inline-size: 0`만 가지며 크기·여백을 더하지 않는다 | `packages/react/src/styles.css` `.hjm-context-menu*` |
|
|
89
|
+
| 간격 | 메뉴 안쪽 여백 `spacing.xs` 8(`menuRecipe.surface.padding`, Menu와 같다. 2026-10-06까지 4, 1.12.1 이후 미게시), 항목 여백 `spacing.xs` 8 · `spacing.sm` 12, 라벨–단축키 간격 `spacing.sm` 12 | `packages/react/src/styles.css` `.hjm-context-menu__item` |
|
|
90
|
+
| 순서·정렬 | 항목은 `items` 배열 순서대로 위에서 아래로 그린다. 단축키는 끝에 붙고 줄바꿈하지 않는다. 우클릭·길게 누르기는 누른 좌표, Shift+F10·메뉴 키는 영역의 시작 쪽 아래 모서리에 메뉴를 연다 | `packages/react/src/context-menu.tsx`, `src/context-menu.ts` |
|
|
91
|
+
| 고정·스크롤 | `position: fixed`로 뜨고 뷰포트 밖으로 넘치면 안쪽으로 밀어 넣는다. 최대 높이를 넘으면 메뉴 안에서 스크롤한다 | `packages/react/src/context-menu.tsx` |
|
|
92
|
+
| 좁은 폭·큰 글자 | 좁은 폭에서는 최대 폭이 `90vw`로 줄고 항목 라벨은 줄바꿈한다. Native `NativeContextMenu`의 위치·크기·미리보기는 OS가 정한다 | `packages/react/src/styles.css` |
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
┌ 카드(ContextMenu 영역) ─────────────────┐
|
|
96
|
+
│ ● 우클릭·길게 누른 지점 │
|
|
97
|
+
│ ┌──────────────────────┐ │
|
|
98
|
+
│ │ 공유 ⌘S │ │ 항목 최소 높이 44
|
|
99
|
+
│ │ 삭제(danger) │ │
|
|
100
|
+
│ └──────────────────────┘ │
|
|
101
|
+
└──────────────────────────────────────────┘
|
|
102
|
+
키보드로 열면 메뉴 왼쪽 위가 영역의 왼쪽 아래 모서리에 붙는다(RTL은 시작 쪽).
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 꼭 지킬 것
|
|
106
|
+
|
|
107
|
+
- 메뉴 이름·항목 라벨은 i18n 키로 넣는다. 위험한 행동은 `tone: "danger"`로 표시하고 실행 전 확인은 제품이 한다.
|
|
108
|
+
- Web 항목은 `MenuItemDescriptor`(`id`·`label`·`textValue` 필수, `shortcut`·`disabled`·`tone` 선택)다.
|
|
109
|
+
`description`은 타입에 있지만 ContextMenu는 그리지 않는다.
|
|
110
|
+
- Web 영역은 `tabIndex=0` 래퍼가 되어 키보드로 열 수 있다. 같은 명령을 다른 보이는 경로에도 둔다.
|
|
111
|
+
- `NativeContextMenu`의 `children`은 접근 가능한 native 요소 하나다(`asChild`로 트리거가 된다).
|
|
112
|
+
항목이 비거나, id가 중복되거나, id·라벨이 비면 실행 중 `TypeError`다.
|
|
113
|
+
- 메뉴 색·모양은 OS(Native)와 `menuRecipe`(Web)가 소유한다. 임의 색은 지원하지 않는다.
|
|
114
|
+
|
|
115
|
+
### Native 선택 어댑터 설치 조건
|
|
116
|
+
|
|
117
|
+
- `zeego` 3.0.6이 optional peer다. zeego가 요구하는 `@react-native-menu/menu` 1.2.2,
|
|
118
|
+
`react-native-ios-context-menu` 3.2.1, `react-native-ios-utilities` 5.2.0도 package.json의 optional peer로
|
|
119
|
+
고정돼 있다. 이 subpath를 쓰는 앱만 설치한다. 기본 entry는 이 peer 없이 동작한다.
|
|
120
|
+
- native module이라 개발 클라이언트를 다시 빌드해야 하고 Expo Go로는 확인할 수 없다.
|
|
121
|
+
- 배포된 HJM은 workspace patch를 적용해 주지 않는다. `@hjmds/react-native/docs/patches/`의 menu·
|
|
122
|
+
ios-context-menu·ios-utilities patch를 앱에 복사해 등록한 뒤 빌드한다([선택 어댑터](../../optional-adapters.md)).
|
|
123
|
+
- 2026-10에 optional native peer가 없는 앱에서 다른 선택 subpath(celebration·effect-surface·qr-code·
|
|
124
|
+
thinking-orb·toast-liquid)가 tsc·테스트는 통과하고 기기 Metro에서 크래시를 냈다. 이 subpath도
|
|
125
|
+
peer를 import하므로 기기에서 실행해 확인한다.
|
|
126
|
+
|
|
127
|
+
## 플랫폼 차이
|
|
128
|
+
|
|
129
|
+
| 항목 | Web | Native |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| 구현 | `ContextMenu` | `NativeContextMenu` |
|
|
132
|
+
| 여는 방법 | 우클릭, 터치 길게 누르기(500ms), Shift+F10·메뉴 키 | OS 길게 누르기 |
|
|
133
|
+
| 메뉴 접근성 이름 | `accessibilityLabel`(필수) | 없음(자식 요소가 이름을 가짐) |
|
|
134
|
+
| 항목 `textValue`·`shortcut` | 있음 | 없음 |
|
|
135
|
+
| 열림 알림 | 없음 | `onOpenChange(open)` |
|
|
136
|
+
| 성숙도 | stable | 실험적 어댑터(기기 증거 전까지 canonical unsupported) |
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# CounterBadge
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: recipe `counterBadgeRecipe`(`src/counter-badge-recipe.ts`), 숫자 규칙 `formatCounterBadgeCount`(`src/counter-badge.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/숫자 배지`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
읽지 않은 알림·메시지·장바구니 수처럼 **셀 수 있는 개수**를 아이콘·행 옆에 작게 보일 때 쓴다.
|
|
14
|
+
0이면 아무것도 그리지 않고, `max`를 넘으면 `99+`처럼 줄인다. Web은 숫자 없이 "새것이 있음"만
|
|
15
|
+
알리는 점(`dot`)도 그린다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 상태·분류 텍스트(신규, 완료, 오류) | [Badge](badge.md), [Tag](tag.md) |
|
|
22
|
+
| 하단 탭의 개수 | [BottomNavigation](bottom-navigation.md) 항목의 `badge` |
|
|
23
|
+
| 큰 숫자 지표 | [Statistic](statistic.md) |
|
|
24
|
+
| 진행률 | [Progress](progress.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `CounterBadge` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { CounterBadge } from "@hjmds/react/display";
|
|
37
|
+
|
|
38
|
+
<CounterBadge
|
|
39
|
+
count={unread}
|
|
40
|
+
accessibilityLabel={t("inbox.unreadCount", { count: unread })}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Native
|
|
46
|
+
import { CounterBadge } from "@hjmds/react-native/data-display";
|
|
47
|
+
|
|
48
|
+
<CounterBadge
|
|
49
|
+
count={unread}
|
|
50
|
+
accessibilityLabel={t("inbox.unreadCount", { count: unread })}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 축과 기본값
|
|
55
|
+
|
|
56
|
+
| prop | 값 | 기본값 | 설명 |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| `tone` | `danger` · `brand` · `neutral` | `danger` | — |
|
|
59
|
+
| `size` | `small` · `medium` | `medium` | — |
|
|
60
|
+
| `variant` | `inline` · `floating` | `inline` | `floating`은 아이콘 모서리에 겹칠 때 쓰는 테두리 있는 판 |
|
|
61
|
+
| `max` | 숫자 | `99` | 소수는 버리고 음수·`NaN`은 0으로 본다 |
|
|
62
|
+
| `dot` | `true` · `false` | `false` | Web만. 숫자 대신 8px 점을 그린다 |
|
|
63
|
+
| `count` | `number` | — (필수) | 0 이하면 아무것도 그리지 않는다 |
|
|
64
|
+
| `accessibilityLabel` | `string` | — | 없으면 보조기술에서 숨는다. 빈 문자열은 `TypeError` |
|
|
65
|
+
| `layoutStyle` | 배치 전용 style | — | 위치·여백만. Native `style`은 deprecated |
|
|
66
|
+
|
|
67
|
+
콜백 prop은 없다.
|
|
68
|
+
|
|
69
|
+
## 배치
|
|
70
|
+
|
|
71
|
+
| 항목 | 값 | 근거 |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| 크기 | `medium` 높이·최소 폭 20, `small` 높이·최소 폭 16, `dot` 8×8, 모두 `radius.full`. 자릿수가 늘면 폭만 늘어난다(`99+`). 터치 대상이 아니므로 누르는 것은 부모다 | `src/counter-badge-recipe.ts`, `packages/react/src/styles.css` `.hjm-counter-badge` |
|
|
74
|
+
| 간격 | 좌우 여백 `medium` `spacing.xs` 8, `small` `spacing.xxs` 4. `floating` 테두리 `stroke.strong` 2가 아이콘과 배지를 떼어 보인다 | `src/counter-badge-recipe.ts` |
|
|
75
|
+
| 순서·정렬 | `inline`: 탭 라벨·목록 행의 끝 쪽에 텍스트와 나란히 둔다. `floating`: 컴포넌트가 스스로 위치를 잡지 않으므로 부모를 relative로 두고 배지를 위·끝 모서리(`top: 0`, 끝 0)에 absolute로 놓는다. 공개된 `NotificationBell`(`/notification-bell`)이 IconButton 위에 이 배치를 그대로 쓴다 | `packages/react/src/notification-bell.tsx`, `packages/react-native/src/notification-bell.tsx` |
|
|
76
|
+
| 고정·스크롤 | — | — |
|
|
77
|
+
| 좁은 폭·큰 글자 | Web은 `flex-shrink: 0`이라 좁아져도 줄지 않는다 | `packages/react/src/styles.css` `.hjm-counter-badge` |
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
inline (행 끝) floating (아이콘 모서리)
|
|
81
|
+
┌─────────────────────────────┐ ┌──────┐(3) ← top 0, 끝 0
|
|
82
|
+
│ 받은 편지함 (12) │ │ 🔔 │
|
|
83
|
+
└─────────────────────────────┘ └──────┘ IconButton 44×44
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 꼭 지킬 것
|
|
87
|
+
|
|
88
|
+
- 이름을 붙이지 않으면 배지는 보조기술에서 숨겨진다. 부모(아이콘 버튼·행)의 접근성 이름이 개수를
|
|
89
|
+
이미 말할 때만 `accessibilityLabel`을 생략한다. 빈 문자열을 넘기면 `TypeError`가 난다.
|
|
90
|
+
- `dot`에는 `accessibilityLabel`이 필수다(없으면 `TypeError`).
|
|
91
|
+
- 개수 표기·`+` 처리는 컴포넌트에 맡기고 `"99+"` 같은 문자열을 앱에서 만들지 않는다. 배지가 아닌
|
|
92
|
+
자리에서 같은 표기가 필요하면 `formatCounterBadgeCount`(`@hjmds/design-contracts`, `/recipes`)를 쓴다.
|
|
93
|
+
- 색은 `tone`과 제품 테마 토큰으로 바꾼다. 배지 색을 직접 칠하지 않는다.
|
|
94
|
+
- 배치는 `layoutStyle`로 한다(Web·Native). Native `style`은 deprecated — 배치는 `layoutStyle`, 외형은 `tone`·`size`·`variant`로 옮긴다(개발 모드 1회 경고, 다음 major 제거).
|
|
95
|
+
|
|
96
|
+
## 플랫폼 차이
|
|
97
|
+
|
|
98
|
+
| 항목 | Web | Native |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `dot` | 있음 | 없음 |
|
|
101
|
+
| 배치 | `layoutStyle`(그 밖에 `span` 속성 전달, `style`과 합친다) | `layoutStyle`(`style`은 deprecated) |
|
|
102
|
+
| ref | `HTMLSpanElement` | 없음 |
|
|
103
|
+
|
|
104
|
+
## 함정
|
|
105
|
+
|
|
106
|
+
- `dot`이어도 `count`가 0이면 아무것도 그리지 않는다(숫자 계산이 먼저 `null`을 돌려준다).
|
|
107
|
+
점을 보이려면 `count`를 1 이상으로 넘긴다.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# DataTable
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [DataTable](../../data-table.md), `src/data-table.ts`(`dataTableRecipe`, `getNextDataTableSortState`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/데이터 표`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
여러 행의 데이터를 열로 맞춰 훑고, 열 기준으로 정렬하거나 행을 골라 일괄 작업할 때 쓴다(Web).
|
|
14
|
+
정렬·필터 실행, 페이지 나누기는 제품이 소유한다. DataTable은 다음 정렬 상태와 선택 상태만 판정한다.
|
|
15
|
+
선택·정렬이 없는 단순 표는 companion `Table`을 쓴다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 한 대상의 속성(이름–값) 나열 | [DescriptionList](description-list.md) |
|
|
22
|
+
| 행 하나가 하나의 항목인 목록, 모바일 화면 | [List](list.md), [ListRow](list-row.md) |
|
|
23
|
+
| 아주 긴 목록의 가상 스크롤 | [VirtualList](virtual-list.md) |
|
|
24
|
+
| 행 확장(상세 펼침) | [Accordion](accordion.md)·[Collapsible](collapsible.md)을 행 안에 합성 |
|
|
25
|
+
| Native 화면 | Native renderer 없음 |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `DataTable` | 기본 — 정렬·행 선택·비동기 상태를 갖는 표 | `@hjmds/react`, `/data-table` | 없음 |
|
|
32
|
+
| `Table` | 동반 — 행 객체와 `cell` 렌더러를 받는 단순 표 | `@hjmds/react`, `/display` | 없음 |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { DataTable } from "@hjmds/react/data-table";
|
|
39
|
+
|
|
40
|
+
<DataTable
|
|
41
|
+
columns={[
|
|
42
|
+
{ id: "name", header: t("orders.col.name"), sortable: true },
|
|
43
|
+
{ id: "total", header: t("orders.col.total"), align: "end" },
|
|
44
|
+
]}
|
|
45
|
+
rows={orders.map((order) => ({ id: order.id }))}
|
|
46
|
+
renderCell={(rowId, columnId) => cellFor(rowId, columnId)}
|
|
47
|
+
labels={{
|
|
48
|
+
table: t("orders.table"),
|
|
49
|
+
selectAll: t("orders.selectAll"),
|
|
50
|
+
selectRow: (rowId) => t("orders.selectRow", { name: nameOf(rowId) }),
|
|
51
|
+
// 두 번째 인자는 이 열이 아니라 표 전체의 정렬 상태다(함정 참조).
|
|
52
|
+
sortColumn: (header) => t("orders.sortBy", { header }),
|
|
53
|
+
}}
|
|
54
|
+
sortState={sort}
|
|
55
|
+
onSortChange={setSort}
|
|
56
|
+
selection={{ mode: "multiple", selectedKeys: selected, onSelectionChange: setSelected }}
|
|
57
|
+
asyncState={loading ? { status: "loading", message: t("orders.loading") } : { status: "idle" }}
|
|
58
|
+
footer={pagination /* 제품이 합성한 Pagination 요소 */}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Native: 없음. Native renderer가 없다.
|
|
63
|
+
|
|
64
|
+
## 축과 기본값
|
|
65
|
+
|
|
66
|
+
| prop | 값 | 기본값 | 설명 |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `columns` | `{ id, header: string, align?, sortable?, width? }[]` | — (필수, 하나 이상) | `id`는 비어 있지 않고 중복 없음 |
|
|
69
|
+
| `rows` | `{ id, disabled? }[]` | — (필수) | 셀 내용은 `renderCell`이 그린다 |
|
|
70
|
+
| `renderCell` | `(rowId: RowKey, columnId: ColumnKey) => ReactNode` | — (필수) | 행·열 id로 셀을 그린다 |
|
|
71
|
+
| `labels` | `{ table: string; selectAll: string; selectRow: (rowId: string) => string; sortColumn: (header: string, direction: DataTableSortState) => string }` | — (필수) | 모든 문구는 i18n 키 |
|
|
72
|
+
| `sortState` | `{ columnId, direction: "ascending" \| "descending" } \| null` | `null` | 제어 전용(내부 상태 없음) |
|
|
73
|
+
| `onSortChange` | `(next: DataTableSortState<ColumnKey>) => void` | — | 다음 정렬 상태(`null` = 정렬 없음)를 알린다. 재정렬은 제품이 한다 |
|
|
74
|
+
| `sortCycle` | `three-state` · `two-state` | `three-state` | `three-state`는 오름차순 → 내림차순 → 정렬 없음 |
|
|
75
|
+
| `selection` | `{ mode: "none" }` · `{ mode: "single", selectedKey: Key \| null, onSelectionChange: (key: Key \| null) => void, disallowEmptySelection? }` · `{ mode: "multiple", selectedKeys: ReadonlySet<Key>, onSelectionChange: (keys: ReadonlySet<Key>) => void }` | 없음(선택 열 없음) | 제어(`selectedKey(s)`) 대신 `defaultSelectedKey(s)`를 주면 비제어로 내부 상태가 유지된다 |
|
|
76
|
+
| `asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error", message: string }` | `{ status: "idle" }` | `error`는 `role="alert"`, 나머지는 `role="status"`로 표 위에 나온다 |
|
|
77
|
+
| `density` | `regular` · `compact` | Provider 밀도 | 지정하지 않으면 Provider 밀도를 따른다(`comfortable` → `regular`) |
|
|
78
|
+
| 열 `align` | `start` · `center` · `end` | `start` | 머리 칸과 본문 칸 모두에 적용 |
|
|
79
|
+
| 열 `sortable` | `boolean` | `false` | — |
|
|
80
|
+
| 열 `width` | 양수 px | — | 힌트 |
|
|
81
|
+
| `footer` | `ReactNode` | — | 표 아래 합성 영역 |
|
|
82
|
+
| `layoutStyle` | 배치 전용 style(여백·폭·grid 위치) | — | 루트 wrapper에 붙는다. 시각 키는 받지 않는다 |
|
|
83
|
+
|
|
84
|
+
## 배치
|
|
85
|
+
|
|
86
|
+
| 항목 | 값 | 근거 |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| 크기 | 본문 폭을 채운다(표 `inline-size: 100%`). 선택 열 폭 `control.minTouchTarget` 44, 정렬 버튼·체크 상자 터치 영역 44 | `packages/react/src/styles.css` `.hjm-data-table*`, `src/data-table.ts` |
|
|
89
|
+
| 간격 | 루트 세로 간격 `spacing.sm` 12. 셀 여백(`dataTableRecipe.density`): `regular` 세로 `spacing.sm` 12 · 가로 `spacing.md` 16, `compact` 세로 `spacing.xs` 8 · 가로 `spacing.sm` 12. `footer` 간격 `spacing.sm` 12 | `packages/react/src/styles.css` |
|
|
90
|
+
| 순서·정렬 | 위에서 [상태 메시지] → 표 → [footer]. 선택 열은 맨 앞. 숫자·금액 열은 `align: "end"`로 끝 정렬하고 머리 행 정렬도 열 `align`을 따른다. 셀은 위 정렬. `footer`는 한 줄 flex(`space-between`, 넘치면 줄바꿈)이며 요약을 앞에, [Pagination](pagination.md)을 끝에 둔다 | `packages/react/src/data-table.tsx` |
|
|
91
|
+
| 고정·스크롤 | 가로 스크롤 컨테이너는 그리지 않는다. 긴 글자는 줄바꿈한다 | `packages/react/src/styles.css` |
|
|
92
|
+
| 좁은 폭·큰 글자 | 좁은 폭(모바일)에서 열이 많으면 표를 줄이지 말고 [List](list.md)로 바꾼다 | — |
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
┌ DataTable ───────────────────────────────────────┐
|
|
96
|
+
│ 불러오는 중…(asyncState 메시지, status/alert) │
|
|
97
|
+
│ ☐ │ 이름 ▲ │ 상태 │ 합계 │ ← 머리 행
|
|
98
|
+
│ ☐ │ … │ … │ 12,000 │
|
|
99
|
+
├────────────────────────────────────────────────────┤
|
|
100
|
+
│ 3개 선택됨 [‹ 1 2 3 ›] Pagination │ ← footer
|
|
101
|
+
└────────────────────────────────────────────────────┘
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 꼭 지킬 것
|
|
105
|
+
|
|
106
|
+
- 열 `id`·행 `id`는 비어 있지 않고 중복이 없어야 한다. 열은 하나 이상. 어기면 렌더 중 `TypeError`가 난다.
|
|
107
|
+
- `sortState`는 `sortable: true`인 열만 가리킨다. 아니면 `TypeError`.
|
|
108
|
+
- `labels`의 모든 문구는 i18n 키로 만든다. `selectRow`는 행 `id`를 받으므로 사람이 읽을 이름으로 바꿔 돌려준다.
|
|
109
|
+
- 받은 `onSortChange` 값으로 행을 재정렬하는 일은 제품이 한다(로컬 배열이든 서버 쿼리든).
|
|
110
|
+
- 페이지네이션·더 보기는 `footer`에 [Pagination](pagination.md)·[LoadMore](load-more.md)로 합성한다.
|
|
111
|
+
- 배치는 `layoutStyle`, 시각 override는 `className`으로만 한다. 행 hover·선택 색을 덮지 않는다.
|
|
112
|
+
|
|
113
|
+
## 함정
|
|
114
|
+
|
|
115
|
+
- `sortState`는 제어 전용이다. 내부 상태가 없어 `sortState` 없이 `onSortChange`만 주면 머리 칸 화살표가 바뀌지 않는다.
|
|
116
|
+
선택은 2026-10-06부터 `defaultSelectedKey(s)` 비제어도 내부 상태로 유지된다(이전에는 표시가 바뀌지 않았다).
|
|
117
|
+
- 현재 `labels.sortColumn`의 두 번째 인자는 그 열의 방향이 아니라 표 전체의 `sortState`다(정렬되지 않은 열도 다른 열의
|
|
118
|
+
상태를 받는다). 열의 현재 방향을 이름에 넣으려면 제품이 `header`로 열을 찾아 `columnId`와 비교한다.
|
|
119
|
+
- `asyncState`의 `message`는 표 위에 `status`/`alert`로 나오지만 표는 그대로 그려진다. 빈 상태 화면이
|
|
120
|
+
따로 필요하면 [EmptyState](empty-state.md)로 표를 대신 그린다.
|
|
121
|
+
- `Table`의 `onSortChange`는 `null`을 받지 못해 항상 two-state로 돈다. `emptyState`는 필수 prop이다.
|
|
122
|
+
- 미게시(1.12.1 이후) 변경: 1.12.1까지 Web `regular` 셀은 사방 `spacing.sm` 12였다. 열 폭을 그 값으로 맞춘 화면은 가로 16에서 다시 확인한다.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# DatePicker
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [DatePicker](../../date-picker.md), 격자는 [Calendar](../../calendar.md), recipe `datePickerRecipe`(`src/date-picker.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/날짜 선택`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
폼·필터 자리에서 날짜 **하나**를 고를 때 쓴다(생년월일, 방문일, 시작일 필터). 평소에는 필드 트리거만
|
|
14
|
+
보이고, 누르면 Web은 필드에 붙은 팝오버, Native는 [Sheet](sheet.md) 안에 같은 달력 격자를 연다.
|
|
15
|
+
날짜를 고르거나 지우면 닫히고 트리거로 포커스가 돌아간다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 시작~끝 기간 | [DateRangePicker](date-range-picker.md) |
|
|
22
|
+
| 달력을 화면에 늘 펼쳐 둠 | [Calendar](calendar.md) |
|
|
23
|
+
| 날짜를 키보드로 타이핑 | 지원하지 않는다(트리거로만 연다) |
|
|
24
|
+
| 날짜가 아닌 목록에서 하나 고름 | [Select](select.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `DatePicker` | 기본 | `@hjmds/react`, `/date-picker`, `/forms` | `@hjmds/react-native`, `/date-picker`, `/inputs` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
격자 `cells`(7의 배수, 행 우선)·`weekdayLabels`·`todayDate`는 제품이 자기 시계와 로캘로 만든다.
|
|
35
|
+
DatePicker는 날짜를 포맷하지 않으므로 `displayValue`도 제품이 만든다.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Web
|
|
39
|
+
import { DatePicker } from "@hjmds/react/date-picker";
|
|
40
|
+
|
|
41
|
+
<DatePicker
|
|
42
|
+
descriptor={{
|
|
43
|
+
grid: { cells, weekdayLabels, todayDate },
|
|
44
|
+
label: t("visit.date"),
|
|
45
|
+
placeholder: t("visit.datePlaceholder"),
|
|
46
|
+
displayValue: visitDate === null ? null : formatDate(visitDate),
|
|
47
|
+
selectedDate: visitDate,
|
|
48
|
+
onSelectionChange: (date) => setVisitDate(date),
|
|
49
|
+
focusedMonth: month,
|
|
50
|
+
onFocusedMonthChange: (next) => setMonth(next),
|
|
51
|
+
}}
|
|
52
|
+
monthLabel={formatMonth(month)}
|
|
53
|
+
previousMonth={{ month: prevMonthOf(month), label: t("calendar.previousMonth") }}
|
|
54
|
+
nextMonth={{ month: nextMonthOf(month), label: t("calendar.nextMonth") }}
|
|
55
|
+
composeAccessibleName={({ date, isToday, isSelected }) =>
|
|
56
|
+
t(isSelected ? "calendar.cell.selected" : isToday ? "calendar.cell.today" : "calendar.cell.default", { date: formatDate(date) })}
|
|
57
|
+
clearLabel={t("visit.clearDate")}
|
|
58
|
+
closeLabel={t("calendar.close")}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// Native
|
|
64
|
+
import { DatePicker } from "@hjmds/react-native/date-picker";
|
|
65
|
+
|
|
66
|
+
<DatePicker
|
|
67
|
+
descriptor={/* Web과 같은 descriptor */ descriptor}
|
|
68
|
+
monthLabel={formatMonth(month)}
|
|
69
|
+
composeAccessibleName={composeCellName}
|
|
70
|
+
clearLabel={t("visit.clearDate")}
|
|
71
|
+
closeLabel={t("calendar.close")}
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 축과 기본값
|
|
76
|
+
|
|
77
|
+
| prop | 값 | 기본값 | 설명 |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| `size` | `medium` · `large` | `medium` | — |
|
|
80
|
+
| `descriptor.grid` | `{ cells: { date?, outsideFocusedMonth?, disabled?, content? }[], weekdayLabels: [7개 string], todayDate: string }` | — (필수) | `cells`는 7의 배수, 행 우선 |
|
|
81
|
+
| `descriptor.label` · `accessibilityLabel` | `string` | — | 둘 중 하나는 필수 |
|
|
82
|
+
| `descriptor.placeholder` · `displayValue` | `string` · `string \| null` | — (필수) | 트리거 문구. `displayValue`는 제품이 포맷한다 |
|
|
83
|
+
| `descriptor.open` / `defaultOpen` / `onOpenChange` | `boolean` · `(open: boolean, reason: "trigger" \| "keyboard" \| "selection" \| "clear" \| "escape" \| "outside" \| "blur" \| "programmatic") => void` | 비제어 닫힘 | 제어(`open`+`onOpenChange`) 또는 비제어 한 쌍 |
|
|
84
|
+
| `descriptor.selectedDate` / `defaultSelectedDate` / `onSelectionChange` | ISO `YYYY-MM-DD` \| `null` · `(date: string \| null, reason: "activate" \| "clear") => void` | — | 제어 또는 비제어 한 쌍만 쓴다. 지우기는 `null`, `"clear"` |
|
|
85
|
+
| `descriptor.focusedMonth` / `defaultFocusedMonth` / `onFocusedMonthChange` | `YYYY-MM` · `(month: string, reason: "previous" \| "next" \| "jump") => void` | — | 표시 달. 제어 또는 비제어 한 쌍만 쓴다 |
|
|
86
|
+
| `descriptor.disabled` · `readOnly` | `boolean` | `false` | 트리거를 열지 않고 모든 날짜 셀을 비활성으로 그린다. `disabled`는 라벨과 트리거 줄을 `fieldRecipe.disabledOpacity`(0.6)로 흐리고 도움말·오류는 그대로 둔다([Field](field.md)) |
|
|
87
|
+
| `descriptor.invalid` · `error` | `boolean` · 오류 문구(Web `ReactNode`, Native `string`) | — | 둘 중 하나가 있으면 오류 테두리 |
|
|
88
|
+
| `monthLabel` | `string` | — (필수) | 제품이 포맷한 달 제목 |
|
|
89
|
+
| `composeAccessibleName` | `(info: { date, isToday, isSelected, disabled, content? }) => string` | — (필수) | 셀 이름 |
|
|
90
|
+
| `previousMonth` · `nextMonth` | `{ month: string, label: string }` | — | 없으면 이동 버튼이 없다 |
|
|
91
|
+
| `renderCellContent` | `(cell: ResolvedCalendarDateCell) => ReactNode` | — | 날짜 아래 보조 표시 |
|
|
92
|
+
| Web `onNavigateBeyondGrid` | `(detail: { date, intent, overflow: "before" \| "after" }, focusDate: (date: string) => void) => void` | — | 키보드가 격자 밖으로 나갈 때 |
|
|
93
|
+
| Native `safeAreaInsets` | Sheet `safeAreaInsets` | Provider 값 | — |
|
|
94
|
+
| `layoutStyle` | 배치 전용 style | — | 루트 배치. Native `style`은 deprecated |
|
|
95
|
+
|
|
96
|
+
## 배치
|
|
97
|
+
|
|
98
|
+
| 항목 | 값 | 근거 |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| 크기 | 트리거 높이: `medium` 44 · `large` 52(`datePickerRecipe.sizes`, 두 플랫폼), 좌우 여백 16 · 20. 2026-10-06까지 렌더 값이 Web 44·56, Native 48·56이었다(1.12.1 이후 미게시). 지우기 버튼 44×44. Web 팝오버 폭 `min(22.5rem, 100vw − 2rem)`(최대 360). 날짜 셀 44, 7열 | `datePickerRecipe.sizes`, `packages/react/src/styles.css` `.hjm-date-picker*`, `packages/react-native/src/date-picker.tsx`, `src/calendar.ts` |
|
|
101
|
+
| 간격 | 라벨·트리거·설명·오류 사이 Web `spacing.xs` 8, Native 6(렌더러 고정값). 트리거 가로 여백 `medium` `spacing.md` 16, `large` `spacing.lg` 20. 팝오버는 트리거 아래 `spacing.xs` 8, 안쪽 여백 `spacing.sm` 12 | 같은 파일 |
|
|
102
|
+
| 순서·정렬 | 폼 안에서 다른 필드와 같은 폭으로 세로로 쌓는다. 위에서 라벨 → 트리거 → 설명 → 오류. 지우기 버튼(값이 있을 때)은 Web은 트리거 안 끝에 겹치고(트리거가 끝 여백 3rem을 비움), Native는 트리거 바깥 끝에 최소 터치 영역으로 붙는다. Web 팝오버는 시작 쪽 정렬 | `packages/react/src/styles.css`, `packages/react-native/src/date-picker.tsx` |
|
|
103
|
+
| 고정·스크롤 | Web 팝오버는 필드에 붙어 층 800에 뜬다. Native 달력은 화면 아래에서 올라오는 [Sheet](sheet.md)이며 하단 안전 영역은 Provider `safeAreaInsets`를 쓴다 | 같은 파일 |
|
|
104
|
+
| 좁은 폭·큰 글자 | Web 팝오버 폭은 뷰포트 − 2rem까지 줄어든다 | `packages/react/src/styles.css` `.hjm-date-picker__popover` |
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Web Native
|
|
108
|
+
라벨 라벨
|
|
109
|
+
┌──────────────────────────[×]┐ ┌───────────────────────┐[×]
|
|
110
|
+
│ ▣ 2026-10-06 │ │ ▣ 2026-10-06 │
|
|
111
|
+
└─────────────────────────────┘ └───────────────────────┘
|
|
112
|
+
↓ spacing.xs 8 ┌ Sheet ─────────────────┐
|
|
113
|
+
┌ 팝오버 (최대 360) ───────────┐ │ 제목(라벨) [닫기]│
|
|
114
|
+
│ ‹ 2026년 10월 › │ │ ‹ 2026년 10월 › │
|
|
115
|
+
│ 일 월 화 수 목 금 토 │ │ 7×N 날짜 격자 │
|
|
116
|
+
│ 7×N 날짜 격자(셀 44) │ │ ─ 안전 영역 ─ │
|
|
117
|
+
└──────────────────────────────┘ └─────────────────────────┘
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 꼭 지킬 것
|
|
121
|
+
|
|
122
|
+
- `label` 또는 `accessibilityLabel` 중 하나, 비어 있지 않은 `placeholder`가 필수다. 어기면 `TypeError`.
|
|
123
|
+
- 모든 문구(라벨·placeholder·`clearLabel`·`closeLabel`·월 이동 라벨·셀 이름)는 i18n 키로 만든다.
|
|
124
|
+
셀 이름(`composeAccessibleName`)의 어순·문법은 제품이 소유한다.
|
|
125
|
+
- 달을 넘기면 제품이 새 달의 `cells`와 `monthLabel`을 다시 만들어 넘긴다. 달을 바꿔도 선택값은 바뀌지 않는다.
|
|
126
|
+
- 배치는 `layoutStyle`로 한다(Web·Native). 트리거 색·높이를 덮지 않는다. Native `style`은 deprecated — `layoutStyle` 또는 `size`로 옮긴다.
|
|
127
|
+
|
|
128
|
+
## 플랫폼 차이
|
|
129
|
+
|
|
130
|
+
| 항목 | Web | Native |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| 달력이 뜨는 곳 | 필드에 붙은 팝오버(뷰포트 안으로 밀고 공간이 없으면 위로 뒤집음) | `Sheet` |
|
|
133
|
+
| `description`·`error` | `ReactNode` | `string` |
|
|
134
|
+
| 격자 밖 이동 알림 `onNavigateBeyondGrid` | 있음 | 없음 |
|
|
135
|
+
| 하단 안전 영역 `safeAreaInsets` | 없음 | 있음(기본은 Provider 값) |
|
|
136
|
+
| 배치 | `layoutStyle`(그 밖에 `className`) | `layoutStyle`(`style`은 deprecated) |
|
|
137
|
+
|
|
138
|
+
## 함정
|
|
139
|
+
|
|
140
|
+
- `previousMonth`/`nextMonth`를 넘겨도 `descriptor.onFocusedMonthChange`가 없으면 두 renderer 모두 이동 버튼이 비활성이다.
|
|
141
|
+
- 예시의 Native 호출처럼 `previousMonth`/`nextMonth`를 빼면 이동 버튼 자체가 없다. 여러 달을 오가야 하면 넘긴다.
|
|
142
|
+
- 1.12.1 이하를 쓰는 화면은 트리거 높이가 recipe(`medium` 44 · `large` 52)와 달랐다(Web 44·56, Native 48·56). 다음 릴리스로 올리면 Native `medium`은 4, `large`는 두 플랫폼 모두 4 낮아지므로 그 높이로 맞춘 고정 높이 계산을 다시 본다.
|