@hjmds/design-contracts 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +25 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +17 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +5 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +376 -0
- package/docs/sheet.md +12 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +7 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +104 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +215 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +138 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +151 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +274 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Layout
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Layout](../../layout.md), recipe `layoutRecipe`(`src/layout.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/레이아웃/화면 기본 구조`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
앱의 상시 골격(header · sidebar · main · footer)을 한 번 세울 때 쓴다. Web은 실제 landmark
|
|
14
|
+
(`header`·`nav`/`aside`·`main`·`footer`)와 skip link를 만들고, Native는 같은 순서의 영역만 만든다.
|
|
15
|
+
Layout은 영역이 **있다는 사실**만 알고 그 안의 내용과 상태는 모른다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 헤더 막대의 내용 | [TopBar](top-bar.md) (header 슬롯에 넣는다) |
|
|
22
|
+
| 하단 탭 내비게이션 | [BottomNavigation](bottom-navigation.md) (footer 슬롯에 넣는다) |
|
|
23
|
+
| 사이드 내비게이션 내용 | [Sidebar](sidebar.md) (sidebar 슬롯에 넣는다) |
|
|
24
|
+
| 여닫는 패널의 열림 상태 | [SidePanel](side-panel.md) (`renderOverlay`에서 합성) |
|
|
25
|
+
| main 안의 배치 | [Stack](stack.md), [Grid](grid.md), [Container](container.md) |
|
|
26
|
+
| 사이드바 크기 조절 | [Splitter](splitter.md) |
|
|
27
|
+
| 본문 바로가기 링크만 | [SkipNav](skip-nav.md) |
|
|
28
|
+
|
|
29
|
+
## 공개 이름과 import
|
|
30
|
+
|
|
31
|
+
| 이름 | 역할 | Web | Native |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| `Layout` | 기본 | `@hjmds/react`, `/layout` | `@hjmds/react-native`, `/primitives` |
|
|
34
|
+
|
|
35
|
+
## 최소 사용 예
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Web
|
|
39
|
+
import { Layout } from "@hjmds/react/layout";
|
|
40
|
+
|
|
41
|
+
<Layout
|
|
42
|
+
header={topBar /* 제품이 만든 TopBar 요소 */}
|
|
43
|
+
skipLinkLabel={t("a11y.skipToContent")}
|
|
44
|
+
sidebar={{ role: "navigation", mode: "persistent", label: t("nav.main"), children: sidebarNav /* Sidebar 요소 */ }}
|
|
45
|
+
>
|
|
46
|
+
{page}
|
|
47
|
+
</Layout>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Native
|
|
52
|
+
import { Layout } from "@hjmds/react-native/primitives";
|
|
53
|
+
|
|
54
|
+
<Layout footer={bottomNavigation /* 제품이 만든 BottomNavigation 요소 */}>
|
|
55
|
+
{screen}
|
|
56
|
+
</Layout>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 축과 기본값
|
|
60
|
+
|
|
61
|
+
| prop | 값 | 기본값 | 설명 |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `sidebar.role` | `navigation` · `complementary` | — | `navigation`(주 내비게이션) · `complementary`(보조 콘텐츠). 내용의 의미다 |
|
|
64
|
+
| `sidebar.mode` | `persistent` · `overlay` | — | `persistent`(항상 보임) · `overlay`(여닫음). 표시 방식이며 role과 독립이다 |
|
|
65
|
+
| `sidebar.renderOverlay` | Web `(sidebarLandmark: ReactElement) => ReactNode` · Native `(sidebar: ReactNode) => ReactNode` | — | `mode: "overlay"`에서만, 그리고 필수다. 받은 landmark(사이드바 영역)를 SidePanel 등 안에 넣어 돌려준다. 열림·닫힘은 그 SidePanel이 소유한다 |
|
|
66
|
+
| `sidebar` 모양 | `{ role, mode, label, children, renderOverlay? }` + Web `landmarkProps`·`landmarkRef`, Native `containerProps` | — | `label`은 현지화 문자열 |
|
|
67
|
+
| `skipLinkLabel`(Web) | 현지화 문자열 | — | Web skip link는 키보드 포커스가 닿을 때만 보인다 |
|
|
68
|
+
| `mainId`(Web) | 공백 없는 문자열 | 자동 생성 | 비거나 공백이 있으면 `TypeError` |
|
|
69
|
+
| `layoutStyle`(Web) | 배치 key만 | — | 루트 배치. Native는 `layoutStyle`이 없고 `style`(layout primitive, 1.13 deprecated 제외 대상)만 받는다 |
|
|
70
|
+
|
|
71
|
+
- 이벤트 콜백은 없다. 사이드바 열림 상태는 `renderOverlay` 안의 SidePanel `open`·`onOpenChange`가 소유한다.
|
|
72
|
+
|
|
73
|
+
- Web persistent 사이드바 폭은 recipe 280. main 최대 폭·좌우 여백은 foundations의 `contentMaxWidth`·`pagePadding.regular`.
|
|
74
|
+
|
|
75
|
+
## 배치
|
|
76
|
+
|
|
77
|
+
| 항목 | 값 | 근거 |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| 크기 | Web: persistent 사이드바 칸 폭 280(`layoutRecipe.sidebar.width`), main 최대 폭 1200(`layout.contentMaxWidth`). Native: 루트·main은 `flex: 1`이고 여백·폭 제한은 없다 | `design-contracts/src/layout.ts`(`layoutRecipe`), `react/src/layout.tsx`, `react-native/src/primitives.tsx`(Layout) |
|
|
80
|
+
| 간격 | Web: main 좌우 여백 `spacing.lg` 20(`layout.pagePadding.regular`). skip link는 화면 왼쪽 위에서 `spacing.sm` 12 떨어진다 | `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-layout__skip-link`) |
|
|
81
|
+
| 순서·정렬 | Web: 루트는 grid다. header·footer는 전체 폭, persistent 사이드바는 왼쪽(RTL에서는 오른쪽) 칸, main은 나머지 칸에서 가운데 정렬된다. Native: header → (persistent 사이드바) → main → footer 순서로 세로로 쌓인다 | `react/src/styles.css`(`.hjm-layout`), `react-native/src/primitives.tsx`(Layout) |
|
|
82
|
+
| 고정·스크롤 | 스크롤은 main 안의 내용이 소유한다. header·footer는 main 바깥이라 스크롤되지 않는다. Web skip link는 화면에 고정되고 키보드 포커스 때만 내려온다. overlay 사이드바는 그리드 칸을 차지하지 않고 `renderOverlay`의 SidePanel이 위를 덮는다 | `react/src/styles.css`(`.hjm-layout`), `react/src/layout.tsx` |
|
|
83
|
+
| 좁은 폭·큰 글자 | — | — |
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
Web (persistent sidebar) Native
|
|
87
|
+
┌──────────────────────────────────────────┐ ┌──────────────────┐
|
|
88
|
+
│ header(TopBar) — 전체 폭 │ │ header(TopBar) │ ← 고정
|
|
89
|
+
├────────────┬─────────────────────────────┤ ├──────────────────┤
|
|
90
|
+
│ sidebar │ main(최대 1200, 좌우 20) │ │ main (flex 1) │ ← 스크롤은 내용이 소유
|
|
91
|
+
│ 280 │ │ │ │
|
|
92
|
+
│ │ │ ├──────────────────┤
|
|
93
|
+
├────────────┴─────────────────────────────┤ │ footer(BottomNav) │ ← 고정
|
|
94
|
+
│ footer — 전체 폭 │ └──────────────────┘
|
|
95
|
+
└──────────────────────────────────────────┘
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 꼭 지킬 것
|
|
99
|
+
|
|
100
|
+
- Web에서 `header`나 `sidebar`가 있으면 `skipLinkLabel`(현지화)이 필수다. 없으면 `TypeError`.
|
|
101
|
+
- 화면마다 Layout을 다시 세우지 않는다. `main` landmark는 하나여야 한다.
|
|
102
|
+
- 어느 화면 폭에서 persistent↔overlay를 바꿀지는 제품이 정한다. Layout은 전환 규칙을 갖지 않는다.
|
|
103
|
+
- 사이드바 내용·헤더 내용·푸터 내용은 각 컴포넌트가 소유한다. Layout에 상태를 다시 만들지 않는다.
|
|
104
|
+
|
|
105
|
+
## 플랫폼 차이
|
|
106
|
+
|
|
107
|
+
| 항목 | Web | Native |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| 영역 의미 | 실제 landmark role | 없음(순서만, 사이드바는 `accessibilityLabel`) |
|
|
110
|
+
| skip link | 렌더링(`skipLinkLabel`, `skipLinkProps`) | — |
|
|
111
|
+
| 영역 props | `headerProps`·`mainProps`·`footerProps`, `sidebar.landmarkProps`/`landmarkRef`, `mainId` | `headerProps`·`mainProps`·`footerProps`, `sidebar.containerProps` |
|
|
112
|
+
| persistent 사이드바 배치 | 셸 grid 안 옆 칸 | header 아래·main 위 세로 순서 |
|
|
113
|
+
|
|
114
|
+
## 함정
|
|
115
|
+
|
|
116
|
+
- [계약 문서](../../layout.md)는 Native `skipLinkLabel`이 deprecated로 남아 있다고 적지만 1.12.1 Native `LayoutProps`에는 없다.
|
|
117
|
+
- Native persistent 사이드바는 옆으로 놓이지 않고 세로로 쌓인다. 넓은 화면 2단 배치가 필요하면 제품이 main 안에서 구성한다.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Link
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Link](../../link.md), recipe `linkRecipe`(`src/component-recipes.ts`), 목적지 검증 `src/link.ts`
|
|
9
|
+
- 스토리북: `배포/컴포넌트/동작/링크`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
사용자가 복사하거나 새 탭으로 열 수 있는 **목적지**로 이동할 때 쓴다. 앱 안 경로(`/profile`, `?tab=stats`,
|
|
14
|
+
`#details`)와 외부 URL(`https`, `http`, `mailto`, `tel`)이 여기에 속한다. 문장 안 링크와 혼자 서는 링크 둘 다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 같은 화면 안의 행동(저장, 재시도, 뒤로, 인증 확인) | [Button](button.md) (`tone="link"` 포함) |
|
|
21
|
+
| 아이콘만 있는 행동 | [IconButton](icon-button.md) |
|
|
22
|
+
| 경로 계층 표시 | [Breadcrumb](breadcrumb.md) |
|
|
23
|
+
| 같은 페이지 안 구획 목차 | [Anchor](anchor.md) |
|
|
24
|
+
| 목록 한 줄 전체가 목적지 | [ListRow](list-row.md) (Web `href`) |
|
|
25
|
+
| 갈 수 없는 목적지 | [Text](text.md) (비활성 링크를 만들지 않는다) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `Link` | 기본 | `@hjmds/react`, `/actions` | `@hjmds/react-native`, `/actions`, `/bottom-cta` |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web — Next.js Link는 renderAnchor로 연결
|
|
37
|
+
import NextLink from "next/link";
|
|
38
|
+
import { Link } from "@hjmds/react/actions";
|
|
39
|
+
|
|
40
|
+
<Link href="/settings/privacy" variant="standalone"
|
|
41
|
+
renderAnchor={({ href, ...rest }) => <NextLink href={href ?? "/"} {...rest} />}>
|
|
42
|
+
{t("settings.privacy")}
|
|
43
|
+
</Link>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Native — 목적지는 descriptor, 실제 이동은 제품 router, 아이콘 glyph는 renderIcon
|
|
48
|
+
import { Linking } from "react-native";
|
|
49
|
+
import { Link } from "@hjmds/react-native/actions";
|
|
50
|
+
import { createLucideGlyph } from "@hjmds/react-native/icon-lucide";
|
|
51
|
+
import { ChevronRight } from "lucide-react-native";
|
|
52
|
+
|
|
53
|
+
const renderGlyph = createLucideGlyph({ chevronEnd: ChevronRight }); // 제품이 고른 glyph
|
|
54
|
+
|
|
55
|
+
<Link
|
|
56
|
+
descriptor={{
|
|
57
|
+
label: t("profile.view"),
|
|
58
|
+
destination: { kind: "internal", href: `/u/${id}` },
|
|
59
|
+
trailingIcon: { name: "chevronEnd" },
|
|
60
|
+
}}
|
|
61
|
+
renderIcon={renderGlyph}
|
|
62
|
+
onNavigate={(destination) =>
|
|
63
|
+
destination.kind === "internal" ? router.push(destination.href) : Linking.openURL(destination.href)}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 축과 기본값
|
|
68
|
+
|
|
69
|
+
| prop | 값 | 기본값 | 설명 |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| `tone`(Web) | `brand` · `neutral` | `brand` | — |
|
|
72
|
+
| `variant`(Web) | `inline` · `standalone` | `inline` | `inline`은 밑줄 항상, `standalone`은 밑줄 hover, 최소 44 target |
|
|
73
|
+
| `target`·`rel`(Web) | — | — | `target="_blank"`이고 `rel`이 없으면 `noreferrer noopener`를 붙인다 |
|
|
74
|
+
| `descriptor.label`·`accessibilityLabel`(Native) | 문자열 | — | 앞뒤 공백 없이 비어 있지 않아야 하고, `accessibilityLabel`은 보이는 label을 포함해야 한다 |
|
|
75
|
+
| `descriptor`(Native) | `{ label, accessibilityLabel?, destination, leadingIcon?, trailingIcon? }` | — | 허용된 key만 받는다. 모르는 key·명령 필드(`onPress` 등)는 `TypeError` |
|
|
76
|
+
| `descriptor.destination`(Native) | `{ kind: "internal" \| "external", href }` | — | internal `href`는 `/`·`?`·`#`로 시작, external은 허용 protocol(`https`·`http`·`mailto`·`tel`)의 절대 URL만(자격증명 포함 금지) |
|
|
77
|
+
| `descriptor.leadingIcon`·`trailingIcon`(Native) | `{ name: SemanticIconName }` | — | 크기·tone·굵기·방향은 넘길 수 없다(`linkRecipe`가 정함). 그리려면 `renderIcon`이 필요하다 |
|
|
78
|
+
| `onNavigate`(Native) | `(destination: LinkDestination) => void \| Promise<void>` | — | 필수. 검증된 `{ kind, href }` 새 객체를 받는다. router·`Linking` 호출은 제품이 한다 |
|
|
79
|
+
| `renderIcon`(Native) | `(props: { name, size, color, strokeWidth }) => ReactNode` | — | semantic name → glyph 경계. 보통 `createLucideGlyph`. 크기·색은 HJM이 넘긴 값을 그대로 쓴다 |
|
|
80
|
+
| `renderAnchor`(Web) | `(props: LinkRenderProps) => ReactElement` | — | 받은 props(`href`·`ref`·`className`·`data-*`·`aria-disabled`·`onClick`·`children`·병합된 `style`)를 framework Link에 그대로 넘긴다 |
|
|
81
|
+
| `layoutStyle` | 배치 key만 | — | 양쪽 있음. Web은 `renderAnchor`에도 병합된 `style`로 전달된다 |
|
|
82
|
+
| `style`(Native) | — | — | **deprecated**(1.13, 개발 모드 1회 경고, 다음 major 제거) — `layoutStyle`을 쓴다 |
|
|
83
|
+
|
|
84
|
+
- Native는 tone·variant가 없다. 항상 brand 색 · 밑줄 · `bodyLarge` 글자 · 최소 터치 target이다.
|
|
85
|
+
|
|
86
|
+
## 배치
|
|
87
|
+
|
|
88
|
+
| 항목 | 값 | 근거 |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| 크기 | `inline`은 문장 안에 들어가 줄높이를 따르고 최소 크기가 없다. 문장 밖에 혼자 놓는 링크는 `standalone`(Web)으로 둬 최소 44×44(`control.minTouchTarget`)를 확보한다. Native Link는 항상 최소 44×44다 | `design-contracts/src/component-recipes.ts`(`linkRecipe`), `react-native/src/internal/styles.ts` |
|
|
91
|
+
| 간격 | 앞뒤 그림과 문구 간격: Web `spacing.xxs` 4(`linkRecipe.gap`), Native `spacing.xs` 8. 여러 혼자 서는 링크를 나열할 때 간격은 바깥 Stack이 준다(링크 자체에 margin을 주지 않는다) | `react/src/styles.css`(`.hjm-link`), `react-native/src/actions.tsx`(Link) |
|
|
92
|
+
| 순서·정렬 | Native Link는 `alignSelf: "flex-start"`라 부모 폭을 채우지 않고 시작 쪽에 붙는다 | `react-native/src/actions.tsx`(Link) |
|
|
93
|
+
| 고정·스크롤 | — | — |
|
|
94
|
+
| 좁은 폭·큰 글자 | 긴 문구는 줄바꿈된다(Web `overflow-wrap: anywhere`, `max-inline-size: 100%`). 자르지 않는다 | `react/src/styles.css`(`.hjm-link`) |
|
|
95
|
+
|
|
96
|
+
## 꼭 지킬 것
|
|
97
|
+
|
|
98
|
+
- 라벨은 i18n 키로 넣는다. 접근성 이름을 따로 줄 때도 보이는 문구를 포함한다.
|
|
99
|
+
- navigation을 `onClick`/`onPress` callback으로 대신하지 않는다. Web은 실제 `href`를, Native는 `destination`을 둔다.
|
|
100
|
+
- 앞뒤 아이콘은 장식이다. 이름은 링크 문구가 소유한다.
|
|
101
|
+
- 색·밑줄·글자 크기를 덮지 않는다. 배치는 바깥 래퍼에서 한다.
|
|
102
|
+
|
|
103
|
+
## 플랫폼 차이
|
|
104
|
+
|
|
105
|
+
| 항목 | Web | Native |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| API | `<a>` 속성 + `tone`·`variant` | `descriptor` + `onNavigate` |
|
|
108
|
+
| router 연결 | `renderAnchor`(받은 props를 그대로 anchor에 전달) | `onNavigate(destination)` |
|
|
109
|
+
| 앞뒤 그림 | `leading`·`trailing` ReactNode | descriptor `leadingIcon`·`trailingIcon` + `renderIcon`(우선), 또는 `leading`·`trailing` ReactNode |
|
|
110
|
+
| `disabled` | 있음(`aria-disabled`, tabIndex -1) | — |
|
|
111
|
+
| `accessibilityHint` | — | 있음 |
|
|
112
|
+
|
|
113
|
+
## 함정
|
|
114
|
+
|
|
115
|
+
- Web `disabled` prop이 남아 있지만 [계약](../../link.md#목적지)은 비활성 링크 대신 plain Text를 요구한다. 쓰지 않는다.
|
|
116
|
+
- Native에서 descriptor 아이콘을 주고 `renderIcon`을 빼면 개발 모드에서 한 번 경고하고 아이콘을 그리지 않는다(throw하지 않음).
|
|
117
|
+
descriptor 아이콘과 `leading`/`trailing` node를 함께 주면 descriptor가 그 자리를 갖고 경고가 난다. 1.12까지는 Native가
|
|
118
|
+
descriptor 아이콘을 그리지 않았다.
|
|
119
|
+
- Native `style`은 deprecated다. 아직 recipe 뒤에 합쳐져 시각 값이 통과하므로 배치는 `layoutStyle`로만 준다.
|
|
120
|
+
- 현재 Native 스토리(`showcase/native/src/component-examples.tsx` LinkExample)는 descriptor 아이콘·`renderIcon` 예가 없고
|
|
121
|
+
문구가 i18n 키가 아니다. 아이콘 경로는 위 최소 사용 예로 확인한다.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ListDetailScreen
|
|
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
|
+
목록 pane은 상세가 열려 있는 동안 숨겨질 뿐 mounted 상태를 유지한다. 새로고침·추가 로딩 버튼 자리도 준다.
|
|
15
|
+
데이터·페이지 cursor·라우팅은 제품이 소유한다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 상세가 별도 route·URL이어야 함 | 제품 router + [ScreenLayout](screen-layout.md) 두 화면 |
|
|
22
|
+
| 목록만 있는 화면 | [ScreenLayout](screen-layout.md) + [List](list.md) |
|
|
23
|
+
| 상태 머신(로딩·오류·끝)이 있는 다음 페이지 footer | [LoadMore](load-more.md)를 `list` 안에 둔다 |
|
|
24
|
+
| 넓은 화면에서 목록·상세를 나란히 | [Splitter](splitter.md), [Layout](layout.md) |
|
|
25
|
+
| 상세를 화면 위에 띄움 | [Sheet](sheet.md), [SidePanel](side-panel.md) |
|
|
26
|
+
| 검색어·필터가 중심인 목록 | [SearchScreen](search-screen.md) |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `ListDetailScreen` | 목록 유지 + 상세 전환 화면 | `/screen-flows` | `/screen-flows` |
|
|
33
|
+
|
|
34
|
+
granular subpath로만 import 된다(루트 entry에 없음). 추가 peer는 없다.
|
|
35
|
+
|
|
36
|
+
## 최소 사용 예
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Web
|
|
40
|
+
import { List } from "@hjmds/react/display";
|
|
41
|
+
import { ListDetailScreen } from "@hjmds/react/screen-flows";
|
|
42
|
+
|
|
43
|
+
<ListDetailScreen
|
|
44
|
+
title={t("orders.title")}
|
|
45
|
+
list={<List label={t("orders.list")}>{rows}</List>}
|
|
46
|
+
{...(selected ? { detail: { title: selected.name, content: <OrderDetail order={selected} /> } } : {})}
|
|
47
|
+
back={{ label: t("common.back"), onAction: () => setSelected(null) }}
|
|
48
|
+
refresh={{ label: t("common.refresh"), onAction: refetch, pending: isRefetching }}
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Native
|
|
54
|
+
import { ListDetailScreen } from "@hjmds/react-native/screen-flows";
|
|
55
|
+
|
|
56
|
+
<ListDetailScreen title={t("orders.title")} list={orderList}
|
|
57
|
+
{...(selected ? { detail: { title: selected.name, content: <OrderDetail order={selected} /> } } : {})}
|
|
58
|
+
back={{ label: t("common.back"), onAction: closeDetail }}
|
|
59
|
+
loadMore={{ label: t("orders.more"), onAction: fetchNext, pending: isFetchingNext }} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 축과 기본값
|
|
63
|
+
|
|
64
|
+
| prop | 값 | 기본값 | 설명 |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `list` | `ReactNode` | 필수 | 목록 pane 본문. 상세가 열려도 mount를 유지한다 |
|
|
67
|
+
| `detail` | `{ title: string; content: ReactNode }` | 없음 | 있으면 상세 pane을 보이고 목록 pane을 숨긴다. 닫힌 상태는 prop을 빼서 표현한다(`undefined`를 넘기지 않는다) |
|
|
68
|
+
| `back` | `ScreenFlowAction`(`{ label, onAction(), disabled?, pending? }`) | 필수 | 상세 pane `leading` 자리의 ghost 버튼 |
|
|
69
|
+
| `refresh` | `ScreenFlowAction` | 없음 | 목록 pane `actions` 자리의 ghost 버튼. 주면 `actions`를 덮는다 |
|
|
70
|
+
| `loadMore` | `ScreenFlowAction` | 없음 | 목록 pane footer의 ghost 버튼 |
|
|
71
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | 없음 | Web·Native 모두 목록·상세 pane을 함께 담는 **바깥 틀**에 적용한다(안쪽 목록 ScreenLayout이 아니다). 목록/상세 전환과 상관없이 같은 루트에 남는다. 미게시(1.12.1 이후) |
|
|
72
|
+
| 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | 목록 pane에만 적용된다(`title` 필수, `state`, `scroll` 등) |
|
|
73
|
+
|
|
74
|
+
## 배치
|
|
75
|
+
|
|
76
|
+
| 항목 | 값 | 근거 |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 크기 | 목록 또는 상세 pane 하나만 표시하고 각 pane이 host 높이 100%(Native `flex: 1`)를 채운다; 각 pane은 ScreenLayout 폭(최대 720) | Web `height: 100%`, Native `Pane` |
|
|
79
|
+
| 간격 | 각 pane의 ScreenLayout padding `spacing.md` 16; 추가 간격 없음, 목록 행 간격은 `list`(제품·List) 소유 | `ScreenLayout`, `screen-flows.tsx` |
|
|
80
|
+
| 순서·정렬 | 목록 pane: 헤더(제목 → 새로고침) → 목록 → footer(더 보기); 상세 pane: 헤더(뒤로 → 제목) → `detail.content` | `ListDetailScreen` 렌더 순서 |
|
|
81
|
+
| 고정·스크롤 | 두 pane 모두 헤더 고정·본문 스크롤; 숨긴 목록 pane은 mount를 유지해 스크롤·입력이 남는다(Web `hidden`, Native `display: "none"`) | Web·Native `ListDetailScreen` |
|
|
82
|
+
| 좁은 폭·큰 글자 | 제목 열 최소 폭 120 × 글자 배율, 모자라면 새로고침 버튼이 다음 줄로 내려간다; 넓은 폭에서도 두 pane을 나란히 두지 않는다 | `screenPatternRecipe.headerMinWidth` |
|
|
83
|
+
|
|
84
|
+
## 꼭 지킬 것
|
|
85
|
+
|
|
86
|
+
- 라벨은 모두 i18n 키로 넣는다.
|
|
87
|
+
- `state`(로딩·빈·오류·제한)는 목록 pane에만 적용된다. 상세의 로딩·오류는 `detail.content` 안에서 표시한다.
|
|
88
|
+
- Web은 상세 진입 시 뒤로 버튼에 포커스를 옮기고 복귀 시 목록의 원래 요소로 돌린다. 요소가 삭제되었으면 목록 본문으로 이동한다.
|
|
89
|
+
처음부터 `detail`이 있는 채로 mount되면(딥링크) 사용자 전환이 아니므로 포커스를 옮기지 않는다. 그때 포커스는 제품 router가 정한다.
|
|
90
|
+
- 상세 열림 상태는 제품이 가진다. OS 뒤로가기·브라우저 뒤로가기와 `back`을 제품 router에서 함께 연결한다.
|
|
91
|
+
|
|
92
|
+
## 플랫폼 차이
|
|
93
|
+
|
|
94
|
+
| 항목 | Web | Native |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `layoutStyle` 적용 위치 | 두 pane을 담는 바깥 `div`(기본 `height: 100%`) | 두 pane을 담는 바깥 `View`(기본 `flex: 1`) |
|
|
97
|
+
| 포커스 이동 | 상세 진입·복귀 때 이동, 딥링크 첫 mount는 이동 없음 | 이동하지 않는다 |
|
|
98
|
+
|
|
99
|
+
## 함정
|
|
100
|
+
|
|
101
|
+
- `refresh`를 주면 ScreenLayout `actions`는 무시된다. 둘 다 필요하면 `refresh` 대신 `actions`에 직접 조합한다.
|
|
102
|
+
- 상세 pane은 `title`과 `back`(leading)만 받는다. 상세 쪽 `description`·`actions`·footer 자리는 없다.
|
|
103
|
+
- `loadMore`는 상태 없는 버튼이다. 끝·오류·중복 요청 방지가 필요하면 LoadMore를 쓴다.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# ListRow
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [ListRow](../../list-row.md), recipe `listRowRecipe`(`src/component-recipes.ts`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/목록 행`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
목록의 한 줄에 쓴다. 제목, 선택 설명, 앞(아바타·아이콘)·뒤(값·chevron·배지) 슬롯으로 구성되고,
|
|
14
|
+
누르면 상세로 가거나 행동을 하는 행, 정보만 보여 주는 행 모두 여기에 속한다. 행 사이 구분선은 감싸는
|
|
15
|
+
[List](list.md)가 소유한다.
|
|
16
|
+
|
|
17
|
+
| 화면 형태 | 쓸 것 |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| 행 한 줄 | `ListRow` |
|
|
20
|
+
| 행 묶음과 구분선 | [List](list.md) |
|
|
21
|
+
| 고정 높이 행 수백~수천 개 | [VirtualList](virtual-list.md)의 `renderItem` 안에서 `ListRow` |
|
|
22
|
+
| 다음 페이지 요청 | [LoadMore](load-more.md) |
|
|
23
|
+
|
|
24
|
+
## 쓰지 않을 때
|
|
25
|
+
|
|
26
|
+
| 상황 | 대신 쓸 것 |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| 이미지·여러 행동이 있는 카드 | [Card](card.md) |
|
|
29
|
+
| 이름·값 쌍 | [DescriptionList](description-list.md) |
|
|
30
|
+
| 체크·라디오가 행 자체 | [Checkbox](checkbox.md), [Radio](radio.md) |
|
|
31
|
+
| 표의 행 | [DataTable](data-table.md) |
|
|
32
|
+
| 밀어서 드러나는 행 행동 | [SwipeActions](swipe-actions.md) |
|
|
33
|
+
|
|
34
|
+
## 공개 이름과 import
|
|
35
|
+
|
|
36
|
+
| 이름 | 역할 | Web | Native |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `ListRow` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
|
|
39
|
+
|
|
40
|
+
## 최소 사용 예
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// Web
|
|
44
|
+
import { Icon, ListRow } from "@hjmds/react/display";
|
|
45
|
+
|
|
46
|
+
<ListRow title={t("settings.language")} description={languageName} href="/settings/language"
|
|
47
|
+
trailing={<Icon name="chevronEnd" decorative />} />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Native
|
|
52
|
+
import { Avatar, ListRow } from "@hjmds/react-native/data-display";
|
|
53
|
+
|
|
54
|
+
<ListRow title={member.name} description={member.role}
|
|
55
|
+
leading={<Avatar name={member.name} decorative />} leadingShape="circle"
|
|
56
|
+
trailingText={t("member.joined", { date })} onPress={() => openMember(member.id)} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 축과 기본값
|
|
60
|
+
|
|
61
|
+
| prop | 값 | 기본값 | 설명 |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `density` | `compact` · `comfortable` · `relaxed` · `spacious` | `comfortable` | 최소 높이는 설명 유무(한 줄·두 줄)와 함께 정해진다 |
|
|
64
|
+
| `leadingShape` | `square` · `circle` | `square` | leading 프레임 크기는 recipe가 그린다 |
|
|
65
|
+
| `selected` | `true` · `false` | `false` | — |
|
|
66
|
+
| `disabled` | `true` · `false` | `false` | — |
|
|
67
|
+
| `href`(Web) | 문자열 | — | 있으면 `<a>` 행 |
|
|
68
|
+
| `onClick`(Web) | `(event: MouseEvent<HTMLElement>) => void` | — | 있으면 `<button>` 행 |
|
|
69
|
+
| `onPress`(Native) | `(event: GestureResponderEvent) => void` | — | 있으면 누를 수 있는 행 |
|
|
70
|
+
| `loading`·`loadingLabel`(Web) | `boolean` · 현지화 문자열 | `false` · — | 같은 슬롯 모양의 자리표시 행. `loadingLabel`이 상태로 읽힌다 |
|
|
71
|
+
| `trailingAction`(Native) | ReactNode | — | 행 명령 옆에 따로 그리는 별도 target(IconButton 등). 자기 `onPress`를 가진다 |
|
|
72
|
+
| `layoutStyle` | 배치 key만 | — | 행 루트 배치. Native는 슬롯 배치용 `leadingStyle`·`contentStyle`·`titleRowStyle`·`trailingStyle`·`trailingActionStyle`(모두 배치 key만)도 받는다 |
|
|
73
|
+
| `titleStyle`·`descriptionStyle`(Native) | — | — | **deprecated**(1.13, 개발 모드 1회 경고, 다음 major 제거) — `density`·`selected`, 글자는 listRowRecipe가 정한다 |
|
|
74
|
+
|
|
75
|
+
- 상호작용: Web은 `href`면 `<a>`(selected는 `aria-current="page"`), `onClick`이면 `<button>`(selected는 `aria-pressed`),
|
|
76
|
+
둘 다 없으면 `<div>`. Native는 `onPress`가 있으면 누를 수 있는 행이 된다.
|
|
77
|
+
|
|
78
|
+
## 배치
|
|
79
|
+
|
|
80
|
+
| 항목 | 값 | 근거 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| 크기 | 폭은 부모를 가득 채운다(Web `inline-size: 100%`). leading 프레임은 40×40(`leadingSize`), `circle`이면 `radius.full`. trailing 아이콘은 `glyph.sm` 20. 최소 높이는 아래 density 표 | `design-contracts/src/component-recipes.ts`(`listRowRecipe`), `design-contracts/src/foundations.ts`(`layout.rowHeight`) |
|
|
83
|
+
| 간격 | leading·content·trailing 사이 `spacing.sm` 12(`listRowRecipe.gap`). 좌우 여백은 모두 `spacing.xs` 8, 위아래 여백은 아래 density 표. 행에 margin을 주지 않는다 | `design-contracts/src/component-recipes.ts`(`listRowRecipe`), `react/src/styles.css`(`.hjm-list-row`) |
|
|
84
|
+
| 순서·정렬 | leading → content(제목·설명) → trailing. Native `trailingAction`은 행 명령 바깥 오른쪽(끝)에 따로 놓이고 끝 여백 `spacing.xs` 8을 갖는다. 행 사이 구분선·바깥 둥근 배경은 [List](list.md)가 그린다 | `react-native/src/data-display.tsx`(ListRow) |
|
|
85
|
+
| 고정·스크롤 | — | — |
|
|
86
|
+
| 좁은 폭·큰 글자 | 긴 제목·설명은 줄바꿈되고(`overflow-wrap: anywhere`) 행 높이가 늘어난다. 큰 글자에서도 자르지 않는다 | `react/src/styles.css`(`.hjm-list-row`) |
|
|
87
|
+
|
|
88
|
+
| density | 한 줄 | 두 줄 | 위아래 여백 |
|
|
89
|
+
| --- | --- | --- | --- |
|
|
90
|
+
| `compact` | 44 | 60 | `spacing.xxs` 4 |
|
|
91
|
+
| `comfortable` | 56(`layout.rowHeight.singleLine`) | 68 | `spacing.xs` 8 |
|
|
92
|
+
| `relaxed` | 64 | 76 | `spacing.sm` 12 |
|
|
93
|
+
| `spacious` | 72 | 84 | `spacing.md` 16 |
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
┌───────────────────────────────────────────────┐
|
|
97
|
+
│8│[40×40]│12│ 제목(bodyLarge bold) │12│ 값 ›│8│
|
|
98
|
+
│ │leading│ │ 설명(body, secondary) │ │trail│ │
|
|
99
|
+
└───────────────────────────────────────────────┘
|
|
100
|
+
최소 높이: comfortable 한 줄 56 · 두 줄 68
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 꼭 지킬 것
|
|
104
|
+
|
|
105
|
+
- 제목·설명은 i18n 키로 넣는다. leading의 사진·아이콘은 장식으로 두고 의미는 제목이 말한다.
|
|
106
|
+
- 행 안에 다른 버튼을 넣지 않는다. Native는 별도 target을 `trailingAction`에 둔다(행 명령 옆에 따로 그린다).
|
|
107
|
+
- 배치는 `layoutStyle`로 한다. 높이·여백·배경을 덮지 않는다. 밀도는 `density`로 바꾼다.
|
|
108
|
+
- 아바타 프레임을 제품이 다시 그리지 않는다. `leadingShape`를 쓴다.
|
|
109
|
+
|
|
110
|
+
## 플랫폼 차이
|
|
111
|
+
|
|
112
|
+
| 항목 | Web | Native |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| 제목·설명 타입 | `ReactNode` | `string` |
|
|
115
|
+
| 자리표시 행 | `loading` + `loadingLabel` | — |
|
|
116
|
+
| 링크 행 | `href` | 없음(`onPress`에서 router 호출) |
|
|
117
|
+
| 제목 옆 메타·별도 뒤 행동·뒤 문구 | — | `titleMetadata`, `trailingAction`, `trailingText` |
|
|
118
|
+
| 접근성 이름 조합 | 요소 내용 | `accessibilityLabel` 없으면 제목·`metadataLabel`·설명·`trailingLabel`을 이어 붙임 |
|
|
119
|
+
| 기본 `density` | provider density가 compact면 `compact` | 항상 `comfortable` |
|
|
120
|
+
|
|
121
|
+
## 함정
|
|
122
|
+
|
|
123
|
+
- Web 로딩 행은 슬롯의 **존재**로 모양을 정하고 문구는 무시한다. 실제 행과 같은 슬롯을 넘겨야 높이가 맞는다.
|
|
124
|
+
- Native `titleStyle`·`descriptionStyle`은 deprecated지만 아직 TextStyle 전체를 받아 색·굵기도 바뀐다. 새 코드에서 쓰지 않는다.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# List
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: recipe `listRecipe`(`src/component-recipes.ts`), TaskList 계약: [Task list](../../task-list.md)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/목록` · `배포/컴포넌트/입력/할 일 목록`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
이미 다 불러온, 개수가 많지 않은 행들을 이름 있는 목록 하나로 묶을 때 쓴다. List는 목록 의미
|
|
14
|
+
(`role="list"`)와 행 사이 구분선·묶음 배경만 소유하고, 각 행의 모양은 안에 넣는 [ListRow](list-row.md)가 정한다.
|
|
15
|
+
체크로 완료를 표시하는 할 일 목록은 `TaskList`를 쓴다.
|
|
16
|
+
|
|
17
|
+
### 목록 계열 고르기
|
|
18
|
+
|
|
19
|
+
| 화면 형태 | 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 수십 개 이하의 행을 한 번에 그림(설정, 계정 메뉴, 검색 결과 한 페이지) | `List` + `ListRow` |
|
|
22
|
+
| 행 한 줄의 제목·설명·앞뒤 슬롯 | [ListRow](list-row.md) |
|
|
23
|
+
| 높이가 고정된 행 수백~수천 개를 창 안에서 스크롤 | [VirtualList](virtual-list.md) |
|
|
24
|
+
| 이어지는 목록의 다음 페이지 요청 footer | [LoadMore](load-more.md) (List·VirtualList 아래에 둔다) |
|
|
25
|
+
|
|
26
|
+
## 쓰지 않을 때
|
|
27
|
+
|
|
28
|
+
| 상황 | 대신 쓸 것 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| 행이 많아 한 번에 그리면 느림 | [VirtualList](virtual-list.md) |
|
|
31
|
+
| 열이 있는 표 | [DataTable](data-table.md) |
|
|
32
|
+
| 높이가 다른 카드 격자 | [Masonry](masonry.md), [Grid](grid.md) |
|
|
33
|
+
| 이름·값 쌍 나열 | [DescriptionList](description-list.md) |
|
|
34
|
+
| 여러 항목을 골라 확정 | [CheckboxGroup](checkbox-group.md) |
|
|
35
|
+
| 순서 바꾸기 | [SortableCollection](sortable-collection.md) |
|
|
36
|
+
| 시간순 사건 | [Timeline](timeline.md) |
|
|
37
|
+
|
|
38
|
+
## 공개 이름과 import
|
|
39
|
+
|
|
40
|
+
| 이름 | 역할 | Web | Native |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| `List` | 기본 | `@hjmds/react`, `/display` | `@hjmds/react-native`, `/data-display` |
|
|
43
|
+
| `TaskList` | 확장 — 완료 체크 목록(optional) | `/task-list` | `/task-list` |
|
|
44
|
+
|
|
45
|
+
`TaskList`는 granular subpath로만 import 된다. 추가 peer는 없다. 순서 바꾸기를 합성하면 SortableCollection의 peer가 필요하다.
|
|
46
|
+
|
|
47
|
+
## 최소 사용 예
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Web
|
|
51
|
+
import { List, ListRow } from "@hjmds/react/display";
|
|
52
|
+
|
|
53
|
+
<List label={t("settings.account")} appearance="grouped">
|
|
54
|
+
<ListRow key="email" title={t("settings.email")} description={email} href="/settings/email" />
|
|
55
|
+
<ListRow key="logout" title={t("settings.logout")} onClick={logout} />
|
|
56
|
+
</List>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// Native
|
|
61
|
+
import { TaskList } from "@hjmds/react-native/task-list";
|
|
62
|
+
import { Text } from "@hjmds/react-native/primitives";
|
|
63
|
+
|
|
64
|
+
<TaskList label={t("todo.today")} items={tasks}
|
|
65
|
+
onCompletedChange={(id, completed) => saveTask(id, completed)}
|
|
66
|
+
emptyContent={<Text tone="muted">{t("todo.empty")}</Text>} />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 축과 기본값
|
|
70
|
+
|
|
71
|
+
| prop | 값 | 기본값 | 설명 |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| `label` | 현지화 문자열(필수) | — | 목록의 접근성 이름. 비면 `TypeError` |
|
|
74
|
+
| `separator` | `indented` · `full` · `none` | `indented` | `indented`는 leading 슬롯 폭만큼 들여씀 |
|
|
75
|
+
| `appearance` | `plain` · `grouped` | `plain` | `grouped`는 배경과 둥근 모서리로 한 덩어리 |
|
|
76
|
+
| `layoutStyle` | 배치 key만 | — | 목록 루트 배치. Native `style`은 **deprecated**(1.13, 개발 모드 1회 경고, 다음 major 제거) — `layoutStyle`, 모양은 `appearance`·`separator` |
|
|
77
|
+
| TaskList `items` | `readonly TaskItem[]` — `{ id, label, completed, description?, disabled? }` | — | `id`·`label`이 비거나 `id`가 중복되거나 `completed`가 boolean이 아니면 `TypeError` |
|
|
78
|
+
| TaskList `onCompletedChange` | `(id: string, completed: boolean) => void` | — | 필수. 체크 하나마다 한 번. 목록 순서·저장은 제품이 정한다 |
|
|
79
|
+
| TaskList `disabled` | `true` · `false` | `false` | 전체 체크박스를 끈다 |
|
|
80
|
+
| TaskList `emptyContent` | ReactNode | — | `items`가 비면 `List` 안에 그린다 |
|
|
81
|
+
| TaskList `renderCollection` | `(context: { items, renderItem: (item: TaskItem) => ReactNode }) => ReactNode` | — | SortableCollection을 합성할 수 있다. 이때 목록 루트는 제품이 그린다 |
|
|
82
|
+
| TaskList `layoutStyle`(Web) | 배치 key만 | — | List 루트 배치. `renderCollection`을 쓰면 무시되고 제품 루트를 배치한다. Native TaskList는 `layoutStyle`이 없다 |
|
|
83
|
+
|
|
84
|
+
- List 자체에는 이벤트 콜백이 없다. 행의 누름은 각 [ListRow](list-row.md)가 받는다.
|
|
85
|
+
|
|
86
|
+
## 배치
|
|
87
|
+
|
|
88
|
+
| 항목 | 값 | 근거 |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| 크기 | 화면 본문 폭을 채우는 세로 묶음이다. `grouped`는 배경 `--hjm-color-bg`와 모서리 `radius.lg` 16으로 한 덩어리가 된다 | `design-contracts/src/foundations.ts`(`radius`), `react/src/styles.css`(`.hjm-list`) |
|
|
91
|
+
| 간격 | 행 사이 간격은 없고 구분선 1px(`--hjm-color-border`)만 들어간다. `indented` 구분선의 시작 들여쓰기: 52(leading 40 + `spacing.sm` 12, `listRecipe.separators.indented`, 두 플랫폼). 끝 쪽은 둘 다 0. 덩어리 바깥 여백·섹션 사이 간격은 감싸는 Stack이 준다(페이지 좌우 여백은 `layout.pagePadding`, 섹션 간격은 `layout.sectionGap` = `spacing.xl` 24) | `design-contracts/src/component-recipes.ts`(`listRecipe`), `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-list`), `react-native/src/data-display.tsx`(List) |
|
|
92
|
+
| 순서·정렬 | 다음 페이지 요청은 목록 바로 아래에 [LoadMore](load-more.md)를 둔다 | — |
|
|
93
|
+
| 고정·스크롤 | List 자체는 스크롤하지 않는다. 화면 스크롤 안에 둔다 | `react-native/src/data-display.tsx`(List) |
|
|
94
|
+
| 좁은 폭·큰 글자 | — | — |
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
┌─ List grouped (radius.lg 16, --hjm-color-bg) ─────────┐
|
|
98
|
+
│ ListRow │
|
|
99
|
+
│ ────────────────────────────────────────── │ ← indented 구분선(시작 들여씀)
|
|
100
|
+
│ ListRow │
|
|
101
|
+
│ ────────────────────────────────────────── │
|
|
102
|
+
│ ListRow │
|
|
103
|
+
└──────────────────────────────────────────────────┘
|
|
104
|
+
[ LoadMore ]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 꼭 지킬 것
|
|
108
|
+
|
|
109
|
+
- 자식 하나가 행 하나다. 자식마다 안정된 `key`를 준다(없으면 순서 index로 key를 만든다).
|
|
110
|
+
- 구분선을 행에 직접 그리지 않는다. `separator`로 정한다.
|
|
111
|
+
- TaskList는 체크해도 순서를 바꾸거나 지우지 않는다. 저장·실패·되돌리기는 제품이 `onCompletedChange`에서 처리한다.
|
|
112
|
+
|
|
113
|
+
## 플랫폼 차이
|
|
114
|
+
|
|
115
|
+
| 항목 | Web | Native |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| 구조 | `role="list"` + 자식마다 `role="listitem"` | `accessibilityRole="list"` |
|
|
118
|
+
| 구분선 | CSS(`data-separator`) | 행 사이 1px `View` |
|
|
119
|
+
| 배치 | `layoutStyle`(+ `className`) | `layoutStyle`(`style`은 deprecated) |
|