@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,104 @@
|
|
|
1
|
+
# SidePanel
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [SidePanel](../../side-panel.md), `src/side-panel.ts`(`sidePanelRecipe`, `sidePanelBehaviorDefaults`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/오버레이/측면 패널`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
Web 화면 가장자리(시작·끝)에 도킹되어 밀려 나오는 보조 패널에 쓴다. 목록 옆 상세 편집, 필터,
|
|
14
|
+
보조 내비게이션이 전형이다. 기본은 모달(초점 가둠·스크롤 잠금)이고, 뒤 페이지를 계속 조작해야 하면
|
|
15
|
+
비모달(`dismissPolicy.modal: false`)로 연다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 모바일·Native, 하단에서 올라오는 작업 패널 | [Sheet](sheet.md) |
|
|
22
|
+
| 짧은 확인·결정 | [AlertDialog](alert-dialog.md), [Dialog](dialog.md) |
|
|
23
|
+
| 항상 보이는 앱 왼쪽 내비게이션 | [Sidebar](sidebar.md) |
|
|
24
|
+
| 트리거 옆 작은 내용 | [Popover](popover.md) |
|
|
25
|
+
| 고정 2단 배치(크기 조절) | [Splitter](splitter.md), [Layout](layout.md) |
|
|
26
|
+
|
|
27
|
+
## 공개 이름과 import
|
|
28
|
+
|
|
29
|
+
| 이름 | 역할 | Web | Native |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `SidePanel` | 기본 | `@hjmds/react`, `/side-panel` | — (계약상 unsupported) |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web — 모달(기본)
|
|
37
|
+
import { Button } from "@hjmds/react/actions";
|
|
38
|
+
import { SidePanel } from "@hjmds/react/side-panel";
|
|
39
|
+
|
|
40
|
+
<SidePanel open={open} onOpenChange={(next) => setOpen(next)}
|
|
41
|
+
title={t("orders.filter.title")} closeLabel={t("common.close")}
|
|
42
|
+
footer={<Button onClick={apply}>{t("orders.filter.apply")}</Button>}>
|
|
43
|
+
<OrderFilters value={draft} onChange={setDraft} />
|
|
44
|
+
</SidePanel>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// Web — 비모달: 뒤 목록을 계속 조작
|
|
49
|
+
import { SidePanel } from "@hjmds/react/side-panel";
|
|
50
|
+
|
|
51
|
+
<SidePanel open={open} onOpenChange={(next) => setOpen(next)} edge="end" size="wide"
|
|
52
|
+
title={t("orders.detail.title")} closeLabel={t("common.close")}
|
|
53
|
+
dismissPolicy={{ modal: false, dismissible: true, dismissWhileBusy: false, escapeDismiss: true }}>
|
|
54
|
+
<OrderDetail id={selectedId} />
|
|
55
|
+
</SidePanel>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Native: 없음. Native는 [Sheet](sheet.md)를 쓴다.
|
|
59
|
+
|
|
60
|
+
## 축과 기본값
|
|
61
|
+
|
|
62
|
+
| prop | 값 | 기본값 | 설명 |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| `edge` | `start` · `end` | `end` | 논리 방향이라 RTL에서 뒤집힌다 |
|
|
65
|
+
| `size` | `compact` 320 · `regular` 400 · `wide` 560 | `regular` | 패널 폭 |
|
|
66
|
+
| `dismissPolicy` | 전체 객체 `{ modal: true, dismissible, dismissWhileBusy, escapeDismiss, outsideDismiss }` 또는 `{ modal: false, dismissible, dismissWhileBusy, escapeDismiss }` | `{ modal: true, dismissible: true, dismissWhileBusy: false, escapeDismiss: true, outsideDismiss: true }` | `Partial`이 아니라 전체 객체를 넘긴다. `modal: false`에는 `outsideDismiss`를 쓸 수 없다(타입 오류) |
|
|
67
|
+
| `open` · `defaultOpen` | `boolean` | 비제어 `false` | 제어하면 `onOpenChange` 필수, 비제어면 `trigger` 필수 |
|
|
68
|
+
| `onOpenChange` | `(open: boolean, detail: { reason }) => void` | — | reason: `trigger` · `close-action` · `escape` · `outside` · `programmatic` |
|
|
69
|
+
| `onDismissComplete` | `(detail: { reason }) => void` | — | 패널이 실제로 사라진 뒤 닫힘마다 한 번 |
|
|
70
|
+
| `title` · `closeLabel` | 문구 | 필수 | `title`은 `ReactNode`, `closeLabel`은 `string` |
|
|
71
|
+
| `busy` | `boolean` | `false` | 사용자 닫기를 막는다 |
|
|
72
|
+
| `modalPriority` | 숫자 | `0` | 모달 스택 순서 |
|
|
73
|
+
| `description` · `footer` | `ReactNode` | — | `footer`는 스크롤 밖 하단 고정. 적용·저장 행동은 여기 |
|
|
74
|
+
| `initialFocusRef` · `returnFocusRef` | `RefObject<HTMLElement \| null>` | — | 열릴 때·닫힐 때 포커스 |
|
|
75
|
+
| `portalContainer` · `className` | `HTMLElement` · 문자열 | — | `layoutStyle`은 받지 않는다(Web 제외 15개 중 하나). 폭은 `size`, 위치는 `edge` |
|
|
76
|
+
|
|
77
|
+
## 배치
|
|
78
|
+
|
|
79
|
+
| 항목 | 값 | 근거 |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| 크기 | 높이 화면 전체(`100dvh`), 폭 `size`(`compact` 320 · `regular` 400 · `wide` 560), radius 0으로 가장자리에 붙는다. 머리 최소 높이 44(`control.minTouchTarget`) | `sidePanelRecipe.sizes`·`content.radius`·`header`, `.hjm-side-panel` |
|
|
82
|
+
| 간격 | 머리 위 `spacing.sm` 12 · 좌우 `spacing.lg` 20, 제목–닫기 `spacing.md` 16. 본문 좌우 `spacing.lg` 20 · 상하 `spacing.md` 16, 자식 간격 `spacing.md` 16. footer 위 `spacing.sm` 12 · 좌우 20, 버튼 사이 `spacing.sm` 12 | `.hjm-side-panel__header`·`__body`·`__footer` |
|
|
83
|
+
| 순서·정렬 | `edge` 쪽 가장자리. 머리(제목·설명 + 끝 쪽 닫기) → 본문 → `footer`. footer는 오른쪽 정렬 가로 줄 [보조][주] | `.hjm-side-panel-positioner`, `.hjm-side-panel__footer` |
|
|
84
|
+
| 고정·스크롤 | 본문만 스크롤, 머리·footer 고정. footer 아래 여백 `max(spacing.sm 12, 아래 안전 영역)`. 비모달은 scrim 없이 `position: fixed`로 떠서 뒤 페이지 일부를 덮는다. 가려지면 안 되면 [Layout](layout.md)·[Splitter](splitter.md)로 고정 2단 | `.hjm-side-panel[data-modal="false"]` |
|
|
85
|
+
| 좁은 폭·큰 글자 | 폭이 패널보다 좁으면 화면 폭 전체(`min(size, 100%)`). footer는 줄을 바꾼다. 모바일 폭이 주 대상이면 [Sheet](sheet.md) | `.hjm-side-panel` |
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
edge="end", 모달 비모달: scrim 없음, 뒤 페이지 조작 가능
|
|
89
|
+
┌──────────────────────┬───────────────┐ ┌──────────────────────┬───────────────┐
|
|
90
|
+
│ (scrim, 누르면 닫힘) │ 제목 [×] │←고정│ 목록(조작 가능) │ 상세 [×] │
|
|
91
|
+
│ │───────────────│ │ │───────────────│
|
|
92
|
+
│ │ 본문 ↕ 스크롤 │ │ │ 본문 ↕ │
|
|
93
|
+
│ │───────────────│ │ │ │
|
|
94
|
+
│ │ [취소] [적용] │←고정│ │ │
|
|
95
|
+
└──────────────────────┴───────────────┘ └──────────────────────┴───────────────┘
|
|
96
|
+
← size 320/400/560 →
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 꼭 지킬 것
|
|
100
|
+
|
|
101
|
+
- 모달 SidePanel은 Dialog·Sheet와 같은 모달 스택에 들어간다. 제품이 따로 focus trap·스크롤 잠금을 얹지 않는다.
|
|
102
|
+
- 비모달은 backdrop이 없고 Escape는 패널 안에서만 듣는다. 닫기 수단(닫기 버튼·Escape)을 막지 않는다.
|
|
103
|
+
- 저장 중에는 `busy`로 사용자 닫기를 막는다. owner가 `open=false`로 닫는 것은 항상 허용된다(`programmatic`).
|
|
104
|
+
- 문구(`title`, `closeLabel`)는 i18n 키로 넣는다. 폭·radius·색을 `className`으로 덮지 않는다. 폭은 `size`로 고른다.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Sidebar
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Sidebar](../../sidebar.md), `src/sidebar.ts`(`sidebarRecipe`, `validateSidebarDescriptor`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/탐색/사이드바`, `배포/컴포넌트/탐색/사이드바 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
데스크톱 Web의 세로 내비게이션에 쓴다. 관리자 화면, 문서, 작업 도구처럼 넓은 화면 왼쪽에
|
|
14
|
+
그룹이 있는 긴 목적지 목록을 두고, 필요하면 아이콘 레일로 접는다. 항목은 링크다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 모바일·Native의 3~5개 최상위 목적지 | [BottomNavigation](bottom-navigation.md) |
|
|
21
|
+
| 앱 셸의 자리(폭·순서·반응형) | [Layout](layout.md) — Sidebar는 그 자리에 넣는 내용 |
|
|
22
|
+
| 열고 닫는 보조 패널(필터·상세) | [SidePanel](side-panel.md) |
|
|
23
|
+
| 상단 가로 메뉴 | [Menubar](menubar.md), [TopBar](top-bar.md) |
|
|
24
|
+
| 같은 화면 안 패널 전환 | [Tabs](tabs.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `Sidebar` | 기본 | `@hjmds/react`, `/sidebar` | — |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { Sidebar } from "@hjmds/react/sidebar";
|
|
37
|
+
|
|
38
|
+
<Sidebar
|
|
39
|
+
descriptor={{
|
|
40
|
+
accessibilityLabel: t("nav.main"),
|
|
41
|
+
currentId: currentRouteId,
|
|
42
|
+
groups: [
|
|
43
|
+
{ id: "work", label: t("nav.group.work"), items: [
|
|
44
|
+
{ id: "orders", label: t("nav.orders"), destination: { kind: "internal", href: "/orders" }, badgeCount: pending },
|
|
45
|
+
{ id: "reports", label: t("nav.reports"), destination: { kind: "internal", href: "/reports" } },
|
|
46
|
+
] },
|
|
47
|
+
],
|
|
48
|
+
}}
|
|
49
|
+
collapseLabels={{ collapse: t("nav.collapse"), expand: t("nav.expand") }}
|
|
50
|
+
renderIcon={(item) => <NavGlyph id={item.id} />}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Native: 없음. 폰은 BottomNavigation, 태블릿 split view는 navigator가 맡는다.
|
|
55
|
+
|
|
56
|
+
## 축과 기본값
|
|
57
|
+
|
|
58
|
+
타입 `SidebarDescriptor`는 `@hjmds/design-contracts/components/sidebar`에 있다.
|
|
59
|
+
|
|
60
|
+
| prop | 값 | 기본값 | 설명 |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| `descriptor` | `{ accessibilityLabel: string; currentId: Id \| null; groups: readonly { id, label?, items }[] }` | 필수 | 그룹 `label`이 없으면 구분선만 있는 묶음 |
|
|
63
|
+
| 항목 | `{ id, label: string, destination?: { kind: "internal" \| "external"; href: string }, badgeCount?: number, disabled? }` | — | `destination`이 없으면 `href` 없는 `<a>`(아래 함정) |
|
|
64
|
+
| `collapsed` · `defaultCollapsed` | `boolean` | `false` | 접힘 폭 72, 펼침 260 |
|
|
65
|
+
| `onCollapsedChange` | `(collapsed: boolean) => void` | — | 접기 버튼을 누를 때 |
|
|
66
|
+
| `collapseLabels` | `{ collapse: string; expand: string }` | — | 줄 때만 접기 버튼을 그린다 |
|
|
67
|
+
| `appearance` | `standard` · `bounce` · `hook` · `proximity` | `standard` | 장식만 바뀌고 링크 영역·키보드 순서는 같다. provider의 동작 줄이기에서는 움직임이 꺼지고, provider가 없으면 움직임 없이 그린다 |
|
|
68
|
+
| `onNavigate` | `(id: Id) => void` | — | 항목을 누를 때 알림. 이동은 `href`가 한다 |
|
|
69
|
+
| `renderIcon` | `(item) => ReactNode` | — | 접힌 레일에 필수 |
|
|
70
|
+
| `renderBadge` | `(count: number, item) => ReactNode` | 숫자 배지 | — |
|
|
71
|
+
| `className` · `layoutStyle` | 문자열 · 배치 전용 style 객체 | — | 루트 배치만. 폭·색은 recipe 소유 |
|
|
72
|
+
|
|
73
|
+
## 배치
|
|
74
|
+
|
|
75
|
+
| 항목 | 값 | 근거 |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| 크기 | 폭 고정: 펼침 260, 접힘 72. 높이는 부모를 채운다(`min-block-size: 100%`). 항목 최소 높이 44(`control.minTouchTarget`), radius `radius.md` 12. 접기 버튼 44×44 | `sidebarRecipe.widths`·`itemMinHeight`·`itemRadius`, `.hjm-sidebar__toggle` |
|
|
78
|
+
| 간격 | 안쪽 위아래 `spacing.sm` 12 · 좌우 `spacing.xs` 8. 그룹 사이 `spacing.md` 16, 항목 사이 `spacing.xxs` 4. 항목 좌우 `spacing.sm` 12, 아이콘·라벨·배지 사이 `spacing.sm` 12. 그룹 제목 좌우 `spacing.sm` 12 · 위아래 `spacing.xxs` 4 | `sidebarRecipe`, `collectionItemContract` |
|
|
79
|
+
| 순서·정렬 | 앱 셸 시작 쪽(LTR 왼쪽) 세로 열. 셸의 열 배치·반응형 전환은 [Layout](layout.md)이 맡는다. 접기 버튼(`collapseLabels`를 줄 때만)이 맨 위 끝 쪽, 그 아래 그룹. 항목은 [아이콘][라벨][배지], 라벨이 남은 폭을 채운다. 끝 쪽 경계선 1px | `.hjm-sidebar`, `src/sidebar.tsx` |
|
|
80
|
+
| 고정·스크롤 | 항목이 넘치면 Sidebar 자체가 스크롤된다(`overflow: auto`) | `.hjm-sidebar` |
|
|
81
|
+
| 좁은 폭·큰 글자 | 긴 라벨·그룹 제목은 폭을 늘리지 않고 줄을 바꾼다. 큰 글자에서도 폭은 그대로, 항목 높이가 늘어난다. 접힘은 라벨·그룹 제목을 숨기고 아이콘 가운데(좌우 `spacing.xxs` 4), 배지는 아이콘 위 끝 쪽에 겹친다 | `.hjm-sidebar[data-collapsed]` |
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
펼침 260 접힘 72
|
|
85
|
+
┌──────────────────┐ ┌──────┐
|
|
86
|
+
│ [«] │ ← 접기 │ [»] │
|
|
87
|
+
│ 작업 (그룹 제목) │ │ │
|
|
88
|
+
│ ▣ 주문 3 │ ← 44 │ ▣³ │
|
|
89
|
+
│ ▣ 보고서 │ │ ▣ │
|
|
90
|
+
│ ↕ (넘치면 스크롤)│ │ │
|
|
91
|
+
└──────────────────┘ └──────┘
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 꼭 지킬 것
|
|
95
|
+
|
|
96
|
+
- 그룹·항목이 비었거나 id가 비거나 중복이거나, `badgeCount`가 음수·소수이거나, `currentId`가 항목에 없으면
|
|
97
|
+
렌더 중 `TypeError`/`RangeError`를 던진다. 라우트 → `currentId` 대응은 제품이 유지한다.
|
|
98
|
+
- 현재 위치는 `currentId`(→ `aria-current="page"`)로만 표시한다. 활성 색을 `className`으로 칠하지 않는다.
|
|
99
|
+
- 접기를 쓰면 `renderIcon`을 반드시 준다. 접힌 레일은 라벨을 숨기므로 아이콘이 없으면 쓸 수 없다.
|
|
100
|
+
- 라벨·그룹 제목·`collapseLabels`는 i18n 키로 넣는다. 긴 문구는 자르지 않고 줄바꿈된다.
|
|
101
|
+
- 아이콘·배지 그림은 제품 소유, 폭·높이·간격·색은 `sidebarRecipe` 소유다.
|
|
102
|
+
|
|
103
|
+
## 함정
|
|
104
|
+
|
|
105
|
+
- 항목은 `href`가 있는 일반 `<a>`이고 클릭 시 기본 이동을 막지 않는다. `onNavigate`는 알림 콜백이지
|
|
106
|
+
이동을 대신하지 않는다. client router 전환(Next.js 등)이 필요하면 제품 셸에서 동작을 확인한다.
|
|
107
|
+
- `destination`을 빼면 `href` 없는 `<a>`가 되어 키보드 초점을 받지 못한다. 이동 항목에는 `destination`을 둔다.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Skeleton
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: `src/component-recipes.ts`(`skeletonRecipe`), `src/foundations.ts`(`glyph`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/상태와 알림/스켈레톤`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
데이터가 오기 전, 곧 채워질 콘텐츠의 **모양을 미리 보여 줄 때** 쓴다. 목록 행·카드·프로필
|
|
14
|
+
사진·본문 줄처럼 도착 뒤의 배치를 알고 있고, 로딩이 끝나면 같은 자리에 실제 내용이 들어오는 경우다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
로딩 표시 셋은 "무엇을 아는가"로 고른다.
|
|
19
|
+
|
|
20
|
+
| 상황 | 대신 쓸 것 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| 도착할 내용의 모양을 모르거나 작은 영역의 짧은 대기 | [Spinner](spinner.md) |
|
|
23
|
+
| 진행량(몇 %, 몇 개 중 몇 개)을 알거나 사용자가 기다려야 하는 긴 작업 | [Progress](progress.md) |
|
|
24
|
+
| 버튼을 누른 뒤의 처리 중 | [Button](button.md)의 `loading` |
|
|
25
|
+
| 목록 끝에서 다음 페이지를 불러옴 | [LoadMore](load-more.md) |
|
|
26
|
+
| 불러온 결과가 비어 있음 | [EmptyState](empty-state.md) |
|
|
27
|
+
|
|
28
|
+
## 공개 이름과 import
|
|
29
|
+
|
|
30
|
+
| 이름 | 역할 | Web | Native |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `Skeleton` | 기본 | `@hjmds/react`, `/feedback` | `@hjmds/react-native`, `/feedback` |
|
|
33
|
+
|
|
34
|
+
## 최소 사용 예
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Web
|
|
38
|
+
import { Skeleton } from "@hjmds/react/feedback";
|
|
39
|
+
|
|
40
|
+
<>
|
|
41
|
+
<Skeleton shape="circle" />
|
|
42
|
+
<Skeleton shape="text" width="60%" />
|
|
43
|
+
</>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Native
|
|
48
|
+
import { Skeleton } from "@hjmds/react-native/feedback";
|
|
49
|
+
|
|
50
|
+
<>
|
|
51
|
+
<Skeleton shape="circle" accessibilityLabel={t("profile.loading")} />
|
|
52
|
+
<Skeleton shape="text" width="60%" />
|
|
53
|
+
</>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 축과 기본값
|
|
57
|
+
|
|
58
|
+
| prop | 값 | 기본값 | 설명 |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| `shape` | `block`(높이 `spacing.xxl`·radius `md`) · `text`(`spacing.md`·`sm`) · `circle`(`glyph.xxl` 지름·`full`) | `block` | 모양과 기본 크기 |
|
|
61
|
+
| `animated` | `boolean` | `true`(펄스) | 0.9.12까지는 `false`였다. 정지 블록이 필요하면 `animated={false}`를 명시한다. reduced motion이면 두 renderer 모두 펄스를 멈추고 불투명한 끝 값으로 그린다(recipe `reducedMotion: "static"`) |
|
|
62
|
+
| `width` | Web `string \| number` · Native `ViewStyle["width"]` | 폭 100%(circle은 지름) | — |
|
|
63
|
+
| `height` | Web `string \| number` · Native `number` | shape 기본값 | — |
|
|
64
|
+
| `accessibilityLabel`(Native) | 문자열 | — | 주면 `busy` 상태로 읽힌다. 영역 첫 Skeleton 하나에만 준다 |
|
|
65
|
+
| `radius`(Native) | 숫자 | — | 0.9 호환용, `shape`보다 우선 |
|
|
66
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 바깥 여백만 |
|
|
67
|
+
|
|
68
|
+
콜백 prop은 없다.
|
|
69
|
+
|
|
70
|
+
## 배치
|
|
71
|
+
|
|
72
|
+
| 항목 | 값 | 근거 |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| 크기 | `block` 폭 100% · 높이 `spacing.xxl` 32, `text` 높이 `spacing.md` 16, `circle` 지름 `glyph.xxl` 44. 텍스트 마지막 줄은 `width="60%"`처럼 짧게 | `skeletonRecipe.shapes`, `.hjm-skeleton` |
|
|
75
|
+
| 간격 | 실제 콘텐츠의 간격 토큰을 그대로 쓴다(예: [Stack](stack.md) `spacing.xs` 8). Skeleton에 margin을 넣지 않는다 | — |
|
|
76
|
+
| 순서·정렬 | 로드 뒤 실제 콘텐츠가 놓일 자리에 같은 순서로. 목록이면 행 구조(원 + 텍스트 줄 둘)를 행 수만큼 반복 | — |
|
|
77
|
+
| 고정·스크롤 | 스크롤 영역 안의 콘텐츠만 대신한다. 고정 바(TopBar·BottomCTA)는 그대로 둔다 | — |
|
|
78
|
+
| 좁은 폭·큰 글자 | `block`·`text`는 폭 100%라 컨테이너를 따른다. 높이는 글자 크기와 무관한 고정값이다 | `.hjm-skeleton` |
|
|
79
|
+
|
|
80
|
+
## 꼭 지킬 것
|
|
81
|
+
|
|
82
|
+
- 실제 콘텐츠와 같은 자리·비슷한 크기로 둔다. 로딩이 끝나면 Skeleton을 내용으로 **바꾼다**(겹쳐 두지 않는다).
|
|
83
|
+
- 색·테두리는 recipe와 테마가 정한다. 배치는 `layoutStyle`로 하고 `style`/`className`으로 회색 배경을 덮지 않는다.
|
|
84
|
+
Native `style`은 deprecated(개발 모드 경고, 다음 major 제거)다.
|
|
85
|
+
- Web Skeleton은 `aria-hidden`이다. 로딩 사실을 알려야 하면 영역 단위로 한 번 알린다
|
|
86
|
+
(예: 감싼 영역의 `aria-busy`, 또는 Spinner 하나). Skeleton마다 라벨을 붙이지 않는다.
|
|
87
|
+
- Web은 `HjmProvider` 안에서 렌더한다. 원 지름·펄스 값을 provider가 내보내는 `--hjm-skeleton-*` 변수로 읽는다.
|
|
88
|
+
|
|
89
|
+
## 플랫폼 차이
|
|
90
|
+
|
|
91
|
+
| 항목 | Web | Native |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| 접근성 | 항상 `aria-hidden` | `accessibilityLabel`을 주면 `busy` 상태로 읽힘, 없으면 숨김 |
|
|
94
|
+
| 높이 | `string \| number` | `number`만 |
|
|
95
|
+
| 모서리 직접 지정 | 없음 | `radius`(0.9 호환용, `shape`보다 우선) |
|
|
96
|
+
| reduced motion 판단 | `prefers-reduced-motion`·`.hjm-root[data-motion="reduced"]` | provider `environment.reducedMotion` |
|
|
97
|
+
|
|
98
|
+
## 함정
|
|
99
|
+
|
|
100
|
+
- Native에서 `width`·`height`·`radius`는 `shape`보다 우선한다. 셋을 다 주면 shape가 사실상 무시된다.
|
|
101
|
+
- Native는 `RN Animated`(native driver)로 펄스를 돌린다. 별도 peer 의존성은 없다.
|
|
102
|
+
- Native `accessibilityLabel?: string`은 `exactOptionalPropertyTypes`에서 `undefined`를 받지 않는다. 첫 Skeleton에만 줄 때는
|
|
103
|
+
`{...(first ? { accessibilityLabel: t("…") } : {})}`처럼 조건부 spread로 넘긴다.
|
|
104
|
+
- 현재 Web `style`은 `width`·`height` prop 값 뒤에 펼쳐져 크기·배경을 덮을 수 있다(타입이 막지 않는다). 크기는 `width`·`height`,
|
|
105
|
+
배치는 `layoutStyle`로 준다.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SkipNav
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [SkipNav](../../skip-nav.md), `src/skip-nav.ts`(`skipNavRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/기반 기능/본문 바로가기`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
Web 화면에서 반복되는 머리(내비게이션·헤더)를 건너뛰고 본문으로 가는 링크가 필요할 때 쓴다.
|
|
14
|
+
내비게이션이 있는 Web 페이지에는 하나를 둔다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 계속 숨겨 두는 화면 리더 전용 문구 | [VisuallyHidden](visually-hidden.md) |
|
|
21
|
+
| 페이지 안의 여러 절로 이동하는 목차 | [Anchor](anchor.md) |
|
|
22
|
+
| 다른 페이지·URL로 이동 | [Link](link.md) |
|
|
23
|
+
| Native 화면 | 없음(화면 리더 rotor가 맡는다) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `SkipNav` | 기본 | `@hjmds/react`, `/skip-nav` | — |
|
|
30
|
+
|
|
31
|
+
## 최소 사용 예
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Web
|
|
35
|
+
import { SkipNav } from "@hjmds/react/skip-nav";
|
|
36
|
+
|
|
37
|
+
<body>
|
|
38
|
+
<SkipNav targetId="main" label={t("a11y.skipToContent")} />
|
|
39
|
+
<header>{/* 내비게이션 */}</header>
|
|
40
|
+
<main id="main">{/* 본문 */}</main>
|
|
41
|
+
</body>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Native: 없음.
|
|
45
|
+
|
|
46
|
+
## 축과 기본값
|
|
47
|
+
|
|
48
|
+
| prop | 값 | 기본값 | 설명 |
|
|
49
|
+
| --- | --- | --- | --- |
|
|
50
|
+
| `targetId` | `#` 없는 id | 필수 | `href`는 받지 않고 `#${targetId}`로 만든다 |
|
|
51
|
+
| `label` | 문구 | 필수 | `children`은 받지 않는다 |
|
|
52
|
+
| `onClick` | `(event: MouseEvent<HTMLAnchorElement>) => void` | — | 먼저 불린 뒤 대상으로 초점을 옮긴다. `preventDefault()`하면 옮기지 않는다 |
|
|
53
|
+
| 나머지 `<a>` 속성 · `className` · `ref` | — | — | `ref`는 `HTMLAnchorElement`. `layoutStyle`은 받지 않는다(Web 제외 15개 중 하나) |
|
|
54
|
+
|
|
55
|
+
## 배치
|
|
56
|
+
|
|
57
|
+
| 항목 | 값 | 근거 |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| 크기 | 최소 높이 `control.minTouchTarget` 44, 좌우 `spacing.md` 16, radius `radius.md` 12 | `skipNavRecipe` |
|
|
60
|
+
| 간격 | 초점을 받으면 시작 쪽 위 모서리에서 `spacing.sm` 12 떨어져 나타난다 | `skipNavRecipe.offset`, `.hjm-skip-nav` |
|
|
61
|
+
| 순서·정렬 | DOM에서 `<body>`(또는 앱 셸 루트)의 첫 자식, 헤더·내비게이션보다 앞. 페이지에 하나 | — |
|
|
62
|
+
| 고정·스크롤 | 평소에는 화면 위로 밀려 보이지 않고, 초점 시 `position: absolute`로 겹쳐 나타난다(레이아웃을 밀지 않음). `position`이 걸린 컨테이너 안에 넣으면 그 기준으로 뜨므로 문서 최상단에 둔다. z-index `layer.toast + 1` 1001 | `.hjm-skip-nav` |
|
|
63
|
+
| 좁은 폭·큰 글자 | 긴 라벨은 화면 폭 − 24(양쪽 offset)를 넘지 않고 줄을 바꾼다 | `.hjm-skip-nav` `max-inline-size` |
|
|
64
|
+
|
|
65
|
+
## 꼭 지킬 것
|
|
66
|
+
|
|
67
|
+
- 문서의 **첫 tab stop**에 둔다. 배치는 renderer가 강제하지 못한다.
|
|
68
|
+
- `targetId`는 `#` 없는 id만 넣는다. `"#main"`이나 빈 문자열은 렌더 중 `TypeError`를 던진다. 빈 `label`도 같다.
|
|
69
|
+
- `targetId`의 요소가 실제로 문서에 있어야 한다. 없으면 클릭해도 초점이 옮겨지지 않는다.
|
|
70
|
+
- `label`은 i18n 키로 넣는다(제품 문구). 숨김·포커스 시 표시 방식은 HJM 소유이므로 `className`으로 계속 숨기거나 위치를 바꾸지 않는다.
|
|
71
|
+
|
|
72
|
+
## 함정
|
|
73
|
+
|
|
74
|
+
- 클릭 시 대상에 `tabindex`가 없으면 `tabindex="-1"`을 붙이고 `focus()`한다. 대상 요소에 생긴 이 속성을 지우지 않는다.
|
|
75
|
+
- `onClick`에서 `event.preventDefault()`를 부르면 초점 이동이 일어나지 않는다.
|
|
76
|
+
- 현재 `style`은 타입상 받지만 HJM이 recipe 변수 style로 덮어써 조용히 버려진다. 위치는 recipe offset이 고정한다.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Slider
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [Slider](../../slider.md), `src/slider.ts`(`sliderRecipe`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/슬라이더`, `배포/컴포넌트/입력/별점`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
범위 안에서 값 하나를 대략적으로, 연속 조작으로 고를 때 쓴다. 만족도 점수, 필터 강도,
|
|
14
|
+
음량처럼 "정확히 몇"보다 "이 근처"가 중요한 입력이다. 항상 값을 가지며 비어 있는 상태가 없다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 정확한 수를 입력하거나 "아직 정하지 않음"이 필요 | [NumberField](number-field.md) |
|
|
21
|
+
| 몇 개의 이름 있는 선택지 중 하나 | [SegmentedControl](segmented-control.md), [RadioGroup](radio-group.md) |
|
|
22
|
+
| 켜기/끄기 | [Switch](switch.md) |
|
|
23
|
+
| 두 손잡이로 구간 고르기 | 없음(계약이 `range`를 넣지 않았다) |
|
|
24
|
+
| Web에서 두 패널의 경계 크기 조절 | [Splitter](splitter.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `Slider` | 기본 | `@hjmds/react`, `/forms`, `/slider` | `@hjmds/react-native`, `/inputs`, `/slider` |
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { Slider } from "@hjmds/react/slider";
|
|
37
|
+
|
|
38
|
+
<Slider
|
|
39
|
+
label={t("filter.intensity")}
|
|
40
|
+
min={0}
|
|
41
|
+
max={100}
|
|
42
|
+
step={5}
|
|
43
|
+
value={intensity}
|
|
44
|
+
onValueChange={setIntensity}
|
|
45
|
+
onValueChangeEnd={saveIntensity}
|
|
46
|
+
getValueText={(v) => t("filter.percent", { value: v })}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Native
|
|
52
|
+
import { Slider } from "@hjmds/react-native/slider";
|
|
53
|
+
|
|
54
|
+
<Slider
|
|
55
|
+
label={t("filter.intensity")}
|
|
56
|
+
min={0}
|
|
57
|
+
max={100}
|
|
58
|
+
step={5}
|
|
59
|
+
value={intensity}
|
|
60
|
+
onValueChange={setIntensity}
|
|
61
|
+
onValueChangeEnd={saveIntensity}
|
|
62
|
+
getValueText={(v) => t("filter.percent", { value: v })}
|
|
63
|
+
incrementLabel={t("filter.increase")}
|
|
64
|
+
decrementLabel={t("filter.decrease")}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 축과 기본값
|
|
69
|
+
|
|
70
|
+
| prop | 값 | 기본값 | 설명 |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `label` · `min` · `max` | 문구 · 숫자 | — | 필수 |
|
|
73
|
+
| `step` | 숫자 | `1` | 사용자 입력의 snap 단위 |
|
|
74
|
+
| `value` · `defaultValue` | 숫자 | `min` | controlled 또는 uncontrolled. 마운트 뒤 두 방식 사이를 바꾸면 Web은 `Error`를 던진다 |
|
|
75
|
+
| `onValueChange` | `(value: number) => void` | — | 드래그·입력 중 매번 |
|
|
76
|
+
| `onValueChangeEnd` | `(value: number) => void` | — | 놓았을 때·키를 뗐을 때·Native 접근성 action 뒤 한 번. 저장·요청은 여기서 한다 |
|
|
77
|
+
| `getValueText` | `(value: number) => string` | 숫자 그대로 | 보이는 값과 접근성 값 문자열 |
|
|
78
|
+
| `incrementLabel` · `decrementLabel`(Native) | 문자열 | 필수 | 접근성 action 이름 |
|
|
79
|
+
| `disabled` | `boolean` | `false` | — |
|
|
80
|
+
| `layoutStyle` | 배치 전용 style 객체 | — | 루트 배치 |
|
|
81
|
+
|
|
82
|
+
## 배치
|
|
83
|
+
|
|
84
|
+
| 항목 | 값 | 근거 |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| 크기 | 가로 폭을 채운다. 트랙 줄은 터치 높이 `control.minTouchTarget` 44 안에 두께 4 트랙과 지름 20 thumb을 세로 중앙에 그린다 | `sliderRecipe.sizes.medium`, `.hjm-slider__control` |
|
|
87
|
+
| 간격 | 머리 줄과 트랙 줄 사이 `sliderRecipe.header.trackGap`(`spacing.xs`) 8, 라벨–값 사이 `header.gap`(`spacing.md`) 16(두 플랫폼). 여러 Slider를 쌓을 때는 Field와 같은 세로 간격(예: [Stack](stack.md) `spacing.md` 16) | `sliderRecipe.header`, `.hjm-slider`, `.hjm-slider__header`, Native `Slider` |
|
|
88
|
+
| 순서·정렬 | 위 [라벨 ……… 값] 머리 줄(라벨 시작 쪽, 값 끝 쪽, `label` 크기·tabular 숫자), 아래 트랙. Web은 트랙 양 끝을 thumb 반지름 10만큼 들여 thumb이 컨테이너 밖으로 나가지 않는다 | `.hjm-slider__header`, `.hjm-slider__interactive` |
|
|
89
|
+
| 고정·스크롤 | 폼·설정·필터 패널 안, 세로 스크롤 안에 두어도 된다. Native는 가로 제스처가 확인될 때만 값이 바뀐다 | Native `Slider`(PanResponder) |
|
|
90
|
+
| 좁은 폭·큰 글자 | Web은 긴 라벨이 줄을 바꾸고 값은 줄지 않는다(`flex: 0 0 auto`). 트랙 터치 높이는 44 그대로 | `.hjm-slider__label`, `.hjm-slider__value` |
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
┌──────────────────────────────────────┐
|
|
94
|
+
│ t("filter.intensity") 60% │ ← 머리 줄
|
|
95
|
+
│ │ ↕ spacing.xs 8
|
|
96
|
+
│ ━━━━━━━━━━━━━━━━━━●──────────────── │ ← 터치 높이 44, 트랙 4, thumb 20
|
|
97
|
+
└──────────────────────────────────────┘
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 꼭 지킬 것
|
|
101
|
+
|
|
102
|
+
- `label`·`getValueText`·(Native) `incrementLabel`/`decrementLabel`은 i18n 문구로 준다. 단위·형식은 제품 소유다.
|
|
103
|
+
- 사용자 입력만 step으로 snap된다. 제품이 준 `value`가 범위 안이면 step 밖이어도 그대로 그린다.
|
|
104
|
+
- 색·트랙 두께·thumb 크기는 HJM 소유다. 배치는 `layoutStyle`로 한다. Web `style`/`className`/`inputClassName`은 배치용으로만 쓰고,
|
|
105
|
+
Native `containerStyle`/`controlStyle`은 deprecated(개발 모드 경고, 다음 major 제거)다.
|
|
106
|
+
|
|
107
|
+
## 플랫폼 차이
|
|
108
|
+
|
|
109
|
+
| 항목 | Web | Native |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| 접근성 | `input type="range"`, `aria-valuetext` | role `adjustable`, `accessibilityValue` |
|
|
112
|
+
| 키 조작 | 방향키·Home/End·PageUp/PageDown(step×10) | `increment`/`decrement` action만(페이지 이동 없음) |
|
|
113
|
+
| 필수 추가 문구 | 없음 | `incrementLabel`, `decrementLabel` |
|
|
114
|
+
| 스타일 슬롯 | `layoutStyle`, `style`(루트), `className`, `inputClassName` | `layoutStyle`. `containerStyle`·`controlStyle`은 deprecated(`style` prop 없음) |
|
|
115
|
+
| 의존성 | 없음 | 없음(PanResponder 사용) |
|
|
116
|
+
|
|
117
|
+
## 함정
|
|
118
|
+
|
|
119
|
+
- Native는 세로 스크롤 안에서 트랙을 스치기만 해도 값이 바뀌지 않도록, 가로 방향이 확인될 때까지 부모 스크롤에 제스처를 양보한다.
|
|
120
|
+
세로로 시작한 제스처는 값 변경도 `onValueChangeEnd`도 없다.
|
|
121
|
+
- Native에서 드래그 중 `disabled`가 되면 마지막 값을 한 번 commit하고 제스처를 끝낸다.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# SortableCollection
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 1.12.1
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: `@hjmds/design-contracts/components/interaction-adapters`(계약 함수), `src/sortable.tsx`(Web·Native)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/입력/할 일 목록`, `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
작은 목록의 순서를 사용자가 바꾸고, 그 순서를 제품이 저장할 때 쓴다(즐겨찾기 순서, 할 일 순서 등).
|
|
14
|
+
드래그와 함께 각 행에 "앞으로/뒤로" 버튼과 키보드·접근성 action이 항상 붙는다. 목록은 제품이 controlled로 쥔다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 순서를 바꾸지 않는 일반 목록 | [List](list.md), [ListRow](list-row.md) |
|
|
21
|
+
| 행을 밀어 나오는 행동(삭제·보관) | [SwipeActions](swipe-actions.md) |
|
|
22
|
+
| 두 목록 사이로 항목 옮기기 | [TransferList](transfer-list.md) |
|
|
23
|
+
| 수백 개 이상의 긴 목록 | [VirtualList](virtual-list.md)(정렬 기능 없음) |
|
|
24
|
+
|
|
25
|
+
## 공개 이름과 import
|
|
26
|
+
|
|
27
|
+
| 이름 | 역할 | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `SortableCollection` | 보조(별도 보조 기능) | `/sortable`만 | `/sortable`만 |
|
|
30
|
+
|
|
31
|
+
root·category entry에서는 export되지 않는다. granular subpath로만 import한다.
|
|
32
|
+
이 subpath는 optional peer를 앱이 직접 설치해야 동작한다.
|
|
33
|
+
|
|
34
|
+
필요한 peer(정확한 버전):
|
|
35
|
+
|
|
36
|
+
- Web: `@dnd-kit/react` 0.5.0, `@dnd-kit/dom` 0.5.0
|
|
37
|
+
- Native: `react-native-sortables` 1.10.1, 그리고 그 peer인 `react-native-gesture-handler`·`react-native-reanimated`(HJM peer 범위: gesture-handler 2.32.0, reanimated ^4.5.1, 함께 선언된 `react-native-worklets` ^0.10.1)
|
|
38
|
+
|
|
39
|
+
다른 optional subpath(celebration·qr-code 등)에서 peer 없이 tsc·테스트는 통과하고 기기 Metro에서
|
|
40
|
+
크래시한 사고가 있었다(2026-10). 설치 여부를 기기 실행으로 확인한다.
|
|
41
|
+
|
|
42
|
+
## 최소 사용 예
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Web
|
|
46
|
+
import { SortableCollection } from "@hjmds/react/sortable";
|
|
47
|
+
|
|
48
|
+
<SortableCollection
|
|
49
|
+
label={t("favorites.orderLabel")}
|
|
50
|
+
items={favorites.map((f) => ({ id: f.id, label: f.name }))}
|
|
51
|
+
labels={sortLabels}
|
|
52
|
+
renderItem={(item) => <FavoriteSummary id={item.id} />}
|
|
53
|
+
onCommit={(intent) => saveOrder(intent.orderedIds)}
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// Native
|
|
59
|
+
import { SortableCollection } from "@hjmds/react-native/sortable";
|
|
60
|
+
|
|
61
|
+
<SortableCollection
|
|
62
|
+
label={t("favorites.orderLabel")}
|
|
63
|
+
items={items}
|
|
64
|
+
labels={sortLabels}
|
|
65
|
+
renderItem={(item) => <FavoriteSummary id={item.id} />}
|
|
66
|
+
onCommit={(intent) => saveOrder(intent.orderedIds)}
|
|
67
|
+
active={isFocused}
|
|
68
|
+
/>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 축과 기본값
|
|
72
|
+
|
|
73
|
+
표의 prop은 Web·Native 공통이다. 타입 `SortableItem`·`SortableLabels`·`ReorderIntent`는
|
|
74
|
+
`@hjmds/design-contracts/components/interaction-adapters`에 있다.
|
|
75
|
+
|
|
76
|
+
| prop | 값 | 기본값 | 설명 |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
| `items` | `readonly { id: string; label: string; disabled?: boolean }[]` | 필수 | 제어 목록. `disabled` 항목은 그 자리에 고정된다 |
|
|
79
|
+
| `label` | 문자열 | 필수 | 목록 접근성 이름. 비면 `TypeError` |
|
|
80
|
+
| `labels` | `{ instructions, dragStart(item), dragCancel, handle(item), previous(item), next(item), position(item, position, total) }` | 필수 | 모두 제품 i18n 문구. 함수형은 `string`을 돌려준다 |
|
|
81
|
+
| `renderItem` | `(item: SortableItem) => ReactNode` | 필수 | 행 내용 |
|
|
82
|
+
| `onCommit` | `(intent: { itemId, fromIndex, toIndex, orderedIds: readonly string[], source: "drag" \| "keyboard" \| "accessibility-action" }) => void` | 필수 | `orderedIds`로 제품 상태를 바꿔야 화면 순서가 바뀐다 |
|
|
83
|
+
| `onCancel` | `() => void` | — | 거절·취소된 드래그, 드래그 중 목록 변경, Native 백그라운드 전환 |
|
|
84
|
+
| `disabled` | `boolean` | `false` | 전체 이동을 막는다 |
|
|
85
|
+
| `active`(Native) | `boolean` | — | 탭 이동 뒤에도 마운트가 남는 화면은 route focus를 넘긴다(드래그 상태 초기화) |
|
|
86
|
+
| `layoutStyle`(Web) | 배치 전용 style 객체 | — | 목록(`<ul>`) 배치. Native는 없음 |
|
|
87
|
+
|
|
88
|
+
## 배치
|
|
89
|
+
|
|
90
|
+
| 항목 | 값 | 근거 |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| 크기 | 핸들 터치 영역 44×44(Web 핸들 버튼, Native 핸들 줄 최소 높이 44). 이동 버튼은 ghost [Button](button.md) 기본 크기 | Web·Native `sortable.tsx` |
|
|
93
|
+
| 간격 | Web 행 위아래 `spacing.xs` 8, 핸들–내용 `spacing.sm` 12, 버튼 사이 `spacing.xs` 8. Native 행 안쪽 `spacing.xs` 8, 세로 간격 `spacing.xs` 8, 버튼 사이 `spacing.xs` 8 | Web·Native `sortable.tsx` |
|
|
94
|
+
| 순서·정렬 | 본문의 세로 목록 자리에 한 열. Web 행: [⠿ 핸들][`renderItem` 내용] 아래 줄에 [앞으로][뒤로]. Native 행: `[⠿ + label]`(끌기 핸들) → `renderItem` → [앞으로][뒤로]. Native는 핸들 줄에 `label`을 보여 주므로 `renderItem`에서 같은 이름을 반복하지 않는다. 행에 다른 행동 버튼을 추가하지 않는다 | Web·Native `sortable.tsx` |
|
|
95
|
+
| 고정·스크롤 | 목록 자체는 스크롤하지 않는다. 부모 스크롤 안에 두고, 고정 저장 버튼이 필요하면 [BottomCTA](bottom-cta.md) | — |
|
|
96
|
+
| 좁은 폭·큰 글자 | 이동 버튼 줄을 따로 두고 줄바꿈(`flexWrap`)해 좁은 폭·200% 글자에서도 라벨을 읽게 한다 | Web `sortable.tsx` 주석 |
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
Web 행 Native 행
|
|
100
|
+
┌────────────────────────────────────┐ ┌────────────────────────────┐
|
|
101
|
+
│ [⠿] renderItem(item) │ │ ⠿ label (≥ 44) │
|
|
102
|
+
│ [앞으로] [뒤로] │ │ renderItem(item) │
|
|
103
|
+
└────────────────────────────────────┘ │ [앞으로] [뒤로] │
|
|
104
|
+
└────────────────────────────┘
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 꼭 지킬 것
|
|
108
|
+
|
|
109
|
+
- `items`는 고유하고 비지 않은 `id`와 번역된 `label`을 가져야 한다. 어기거나 `label`이 비면 `TypeError`를 던진다.
|
|
110
|
+
- `onCommit`이 받은 `ReorderIntent.orderedIds`로 제품 상태를 갱신해야 화면 순서가 바뀐다. 저장·실패 시 되돌리기는 제품 소유다.
|
|
111
|
+
- `item.disabled`인 항목은 그 자리에 고정된다. 그 항목을 가로지르는 이동은 거절된다.
|
|
112
|
+
- 같은 목록이 드래그 중 바뀌면 드래그를 취소하고 `onCancel`을 부른다.
|
|
113
|
+
- `style`/`className`은 없다. Web은 `layoutStyle`로 목록 배치만 한다. 행 모양·핸들·이동 버튼은 HJM 소유다.
|
|
114
|
+
|
|
115
|
+
## 플랫폼 차이
|
|
116
|
+
|
|
117
|
+
| 항목 | Web | Native |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| 엔진 | `@dnd-kit` | `react-native-sortables` 1열 Grid |
|
|
120
|
+
| `active` prop | 없음 | 있음. 탭 이동 뒤에도 마운트가 남는 화면은 route focus를 넘긴다 |
|
|
121
|
+
| 이동 알림 | `role="status"` 영역에 `position` 문구 | `announceForAccessibility` |
|
|
122
|
+
| reduced motion | 전환 애니메이션만 끈다 | 드래그 자체를 끈다(버튼·접근성 action으로만 이동) |
|
|
123
|
+
| 앱 백그라운드 전환 | 해당 없음 | 드래그 취소(`onCancel`) 후 엔진 상태 초기화 |
|
|
124
|
+
|
|
125
|
+
## 함정
|
|
126
|
+
|
|
127
|
+
- `onCancel`은 Web에서 거절된 드래그·취소된 드래그 모두에 온다. 드래그 시작 시 띄운 제품 UI는 여기서 정리한다.
|