@hjmds/design-contracts 1.18.0 → 1.20.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/action-session.d.ts +16 -0
- package/dist/action-session.d.ts.map +1 -1
- package/dist/action-session.js +18 -0
- package/dist/action-session.js.map +1 -1
- package/dist/behaviors.d.ts +8 -6
- package/dist/behaviors.d.ts.map +1 -1
- package/dist/behaviors.js +4 -3
- package/dist/behaviors.js.map +1 -1
- package/dist/bottom-navigation.d.ts +14 -0
- package/dist/bottom-navigation.d.ts.map +1 -1
- package/dist/bottom-navigation.js +21 -0
- package/dist/bottom-navigation.js.map +1 -1
- package/dist/carousel.d.ts +3 -0
- package/dist/carousel.d.ts.map +1 -1
- package/dist/carousel.js.map +1 -1
- package/dist/catalog.d.ts +61 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/component-recipes.d.ts +67 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +27 -1
- package/dist/component-recipes.js.map +1 -1
- package/dist/field-group.d.ts +14 -0
- package/dist/field-group.d.ts.map +1 -1
- package/dist/field-group.js +9 -0
- package/dist/field-group.js.map +1 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +10 -3
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/screen-patterns.d.ts +4 -0
- package/dist/screen-patterns.d.ts.map +1 -1
- package/dist/screen-patterns.js +12 -3
- package/dist/screen-patterns.js.map +1 -1
- package/dist/semantic-colors.d.ts +9 -0
- package/dist/semantic-colors.d.ts.map +1 -1
- package/dist/semantic-colors.js +5 -0
- package/dist/semantic-colors.js.map +1 -1
- package/dist/toast.d.ts +10 -1
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +43 -2
- package/dist/toast.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 +2 -0
- package/docs/asset.md +7 -0
- package/docs/bottom-navigation.md +22 -0
- package/docs/brand-boundary.md +3 -3
- package/docs/collection-rail.md +4 -0
- package/docs/compound-controls.md +8 -3
- package/docs/consumer-policy.md +39 -120
- 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/gravity-letters.md +2 -0
- package/docs/grid-reveal.md +2 -0
- package/docs/layout.md +2 -2
- package/docs/load-more.md +5 -0
- package/docs/migration-native-legacy-removal.md +30 -17
- package/docs/optional-adapters.md +4 -0
- package/docs/screen-patterns.md +23 -1
- package/docs/toast.md +41 -2
- package/docs/usage/README.md +248 -245
- package/docs/usage/SELECTION.md +43 -0
- package/docs/usage/STANDARD.md +22 -10
- package/docs/usage/components/accordion.md +5 -3
- package/docs/usage/components/agreement.md +6 -9
- package/docs/usage/components/alert-dialog.md +12 -6
- package/docs/usage/components/asset.md +10 -4
- package/docs/usage/components/auth-provider-button.md +2 -2
- package/docs/usage/components/avatar.md +8 -6
- package/docs/usage/components/badge.md +4 -4
- package/docs/usage/components/bottom-cta.md +4 -4
- package/docs/usage/components/bottom-info.md +2 -2
- package/docs/usage/components/bottom-navigation.md +30 -6
- package/docs/usage/components/button.md +2 -2
- package/docs/usage/components/card.md +1 -1
- package/docs/usage/components/carousel.md +12 -4
- package/docs/usage/components/chat-message.md +6 -2
- package/docs/usage/components/chat-screen.md +5 -5
- package/docs/usage/components/checkbox-group.md +1 -1
- package/docs/usage/components/checkbox.md +2 -2
- package/docs/usage/components/chip.md +2 -2
- package/docs/usage/components/code-block.md +3 -4
- package/docs/usage/components/collapsible.md +6 -4
- package/docs/usage/components/combobox.md +2 -2
- package/docs/usage/components/command-palette.md +1 -1
- package/docs/usage/components/comment-thread-screen.md +7 -4
- package/docs/usage/components/container.md +1 -1
- package/docs/usage/components/content-transition.md +2 -2
- package/docs/usage/components/context-menu.md +2 -2
- package/docs/usage/components/counter-badge.md +3 -3
- package/docs/usage/components/data-table.md +1 -1
- package/docs/usage/components/date-picker.md +4 -4
- package/docs/usage/components/description-list.md +3 -3
- package/docs/usage/components/design-system-provider.md +2 -2
- package/docs/usage/components/dialog.md +14 -3
- package/docs/usage/components/divider.md +2 -2
- package/docs/usage/components/editor-screen.md +5 -3
- package/docs/usage/components/effect-surface.md +1 -1
- package/docs/usage/components/empty-state.md +6 -6
- package/docs/usage/components/field.md +5 -3
- package/docs/usage/components/file-picker.md +6 -3
- package/docs/usage/components/form.md +1 -1
- package/docs/usage/components/heading.md +3 -3
- package/docs/usage/components/icon-button.md +3 -3
- package/docs/usage/components/icon.md +1 -1
- package/docs/usage/components/image.md +5 -4
- package/docs/usage/components/keyboard-dock.md +1 -1
- package/docs/usage/components/layout.md +1 -1
- package/docs/usage/components/link.md +2 -2
- package/docs/usage/components/list-detail-screen.md +6 -6
- package/docs/usage/components/list-row.md +13 -4
- package/docs/usage/components/list.md +5 -4
- package/docs/usage/components/masonry.md +2 -1
- package/docs/usage/components/media-selection-screen.md +11 -5
- package/docs/usage/components/mentions.md +2 -2
- package/docs/usage/components/menu.md +3 -2
- package/docs/usage/components/menubar.md +2 -2
- package/docs/usage/components/message-composer.md +3 -3
- package/docs/usage/components/moderation-screen.md +3 -2
- package/docs/usage/components/notice.md +2 -2
- package/docs/usage/components/notification-inbox-screen.md +1 -1
- package/docs/usage/components/notification-item.md +2 -2
- package/docs/usage/components/number-field.md +3 -3
- package/docs/usage/components/otp-field.md +1 -1
- package/docs/usage/components/overview-screen.md +1 -1
- package/docs/usage/components/permission-screen.md +1 -1
- package/docs/usage/components/photo-source-sheet.md +1 -1
- package/docs/usage/components/popover.md +2 -2
- package/docs/usage/components/profile-screen.md +1 -1
- package/docs/usage/components/progress.md +2 -2
- package/docs/usage/components/qr-code.md +2 -2
- package/docs/usage/components/radio-group.md +1 -2
- package/docs/usage/components/radio.md +1 -2
- package/docs/usage/components/result.md +1 -1
- package/docs/usage/components/saved-items-screen.md +4 -2
- package/docs/usage/components/screen-layout.md +4 -3
- package/docs/usage/components/search-field.md +1 -1
- package/docs/usage/components/search-screen.md +4 -4
- package/docs/usage/components/section.md +3 -3
- package/docs/usage/components/segmented-control.md +4 -4
- package/docs/usage/components/select.md +2 -3
- package/docs/usage/components/settings-screen.md +11 -49
- package/docs/usage/components/sheet.md +10 -5
- package/docs/usage/components/sidebar.md +1 -1
- package/docs/usage/components/skeleton.md +1 -1
- package/docs/usage/components/skip-nav.md +1 -1
- package/docs/usage/components/slider.md +2 -2
- package/docs/usage/components/spinner.md +2 -2
- package/docs/usage/components/splitter.md +1 -1
- package/docs/usage/components/statistic.md +5 -4
- package/docs/usage/components/steps.md +2 -2
- package/docs/usage/components/surface.md +2 -3
- package/docs/usage/components/switch.md +1 -1
- package/docs/usage/components/tabs.md +4 -4
- package/docs/usage/components/tag.md +2 -2
- package/docs/usage/components/tags-input.md +4 -4
- package/docs/usage/components/text-area.md +2 -3
- package/docs/usage/components/text.md +85 -4
- package/docs/usage/components/timeline.md +2 -2
- package/docs/usage/components/toast.md +17 -7
- package/docs/usage/components/toggle-group.md +3 -3
- package/docs/usage/components/tooltip.md +1 -1
- package/docs/usage/components/top-bar.md +5 -4
- package/docs/usage/components/top.md +3 -3
- package/docs/usage/components/transfer-list.md +5 -4
- package/docs/usage/components/upload-item.md +3 -3
- package/docs/usage/components/virtual-list.md +1 -0
- package/docs/usage/compositions/action-feedback.md +42 -6
- package/docs/usage/compositions/action-recovery-optimistic.md +6 -1
- package/docs/usage/compositions/action-recovery-save.md +19 -23
- package/docs/usage/compositions/action-recovery-undo.md +6 -1
- package/docs/usage/compositions/activity-status.md +11 -5
- package/docs/usage/compositions/collection-detail.md +2 -0
- package/docs/usage/compositions/command-records.md +15 -5
- package/docs/usage/compositions/common-message.md +4 -2
- package/docs/usage/compositions/common-notification.md +5 -3
- package/docs/usage/compositions/compound-controls.md +13 -5
- package/docs/usage/compositions/content-transition-comparison.md +2 -2
- package/docs/usage/compositions/date-entry.md +10 -5
- package/docs/usage/compositions/date-time-selection.md +3 -3
- package/docs/usage/compositions/design-profile-comparison.md +7 -5
- package/docs/usage/compositions/disclosure.md +1 -1
- package/docs/usage/compositions/document-resource.md +10 -3
- package/docs/usage/compositions/expo-interactions.md +3 -2
- package/docs/usage/compositions/family-drawer.md +9 -4
- package/docs/usage/compositions/field-group.md +7 -1
- package/docs/usage/compositions/floating-action-button.md +18 -18
- package/docs/usage/compositions/illustrated-outcome.md +3 -2
- package/docs/usage/compositions/input-sheet.md +6 -2
- package/docs/usage/compositions/interaction-adapters.md +1 -1
- package/docs/usage/compositions/interaction-flow-apply.md +6 -3
- package/docs/usage/compositions/interaction-flow-draft.md +5 -2
- package/docs/usage/compositions/interaction-flow-search.md +5 -2
- package/docs/usage/compositions/live-list.md +7 -5
- package/docs/usage/compositions/navigation-bar-collection.md +8 -7
- package/docs/usage/compositions/optional-adapters.md +1 -1
- package/docs/usage/compositions/origin-dialog.md +5 -5
- package/docs/usage/compositions/paper-surface.md +1 -1
- package/docs/usage/compositions/photo-source.md +1 -1
- package/docs/usage/compositions/purpose-input-comment.md +5 -3
- package/docs/usage/compositions/purpose-input-message.md +5 -3
- package/docs/usage/compositions/reference-first.md +63 -42
- package/docs/usage/compositions/reference-review.md +60 -50
- package/docs/usage/compositions/reference-settings.md +60 -50
- package/docs/usage/compositions/selection-scope.md +48 -35
- package/docs/usage/compositions/stea-order-progress.md +1 -0
- package/docs/usage/compositions/stea-otp-verify.md +5 -3
- package/docs/usage/compositions/stea-pixel-empty.md +3 -3
- package/docs/usage/compositions/time-selection.md +6 -5
- package/docs/usage/compositions/upload-recovery.md +10 -1
- package/docs/usage/compositions/video-dialog.md +10 -3
- package/docs/usage/compositions/web-additions.md +5 -5
- package/docs/usage/compositions/web-navigation.md +1 -1
- package/docs/usage/screens/common-chat.md +21 -8
- package/docs/usage/screens/common-comments.md +13 -1
- package/docs/usage/screens/common-inbox.md +19 -6
- package/docs/usage/screens/common-login.md +19 -7
- package/docs/usage/screens/common-profile.md +22 -9
- package/docs/usage/screens/common-saved.md +16 -2
- package/docs/usage/screens/common-search.md +24 -4
- package/docs/usage/screens/common-settings.md +22 -50
- package/docs/usage/screens/common-shell.md +14 -2
- package/docs/usage/screens/dashboard.md +52 -109
- package/docs/usage/screens/discovery-gallery.md +57 -148
- package/docs/usage/screens/flow-collection.md +38 -9
- package/docs/usage/screens/flow-editor.md +25 -7
- package/docs/usage/screens/flow-media.md +23 -7
- package/docs/usage/screens/flow-moderation.md +15 -2
- package/docs/usage/screens/flow-onboarding.md +3 -3
- package/docs/usage/screens/flow-permission.md +17 -3
- package/docs/usage/screens/landing.md +37 -20
- package/docs/usage/screens/mockup-studio.md +22 -7
- package/docs/usage/screens/notification-settings.md +18 -4
- package/docs/usage/screens/product-bento.md +18 -9
- package/docs/usage/screens/reference-comparison.md +29 -10
- package/docs/usage/tokens/color.md +6 -3
- package/docs/usage/tokens/elevation-opacity.md +4 -2
- package/docs/usage/tokens/font-roles.md +2 -2
- package/docs/usage/tokens/layers.md +3 -3
- package/docs/usage/tokens/motion.md +2 -2
- package/docs/usage/tokens/radius.md +4 -2
- package/docs/usage/tokens/spacing.md +3 -3
- package/docs/usage/tokens/theme-studio.md +2 -2
- package/docs/usage/tokens/typography-studio.md +14 -8
- package/docs/usage/tokens/typography.md +15 -5
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- 단계: 구성
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
|
-
- 적용:
|
|
6
|
+
- 적용: 구성 API 지침 참조
|
|
7
7
|
- 검토일: 2026-10-06
|
|
8
8
|
- 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/시작하기/첫 작업을 만들고 이어하기`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
9
|
- 스토리북: `배포/구성/입력과 작성/첫 작업을 만들고 이어하기`
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
30
30
|
| --- | --- | --- | --- |
|
|
31
|
-
| 바깥 틀 |
|
|
32
|
-
| 내용 | Steps
|
|
31
|
+
| 바깥 틀 | 부모 화면 또는 포함 Surface | 부모 화면의 해당 슬롯 | [배치](../components/steps.md#배치)에 따른다 |
|
|
32
|
+
| 내용 | Steps와 분리한 입력·요약 슬롯 | 주제 → 입력 → 검토 → 결과 | spacing 토큰과 포함 컴포넌트 recipe |
|
|
33
33
|
| 행동 | Button 또는 공개 콜백 | 내용과 가까운 명시 진입점 | 주 행동 하나, 보조 행동과 구분 |
|
|
34
34
|
|
|
35
35
|
## 흐름과 상태
|
|
@@ -46,49 +46,70 @@
|
|
|
46
46
|
|
|
47
47
|
## 코드 골격
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{ id: "alerts", label: t("onboarding.alerts") },
|
|
60
|
-
],
|
|
61
|
-
}}
|
|
62
|
-
statusLabels={{
|
|
63
|
-
pending: t("steps.pending"), current: t("steps.current"),
|
|
64
|
-
complete: t("steps.complete"), error: t("steps.error"),
|
|
65
|
-
}}
|
|
66
|
-
composeAccessibleName={({ position, total, label }) => t("steps.name", { position, total, label })}
|
|
67
|
-
/>
|
|
68
|
-
```
|
|
49
|
+
<!-- usage-example: Web ../../../../../showcase/web/src/patterns/reference-flow-previews.tsx -->
|
|
50
|
+
<!-- usage-example: Native ../../../../../showcase/native/src/reference-flow-previews.tsx -->
|
|
51
|
+
|
|
52
|
+
이 이름의 독립 공개 컴포넌트는 없다. 아래는 공개 `createActionSession`과 입력·Steps·Sheet를 조합하는 예제다. Storybook 예제 파일을 제품에서 import하지 않는다.
|
|
53
|
+
|
|
54
|
+
- [Web 실행 예제](../../../../../showcase/web/src/patterns/reference-flow-previews.tsx)의 `Editor(kind="first")`
|
|
55
|
+
- [Native 실행 예제](../../../../../showcase/native/src/reference-flow-previews.tsx)의 `Editor(kind="first")`
|
|
56
|
+
- [공통 검증·초안 비교](../../../../../showcase/shared/reference-flows.ts)의 `draftError`, `sameDraft`는 예제 데이터 규칙이며 제품 규칙으로 교체한다.
|
|
57
|
+
|
|
58
|
+
주제 선택(0) → 입력(1) → 검토(2) → 저장 결과 순서다. 중단은 paused 표시만 바꾸고 draft와 stage를 보존한다. 이어하기는 이를 다시 보여 준다.
|
|
69
59
|
|
|
70
60
|
```tsx
|
|
71
|
-
// Native
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
61
|
+
// Web — Native도 같은 상태 골격을 사용한다. JSX/문구/영속 저장은 각 제품의 기존 화면 슬롯에 연결한다.
|
|
62
|
+
import { useState, useSyncExternalStore } from "react";
|
|
63
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
64
|
+
|
|
65
|
+
const [session] = useState(() => createActionSession(initialValue));
|
|
66
|
+
const saved = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
67
|
+
const [draft, setDraft] = useState(initialValue);
|
|
68
|
+
const [stage, setStage] = useState(0);
|
|
69
|
+
const [paused, setPaused] = useState(false);
|
|
70
|
+
const busy = saved.status === "pending";
|
|
71
|
+
const dirty = !sameDraft(draft, saved.value);
|
|
72
|
+
|
|
73
|
+
function review() {
|
|
74
|
+
if (busy || draftError(draft)) return; // 오류 문구를 입력에 연결하고 첫 오류 입력으로 이동
|
|
75
|
+
setStage(2);
|
|
76
|
+
}
|
|
77
|
+
async function save() {
|
|
78
|
+
if (draftError(draft)) return;
|
|
79
|
+
const submitted = { ...draft };
|
|
80
|
+
// pending 중 재호출은 계약이 차단한다. 실패하면 saved.value와 draft를 보존한다.
|
|
81
|
+
const result = await session.run(() => saveToServer(submitted));
|
|
82
|
+
if (result.status === "success") setDraft(result.value);
|
|
83
|
+
}
|
|
84
|
+
function discard() {
|
|
85
|
+
if (session.getSnapshot().status === "pending") return;
|
|
86
|
+
setDraft(saved.value);
|
|
87
|
+
session.reset(saved.value);
|
|
88
|
+
setPaused(true);
|
|
89
|
+
}
|
|
89
90
|
```
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
| 표시/행동 | 연결 |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| 진행 단계 | `Steps.descriptor.currentStepId`를 `stage`에서 도출, 저장 실패 시 `currentStepStatus="error"` |
|
|
95
|
+
| 편집 | `TextField`/선택 컨트롤은 draft를 편집. pending 동안 disabled이며 이미 큐에 든 입력·이탈·되돌리기 콜백도 최신 session 상태로 차단. 변경 시 오래된 실패 표시를 해제 |
|
|
96
|
+
| 검토 | 같은 draft의 요약과 수정 버튼. 요약에서 값을 따로 보관하지 않음 |
|
|
97
|
+
| 저장 | `Button.loading=busy`, 성공 결과는 서버 응답에서, 실패는 Notice와 같은 저장 버튼 재시도 |
|
|
98
|
+
| 되돌리기 | dirty이며 pending이 아닐 때 저장값 복원. 서버 요청 취소로 설명하지 않음 |
|
|
99
|
+
| 중단/이탈 | first/review는 메모리 draft·stage를 유지해 이어하기. settings는 dirty이면 확인 Sheet |
|
|
100
|
+
| 확정 이후 | 결과 확인과 새 편집 진입을 구분. 새 편집은 마지막 확정값으로 시작 |
|
|
101
|
+
|
|
102
|
+
## 플랫폼 차이
|
|
103
|
+
|
|
104
|
+
| 항목 | Web | Native |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| 입력/단계 포커스 | DOM 입력·제목 ref | Native 입력 ref와 접근성 포커스 |
|
|
107
|
+
| 결과 알림 | role=status | Android live region + iOS 접근성 알림 |
|
|
108
|
+
| 이탈 확인 | 공개 Sheet의 dismiss/focus 계약 | 공개 Sheet의 dismiss/키보드/복귀 계약 |
|
|
109
|
+
|
|
110
|
+
입력 오류는 입력 ref로 복귀한다. 설정 Sheet를 취소하면 실제 이탈 버튼, 변경을 버려 버튼이 사라지면 유지되는 단계 제목으로 복귀한다. Web은 안정적인 단계 제목/입력 ref에 포커스를 옮기고 `role="status"`로 결과를 알린다. Native는 공개 입력 ref와 단계 제목의 접근성 포커스·알림을 연결하며, unmount 때 예약된 포커스 이동을 취소한다. Sheet의 포커스·키보드·dismiss 정책은 기존 계약에 맡긴다. 제품은 validation 결과, 저장 API, 라우터 이탈 이벤트와 확인 Sheet의 연결, i18n 키를 제공한다.
|
|
111
|
+
|
|
112
|
+
메모리 초안은 이 예제의 mounted 화면 안에서만 유지된다. 새로고침·프로세스 종료·다른 기기 복구와 서버 중복 처리 방지는 제품 저장소와 API 계약이 있어야 한다. `session.reset`은 서버 요청 취소가 아니다.
|
|
92
113
|
|
|
93
114
|
## 함정
|
|
94
115
|
|
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
- 단계: 구성
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
|
-
- 적용:
|
|
6
|
+
- 적용: 구성 API 지침 참조
|
|
7
7
|
- 검토일: 2026-10-06
|
|
8
8
|
- 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/확인/선택 내용 검토와 수정`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
9
|
- 스토리북: `배포/구성/입력과 작성/선택 내용 검토와 수정`
|
|
10
10
|
|
|
11
11
|
## 언제 쓰나
|
|
12
12
|
|
|
13
|
-
선택 내용을 검토하고 수정 후 명시적으로 확정 흐름이 필요할 때 쓴다.
|
|
13
|
+
선택 내용을 검토하고 수정 후 명시적으로 확정 흐름이 필요할 때 쓴다. 입력·단계·저장 세션을 제품 로직에 연결하며 새 데이터 엔진을 만들지 않는다.
|
|
14
14
|
|
|
15
15
|
## 구성 요소
|
|
16
16
|
|
|
17
17
|
| 컴포넌트 | 역할 | 지침 |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
|
|
|
19
|
+
| TextField · createActionSession | 선택 내용을 검토하고 수정 후 명시적으로 확정 | [공개 계약](../components/form.md) |
|
|
20
20
|
| Button | 명시 행동·재시도 | [Button](../components/button.md) |
|
|
21
21
|
|
|
22
22
|
## 배치
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
30
30
|
| --- | --- | --- | --- |
|
|
31
|
-
| 바깥 틀 |
|
|
32
|
-
| 내용 |
|
|
31
|
+
| 바깥 틀 | 부모 화면 또는 포함 Surface | 부모 화면의 해당 슬롯 | [배치](../components/form.md#배치)에 따른다 |
|
|
32
|
+
| 내용 | 부모 화면의 입력·요약 슬롯 | 입력 → 요약 → 수정·저장 | spacing 토큰과 포함 컴포넌트 recipe |
|
|
33
33
|
| 행동 | Button 또는 공개 콜백 | 내용과 가까운 명시 진입점 | 주 행동 하나, 보조 행동과 구분 |
|
|
34
34
|
|
|
35
35
|
## 흐름과 상태
|
|
@@ -46,60 +46,70 @@
|
|
|
46
46
|
|
|
47
47
|
## 코드 골격
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
onSubmit={async () => {
|
|
60
|
-
setServerError(null);
|
|
61
|
-
if (nicknameError) { nicknameInputRef.current?.focus(); return; } // Web은 첫 오류 포커스를 제품이 한다
|
|
62
|
-
try { await save(values); } catch { setServerError(t("profile.saveFailed")); }
|
|
63
|
-
}}
|
|
64
|
-
formError={serverError}
|
|
65
|
-
actions={<Button type="submit">{t("profile.save")}</Button>}
|
|
66
|
-
>
|
|
67
|
-
<TextField ref={nicknameInputRef} label={t("profile.nickname")} value={values.nickname}
|
|
68
|
-
onValueChange={setNickname} error={nicknameError ? t(nicknameError) : undefined} />
|
|
69
|
-
</Form>
|
|
70
|
-
```
|
|
49
|
+
<!-- usage-example: Web ../../../../../showcase/web/src/patterns/reference-flow-previews.tsx -->
|
|
50
|
+
<!-- usage-example: Native ../../../../../showcase/native/src/reference-flow-previews.tsx -->
|
|
51
|
+
|
|
52
|
+
이 이름의 독립 공개 컴포넌트는 없다. 아래는 공개 `createActionSession`과 입력·Steps·Sheet를 조합하는 예제다. Storybook 예제 파일을 제품에서 import하지 않는다.
|
|
53
|
+
|
|
54
|
+
- [Web 실행 예제](../../../../../showcase/web/src/patterns/reference-flow-previews.tsx)의 `Editor(kind="review")`
|
|
55
|
+
- [Native 실행 예제](../../../../../showcase/native/src/reference-flow-previews.tsx)의 `Editor(kind="review")`
|
|
56
|
+
- [공통 검증·초안 비교](../../../../../showcase/shared/reference-flows.ts)의 `draftError`, `sameDraft`는 예제 데이터 규칙이며 제품 규칙으로 교체한다.
|
|
57
|
+
|
|
58
|
+
입력(1) → 검토(2) → 저장 결과 순서다. 검토에서 수정을 누르면 같은 draft로 입력 단계에 돌아간다. 저장은 검토한 draft의 복사본을 캡처한다.
|
|
71
59
|
|
|
72
60
|
```tsx
|
|
73
|
-
// Native
|
|
74
|
-
import {
|
|
75
|
-
import
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
61
|
+
// Web — Native도 같은 상태 골격을 사용한다. JSX/문구/영속 저장은 각 제품의 기존 화면 슬롯에 연결한다.
|
|
62
|
+
import { useState, useSyncExternalStore } from "react";
|
|
63
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
64
|
+
|
|
65
|
+
const [session] = useState(() => createActionSession(initialValue));
|
|
66
|
+
const saved = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
67
|
+
const [draft, setDraft] = useState(initialValue);
|
|
68
|
+
const [stage, setStage] = useState(1);
|
|
69
|
+
const [paused, setPaused] = useState(false);
|
|
70
|
+
const busy = saved.status === "pending";
|
|
71
|
+
const dirty = !sameDraft(draft, saved.value);
|
|
72
|
+
|
|
73
|
+
function review() {
|
|
74
|
+
if (busy || draftError(draft)) return; // 오류 문구를 입력에 연결하고 첫 오류 입력으로 이동
|
|
75
|
+
setStage(2);
|
|
76
|
+
}
|
|
77
|
+
async function save() {
|
|
78
|
+
if (draftError(draft)) return;
|
|
79
|
+
const submitted = { ...draft };
|
|
80
|
+
// pending 중 재호출은 계약이 차단한다. 실패하면 saved.value와 draft를 보존한다.
|
|
81
|
+
const result = await session.run(() => saveToServer(submitted));
|
|
82
|
+
if (result.status === "success") setDraft(result.value);
|
|
83
|
+
}
|
|
84
|
+
function discard() {
|
|
85
|
+
if (session.getSnapshot().status === "pending") return;
|
|
86
|
+
setDraft(saved.value);
|
|
87
|
+
session.reset(saved.value);
|
|
88
|
+
setPaused(true);
|
|
89
|
+
}
|
|
92
90
|
```
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
| 표시/행동 | 연결 |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| 진행 단계 | `Steps.descriptor.currentStepId`를 `stage`에서 도출, 저장 실패 시 `currentStepStatus="error"` |
|
|
95
|
+
| 편집 | `TextField`/선택 컨트롤은 draft를 편집. pending 동안 disabled이며 이미 큐에 든 입력·이탈·되돌리기 콜백도 최신 session 상태로 차단. 변경 시 오래된 실패 표시를 해제 |
|
|
96
|
+
| 검토 | 같은 draft의 요약과 수정 버튼. 요약에서 값을 따로 보관하지 않음 |
|
|
97
|
+
| 저장 | `Button.loading=busy`, 성공 결과는 서버 응답에서, 실패는 Notice와 같은 저장 버튼 재시도 |
|
|
98
|
+
| 되돌리기 | dirty이며 pending이 아닐 때 저장값 복원. 서버 요청 취소로 설명하지 않음 |
|
|
99
|
+
| 중단/이탈 | first/review는 메모리 draft·stage를 유지해 이어하기. settings는 dirty이면 확인 Sheet |
|
|
100
|
+
| 확정 이후 | 결과 확인과 새 편집 진입을 구분. 새 편집은 마지막 확정값으로 시작 |
|
|
95
101
|
|
|
96
102
|
## 플랫폼 차이
|
|
97
103
|
|
|
98
104
|
| 항목 | Web | Native |
|
|
99
105
|
| --- | --- | --- |
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
106
|
+
| 입력/단계 포커스 | DOM 입력·제목 ref | Native 입력 ref와 접근성 포커스 |
|
|
107
|
+
| 결과 알림 | role=status | Android live region + iOS 접근성 알림 |
|
|
108
|
+
| 이탈 확인 | 공개 Sheet의 dismiss/focus 계약 | 공개 Sheet의 dismiss/키보드/복귀 계약 |
|
|
109
|
+
|
|
110
|
+
입력 오류는 입력 ref로 복귀한다. 설정 Sheet를 취소하면 실제 이탈 버튼, 변경을 버려 버튼이 사라지면 유지되는 단계 제목으로 복귀한다. Web은 안정적인 단계 제목/입력 ref에 포커스를 옮기고 `role="status"`로 결과를 알린다. Native는 공개 입력 ref와 단계 제목의 접근성 포커스·알림을 연결하며, unmount 때 예약된 포커스 이동을 취소한다. Sheet의 포커스·키보드·dismiss 정책은 기존 계약에 맡긴다. 제품은 validation 결과, 저장 API, 라우터 이탈 이벤트와 확인 Sheet의 연결, i18n 키를 제공한다.
|
|
111
|
+
|
|
112
|
+
메모리 초안은 이 예제의 mounted 화면 안에서만 유지된다. 새로고침·프로세스 종료·다른 기기 복구와 서버 중복 처리 방지는 제품 저장소와 API 계약이 있어야 한다. `session.reset`은 서버 요청 취소가 아니다.
|
|
103
113
|
|
|
104
114
|
## 함정
|
|
105
115
|
|
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
- 단계: 구성
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
|
-
- 적용:
|
|
6
|
+
- 적용: 구성 API 지침 참조
|
|
7
7
|
- 검토일: 2026-10-06
|
|
8
8
|
- 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/구성/설정/변경 저장과 이탈 확인`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
9
|
- 스토리북: `배포/구성/피드백과 복구/변경 저장과 이탈 확인`
|
|
10
10
|
|
|
11
11
|
## 언제 쓰나
|
|
12
12
|
|
|
13
|
-
저장값과 편집 초안을 비교해 이탈 확인 흐름이 필요할 때 쓴다.
|
|
13
|
+
저장값과 편집 초안을 비교해 이탈 확인 흐름이 필요할 때 쓴다. 입력·단계·저장 세션을 제품 로직에 연결하며 새 데이터 엔진을 만들지 않는다.
|
|
14
14
|
|
|
15
15
|
## 구성 요소
|
|
16
16
|
|
|
17
17
|
| 컴포넌트 | 역할 | 지침 |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
|
|
|
19
|
+
| TextField · createActionSession | 저장값과 편집 초안을 비교해 이탈 확인 | [공개 계약](../components/form.md) |
|
|
20
20
|
| Button | 명시 행동·재시도 | [Button](../components/button.md) |
|
|
21
21
|
|
|
22
22
|
## 배치
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
30
30
|
| --- | --- | --- | --- |
|
|
31
|
-
| 바깥 틀 |
|
|
32
|
-
| 내용 |
|
|
31
|
+
| 바깥 틀 | 부모 화면 또는 포함 Surface | 부모 화면의 해당 슬롯 | [배치](../components/form.md#배치)에 따른다 |
|
|
32
|
+
| 내용 | 부모 화면의 입력·요약 슬롯 | 입력 → 저장·되돌리기 → 이탈 확인 | spacing 토큰과 포함 컴포넌트 recipe |
|
|
33
33
|
| 행동 | Button 또는 공개 콜백 | 내용과 가까운 명시 진입점 | 주 행동 하나, 보조 행동과 구분 |
|
|
34
34
|
|
|
35
35
|
## 흐름과 상태
|
|
@@ -46,60 +46,70 @@
|
|
|
46
46
|
|
|
47
47
|
## 코드 골격
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
onSubmit={async () => {
|
|
60
|
-
setServerError(null);
|
|
61
|
-
if (nicknameError) { nicknameInputRef.current?.focus(); return; } // Web은 첫 오류 포커스를 제품이 한다
|
|
62
|
-
try { await save(values); } catch { setServerError(t("profile.saveFailed")); }
|
|
63
|
-
}}
|
|
64
|
-
formError={serverError}
|
|
65
|
-
actions={<Button type="submit">{t("profile.save")}</Button>}
|
|
66
|
-
>
|
|
67
|
-
<TextField ref={nicknameInputRef} label={t("profile.nickname")} value={values.nickname}
|
|
68
|
-
onValueChange={setNickname} error={nicknameError ? t(nicknameError) : undefined} />
|
|
69
|
-
</Form>
|
|
70
|
-
```
|
|
49
|
+
<!-- usage-example: Web ../../../../../showcase/web/src/patterns/reference-flow-previews.tsx -->
|
|
50
|
+
<!-- usage-example: Native ../../../../../showcase/native/src/reference-flow-previews.tsx -->
|
|
51
|
+
|
|
52
|
+
이 이름의 독립 공개 컴포넌트는 없다. 아래는 공개 `createActionSession`과 입력·Steps·Sheet를 조합하는 예제다. Storybook 예제 파일을 제품에서 import하지 않는다.
|
|
53
|
+
|
|
54
|
+
- [Web 실행 예제](../../../../../showcase/web/src/patterns/reference-flow-previews.tsx)의 `Editor(kind="settings")`
|
|
55
|
+
- [Native 실행 예제](../../../../../showcase/native/src/reference-flow-previews.tsx)의 `Editor(kind="settings")`
|
|
56
|
+
- [공통 검증·초안 비교](../../../../../showcase/shared/reference-flows.ts)의 `draftError`, `sameDraft`는 예제 데이터 규칙이며 제품 규칙으로 교체한다.
|
|
57
|
+
|
|
58
|
+
저장값 session.value와 편집 draft를 비교한다. dirty일 때만 저장·되돌리기를 켜고, 이탈은 확인 Sheet를 연다. 계속 수정은 Sheet만 닫고, 버리기는 마지막 저장값으로 draft를 복원한다.
|
|
71
59
|
|
|
72
60
|
```tsx
|
|
73
|
-
// Native
|
|
74
|
-
import {
|
|
75
|
-
import
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
61
|
+
// Web — Native도 같은 상태 골격을 사용한다. JSX/문구/영속 저장은 각 제품의 기존 화면 슬롯에 연결한다.
|
|
62
|
+
import { useState, useSyncExternalStore } from "react";
|
|
63
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
64
|
+
|
|
65
|
+
const [session] = useState(() => createActionSession(initialValue));
|
|
66
|
+
const saved = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
67
|
+
const [draft, setDraft] = useState(initialValue);
|
|
68
|
+
const [stage, setStage] = useState(1);
|
|
69
|
+
const [paused, setPaused] = useState(false);
|
|
70
|
+
const busy = saved.status === "pending";
|
|
71
|
+
const dirty = !sameDraft(draft, saved.value);
|
|
72
|
+
|
|
73
|
+
function review() {
|
|
74
|
+
if (busy || draftError(draft)) return; // 오류 문구를 입력에 연결하고 첫 오류 입력으로 이동
|
|
75
|
+
setStage(2);
|
|
76
|
+
}
|
|
77
|
+
async function save() {
|
|
78
|
+
if (draftError(draft)) return;
|
|
79
|
+
const submitted = { ...draft };
|
|
80
|
+
// pending 중 재호출은 계약이 차단한다. 실패하면 saved.value와 draft를 보존한다.
|
|
81
|
+
const result = await session.run(() => saveToServer(submitted));
|
|
82
|
+
if (result.status === "success") setDraft(result.value);
|
|
83
|
+
}
|
|
84
|
+
function discard() {
|
|
85
|
+
if (session.getSnapshot().status === "pending") return;
|
|
86
|
+
setDraft(saved.value);
|
|
87
|
+
session.reset(saved.value);
|
|
88
|
+
setPaused(true);
|
|
89
|
+
}
|
|
92
90
|
```
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
| 표시/행동 | 연결 |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| 진행 단계 | `Steps.descriptor.currentStepId`를 `stage`에서 도출, 저장 실패 시 `currentStepStatus="error"` |
|
|
95
|
+
| 편집 | `TextField`/선택 컨트롤은 draft를 편집. pending 동안 disabled이며 이미 큐에 든 입력·이탈·되돌리기 콜백도 최신 session 상태로 차단. 변경 시 오래된 실패 표시를 해제 |
|
|
96
|
+
| 검토 | 같은 draft의 요약과 수정 버튼. 요약에서 값을 따로 보관하지 않음 |
|
|
97
|
+
| 저장 | `Button.loading=busy`, 성공 결과는 서버 응답에서, 실패는 Notice와 같은 저장 버튼 재시도 |
|
|
98
|
+
| 되돌리기 | dirty이며 pending이 아닐 때 저장값 복원. 서버 요청 취소로 설명하지 않음 |
|
|
99
|
+
| 중단/이탈 | first/review는 메모리 draft·stage를 유지해 이어하기. settings는 dirty이면 확인 Sheet |
|
|
100
|
+
| 확정 이후 | 결과 확인과 새 편집 진입을 구분. 새 편집은 마지막 확정값으로 시작 |
|
|
95
101
|
|
|
96
102
|
## 플랫폼 차이
|
|
97
103
|
|
|
98
104
|
| 항목 | Web | Native |
|
|
99
105
|
| --- | --- | --- |
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
106
|
+
| 입력/단계 포커스 | DOM 입력·제목 ref | Native 입력 ref와 접근성 포커스 |
|
|
107
|
+
| 결과 알림 | role=status | Android live region + iOS 접근성 알림 |
|
|
108
|
+
| 이탈 확인 | 공개 Sheet의 dismiss/focus 계약 | 공개 Sheet의 dismiss/키보드/복귀 계약 |
|
|
109
|
+
|
|
110
|
+
입력 오류는 입력 ref로 복귀한다. 설정 Sheet를 취소하면 실제 이탈 버튼, 변경을 버려 버튼이 사라지면 유지되는 단계 제목으로 복귀한다. Web은 안정적인 단계 제목/입력 ref에 포커스를 옮기고 `role="status"`로 결과를 알린다. Native는 공개 입력 ref와 단계 제목의 접근성 포커스·알림을 연결하며, unmount 때 예약된 포커스 이동을 취소한다. Sheet의 포커스·키보드·dismiss 정책은 기존 계약에 맡긴다. 제품은 validation 결과, 저장 API, 라우터 이탈 이벤트와 확인 Sheet의 연결, i18n 키를 제공한다.
|
|
111
|
+
|
|
112
|
+
메모리 초안은 이 예제의 mounted 화면 안에서만 유지된다. 새로고침·프로세스 종료·다른 기기 복구와 서버 중복 처리 방지는 제품 저장소와 API 계약이 있어야 한다. `session.reset`은 서버 요청 취소가 아니다.
|
|
103
113
|
|
|
104
114
|
## 함정
|
|
105
115
|
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
| --- | --- | --- |
|
|
25
25
|
| `Heading level="level3" semanticLevel={2}` | 질문형 제목 `t("share.scopeQuestion")` | [Heading](../components/heading.md) |
|
|
26
26
|
| `Text` | 묶음 설명: 대표 1개 포함 전체 N개 | [Text](../components/text.md) |
|
|
27
|
-
| `
|
|
27
|
+
| `SegmentedControl` | 범위 선택: "대표만 · 1개" / "묶음 전체 · N개". 라벨에 개수를 넣는다 | [SegmentedControl](../components/segmented-control.md) |
|
|
28
28
|
| `Text` | 선택 범위 요약(범위 이름 + 개수) | [Text](../components/text.md) |
|
|
29
29
|
| `Button`(primary) | 적용. 라벨에 개수를 넣는다 `t("share.apply", { count })`. 진행 중은 `loading` | [Button](../components/button.md) |
|
|
30
30
|
| `Text` 상태 | 적용 결과·안내·실패 | [Text](../components/text.md) |
|
|
31
|
-
| `Container`·`Stack` | 바깥 틀, 바깥 `gap="md"`,
|
|
31
|
+
| `Container`·`Stack` | 바깥 틀, 바깥 `gap="md"`, 선택지 내부는 SegmentedControl recipe | [Container](../components/container.md), [Stack](../components/stack.md) |
|
|
32
32
|
|
|
33
33
|
## 배치
|
|
34
34
|
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
| --- | --- | --- | --- |
|
|
54
54
|
| 바깥 틀 | Web: `Container size="reading"` → `Stack gap="md"`. Native: `ScrollView` → `Container` → `Stack gap="md"` | 화면 본문 스크롤. 안전 영역은 화면 host(SafeArea·navigation header)가 준다. 입력이 없어 키보드 처리는 없다. 적용 버튼을 하단에 고정하면 [BottomCTA](../components/bottom-cta.md)가 하단 안전 영역을 맡는다 | Native `ScrollView` 위아래 `spacing.md` 16, 좌우는 `Container` `gutter`(폭 600 미만 `compact` 16, 이상 `regular` 20). 최대 폭 `layout.readingMaxWidth` 720 |
|
|
55
55
|
| 질문·설명 | `Heading` + `Text` | 맨 위 | 사이 `spacing.md` 16 |
|
|
56
|
-
| 범위 선택 | `
|
|
56
|
+
| 범위 선택 | `SegmentedControl` 단일 선택 | 설명 아래, 세로 묶음 | 각 `medium` 44(`control.buttonHeight`), 서로 `spacing.sm` 12 |
|
|
57
57
|
| 범위 요약 | `Text` | 범위 선택 아래 | `spacing.md` 16 |
|
|
58
58
|
| 주 행동 | `Button` 적용 | 요약 아래 | `medium` 44, `spacing.md` 16 |
|
|
59
59
|
| 결과 | `Text` 상태 | 맨 아래 | `spacing.md` 16 |
|
|
60
60
|
|
|
61
|
-
-
|
|
61
|
+
- 범위는 이름 있는 하나의 선택 그룹이다. 다른 덩어리와 `spacing.md` 16으로 띄우고 내부 간격은 SegmentedControl recipe를 따른다.
|
|
62
62
|
- 순서는 좁은 범위(대표) → 넓은 범위(전체)다. 기본 선택은 스토리에서 대표 항목이지만 제품이 정한다.
|
|
63
|
-
- 범위
|
|
63
|
+
- 범위 선택은 SegmentedControl의 선택 의미와 외형을 사용한다. 화면의 primary 행동은 적용 하나다.
|
|
64
64
|
- 적용 버튼이 화면 하단 고정이면 [BottomCTA](../components/bottom-cta.md)에 두고 라벨의 개수는 유지한다.
|
|
65
65
|
|
|
66
66
|
근거: `src/component-recipes.ts`(`stackRecipe.gaps` = `spacing`, `buttonRecipe.states.selected`), `src/foundations.ts`(`control`·`layout`)
|
|
@@ -68,15 +68,15 @@
|
|
|
68
68
|
## 흐름과 상태
|
|
69
69
|
|
|
70
70
|
1. 화면은 묶음 설명과 기본 범위(대표)를 보여 준다.
|
|
71
|
-
2. 사용자가
|
|
72
|
-
3. 적용을 누르면 그 범위의 항목 ID로 제품 작업(공유 대상 확정 등)을 실행한다. 끝날 때까지 적용 버튼은 `loading
|
|
71
|
+
2. 사용자가 범위를 고르면 `value`가 바뀌고, 요약과 적용 버튼 라벨의 개수가 바뀐다. 이전 결과 문구는 지운다.
|
|
72
|
+
3. 적용을 누르면 그 범위의 항목 ID로 제품 작업(공유 대상 확정 등)을 실행한다. 끝날 때까지 적용 버튼은 `loading`이고 선택한 개수의 처리 중 상태를 표시한다. 비활성 렌더 전에 도착한 선택 callback도 현재 세션이 pending이면 거부한다.
|
|
73
73
|
4. 결과 문구로 몇 개에 적용했는지 알린다. 실패하면 범위는 그대로 두고 실패 문구를 알린다. 적용을 다시 누르면 같은 범위로 재요청한다.
|
|
74
74
|
|
|
75
75
|
| 상태 | 모습 | 포커스·알림 |
|
|
76
76
|
| --- | --- | --- |
|
|
77
|
-
| 기본 | 기본
|
|
77
|
+
| 기본 | 기본 범위를 `value`로 지정, 요약에 개수, 안내 문구 `share.chooseScope` | 그룹 이름과 선택 항목을 Web radio/Native radio 접근성 의미로 알림 |
|
|
78
78
|
| 범위 변경 | 요약·적용 라벨의 개수 갱신, 결과 문구는 안내로 돌아감 | 같음 |
|
|
79
|
-
| 진행 중 | 적용 버튼 `loading`(누름 막힘·스피너),
|
|
79
|
+
| 진행 중 | 적용 버튼 `loading`(누름 막힘·스피너), 선택 그룹 `disabled`, 제출 개수의 처리 중 문구 | 포커스는 적용 버튼 유지 |
|
|
80
80
|
| 성공 | 결과 문구 `t("share.applied", { count })` | 결과 알림, 포커스는 적용 버튼 유지 |
|
|
81
81
|
| 실패 | 네트워크·서버 실패: 범위 유지, 결과 문구 `share.applyFailed`, 적용 버튼 다시 활성. 재요청도 실패하면 같은 문구를 다시 알린다 | 결과 알림(Web `role="status"`, Native live region + iOS 알림) |
|
|
82
82
|
|
|
@@ -87,46 +87,49 @@
|
|
|
87
87
|
|
|
88
88
|
```tsx
|
|
89
89
|
// Web
|
|
90
|
-
import { useState } from "react";
|
|
90
|
+
import { useState, useSyncExternalStore } from "react";
|
|
91
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
91
92
|
import { Button } from "@hjmds/react/actions";
|
|
93
|
+
import { SegmentedControl } from "@hjmds/react/selection";
|
|
92
94
|
import { Heading } from "@hjmds/react/heading";
|
|
93
95
|
import { Container, Stack, Text } from "@hjmds/react/layout";
|
|
94
96
|
|
|
95
97
|
const scopeKey = { cover: "share.scope.cover", group: "share.scope.group" } as const;
|
|
96
|
-
const resultKey = { idle: "share.chooseScope", applied: "share.applied", failed: "share.applyFailed" } as const;
|
|
98
|
+
const resultKey = { idle: "share.chooseScope", applying: "share.applying", applied: "share.applied", failed: "share.applyFailed" } as const;
|
|
97
99
|
|
|
98
100
|
const [scope, setScope] = useState<"cover" | "group">("cover");
|
|
99
|
-
const [
|
|
101
|
+
const [session] = useState(() => createActionSession<"cover" | "group" | null>(null));
|
|
102
|
+
const snapshot = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
103
|
+
const result = snapshot.status === "pending" ? "applying" : snapshot.status === "error" ? "failed" : snapshot.status === "success" ? "applied" : "idle";
|
|
100
104
|
const count = scope === "cover" ? 1 : group.items.length;
|
|
101
|
-
|
|
105
|
+
// The owner also guards queued callbacks that arrive before disabled renders.
|
|
106
|
+
const choose = (next: "cover" | "group") => { if (session.getSnapshot().status === "pending") return; setScope(next); session.reset(null); };
|
|
102
107
|
const apply = async () => {
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
const submitted = scope;
|
|
109
|
+
await session.run(async () => { await onApply(submitted); return submitted; });
|
|
105
110
|
};
|
|
106
111
|
|
|
107
112
|
<Container size="reading">
|
|
108
113
|
<Stack gap="md">
|
|
109
114
|
<Heading level="level3" semanticLevel={2}>{t("share.scopeQuestion")}</Heading>
|
|
110
115
|
<Text as="p">{t("share.groupSummary", { total: group.items.length })}</Text>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<Button tone="secondary" selected={scope === "group"} disabled={result === "applying"} onClick={() => choose("group")}>
|
|
116
|
-
{t("share.wholeGroup", { count: group.items.length })}
|
|
117
|
-
</Button>
|
|
118
|
-
</Stack>
|
|
116
|
+
<SegmentedControl label={t("share.scopeQuestion")} value={scope} onValueChange={value => { if (value === "cover" || value === "group") choose(value); }}
|
|
117
|
+
presentation="pills" disabled={result === "applying"}
|
|
118
|
+
items={[{ value: "cover", label: t("share.coverOnly", { count: 1 }) },
|
|
119
|
+
{ value: "group", label: t("share.wholeGroup", { count: group.items.length }) }]} />
|
|
119
120
|
<Text as="p">{t("share.scopeSummary", { scope: t(scopeKey[scope]), count })}</Text>
|
|
120
121
|
<Button loading={result === "applying"} onClick={() => void apply()}>{t("share.apply", { count })}</Button>
|
|
121
|
-
<Text as="p" role="status">{t(resultKey[result
|
|
122
|
+
<Text as="p" role="status">{t(resultKey[result], { count })}</Text>
|
|
122
123
|
</Stack>
|
|
123
124
|
</Container>
|
|
124
125
|
```
|
|
125
126
|
|
|
126
127
|
```tsx
|
|
127
128
|
// Native
|
|
129
|
+
import { StatusText } from "@hjmds/react-native/status-text";
|
|
128
130
|
import { ScrollView, useWindowDimensions } from "react-native";
|
|
129
131
|
import { Button } from "@hjmds/react-native/actions";
|
|
132
|
+
import { SegmentedControl } from "@hjmds/react-native/inputs";
|
|
130
133
|
import { Heading } from "@hjmds/react-native/heading";
|
|
131
134
|
import { Container, Stack, Text } from "@hjmds/react-native/primitives";
|
|
132
135
|
import { useHjmNativeTheme } from "@hjmds/react-native/provider";
|
|
@@ -141,23 +144,19 @@ const gutter = resolveWindowClass(useWindowDimensions().width) === "compact" ? "
|
|
|
141
144
|
<Stack gap="md">
|
|
142
145
|
<Heading level="level3" semanticLevel={2}>{t("share.scopeQuestion")}</Heading>
|
|
143
146
|
<Text>{t("share.groupSummary", { total: group.items.length })}</Text>
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<Button tone="secondary" selected={scope === "group"} disabled={result === "applying"} onPress={() => choose("group")}>
|
|
149
|
-
{t("share.wholeGroup", { count: group.items.length })}
|
|
150
|
-
</Button>
|
|
151
|
-
</Stack>
|
|
147
|
+
<SegmentedControl label={t("share.scopeQuestion")} value={scope} onValueChange={value => { if (value === "cover" || value === "group") choose(value); }}
|
|
148
|
+
presentation="pills" disabled={result === "applying"}
|
|
149
|
+
items={[{ value: "cover", label: t("share.coverOnly", { count: 1 }) },
|
|
150
|
+
{ value: "group", label: t("share.wholeGroup", { count: group.items.length }) }]} />
|
|
152
151
|
<Text>{t("share.scopeSummary", { scope: t(scopeKey[scope]), count })}</Text>
|
|
153
152
|
<Button loading={result === "applying"} onPress={() => void apply()}>{t("share.apply", { count })}</Button>
|
|
154
|
-
<StatusText>{t(resultKey[result
|
|
153
|
+
<StatusText>{t(resultKey[result], { count })}</StatusText>
|
|
155
154
|
</Stack>
|
|
156
155
|
</Container>
|
|
157
156
|
</ScrollView>
|
|
158
157
|
```
|
|
159
158
|
|
|
160
|
-
`group`·`onApply`는 제품 소유다. `StatusText`는 [저장과 재시도의 공통 절](action-recovery-save.md#공통-세션과-상태-알림)
|
|
159
|
+
`group`·`onApply`는 제품 소유다. `StatusText`는 [저장과 재시도의 공통 절](action-recovery-save.md#공통-세션과-상태-알림)에서 사용하는 공개 Text companion이다.
|
|
161
160
|
|
|
162
161
|
## 플랫폼 차이
|
|
163
162
|
|
|
@@ -165,10 +164,24 @@ const gutter = resolveWindowClass(useWindowDimensions().width) === "compact" ? "
|
|
|
165
164
|
| --- | --- | --- |
|
|
166
165
|
| 바깥 틀 | 문서 스크롤 + `Container` | `ScrollView`(위아래 `spacing.md`) + `Container` |
|
|
167
166
|
| 이벤트 | `onClick` | `onPress` |
|
|
168
|
-
|
|
|
167
|
+
| 단일 선택 의미 | 이름 있는 radiogroup, radio checked | 이름 있는 그룹과 radio checked |
|
|
169
168
|
| 상태 알림 | `role="status"` | live region(Android) + iOS 알림 호출(`StatusText`) |
|
|
170
169
|
|
|
171
170
|
## 함정
|
|
172
171
|
|
|
173
172
|
- 구획 제목은 Heading으로 표시한다. 이전 Text heading 예제는 2026-10-06 제목 의미 구조를 맞추면서 수정했다.
|
|
174
173
|
- 스토리의 "다음 적용 실패시키기"(ghost)와 350ms 지연은 실패·진행 중을 확인하는 데모 전용이다. 제품에 넣지 않는다.
|
|
174
|
+
|
|
175
|
+
## 단일 선택 계약
|
|
176
|
+
|
|
177
|
+
2026-10-13 실행 예제는 대표/전체를 두 독립 토글 대신 `SegmentedControl presentation="pills"`로 묶었다. 둘 중 하나인 선택은 그룹 이름과 checked 의미를 전달한다. 위 골격과 같은 공개 선택 계약을 유지한다. 저장·실패·초안은 기존 세션을 유지한다.
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
// Web: @hjmds/react/selection, Native: @hjmds/react-native/inputs
|
|
181
|
+
<SegmentedControl label={t("share.scopeQuestion")} value={scope} onValueChange={value => { if (value === "cover" || value === "group") choose(value); }}
|
|
182
|
+
presentation="pills" disabled={result === "applying"}
|
|
183
|
+
items={[{ value: "cover", label: t("share.coverOnly", { count: 1 }) },
|
|
184
|
+
{ value: "group", label: t("share.wholeGroup", { count: group.items.length }) }]} />
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
2026-10-13 실제 Story 검사에서 Native의 늦은 선택 callback이 pending 요청을 reset하는 경우를 재현했다. Web·Native 예제 모두 세션 snapshot으로 선택 변경과 실패 예약을 즉시 거부한다. 새 범위 선택은 이전 성공 문구를 지우고, 실패 후 재적용은 기존 선택 범위로 요청한다. 이 예제에는 별도 텍스트 초안이 없으며 선택값 자체가 적용 전 초안이다.
|