@hjmds/design-contracts 1.20.0 → 1.22.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.
Files changed (131) hide show
  1. package/dist/authoring-tools.d.ts +38 -0
  2. package/dist/authoring-tools.d.ts.map +1 -0
  3. package/dist/authoring-tools.js +35 -0
  4. package/dist/authoring-tools.js.map +1 -0
  5. package/dist/behaviors.d.ts +33 -3
  6. package/dist/behaviors.d.ts.map +1 -1
  7. package/dist/behaviors.js +2 -0
  8. package/dist/behaviors.js.map +1 -1
  9. package/dist/calendar.d.ts +9 -1
  10. package/dist/calendar.d.ts.map +1 -1
  11. package/dist/calendar.js +4 -2
  12. package/dist/calendar.js.map +1 -1
  13. package/dist/catalog.d.ts +129 -2
  14. package/dist/catalog.d.ts.map +1 -1
  15. package/dist/catalog.js +6 -2
  16. package/dist/catalog.js.map +1 -1
  17. package/dist/chart.d.ts +398 -0
  18. package/dist/chart.d.ts.map +1 -0
  19. package/dist/chart.js +414 -0
  20. package/dist/chart.js.map +1 -0
  21. package/dist/collection-editing.d.ts +38 -0
  22. package/dist/collection-editing.d.ts.map +1 -0
  23. package/dist/collection-editing.js +53 -0
  24. package/dist/collection-editing.js.map +1 -0
  25. package/dist/communication.d.ts +29 -0
  26. package/dist/communication.d.ts.map +1 -0
  27. package/dist/communication.js +11 -0
  28. package/dist/communication.js.map +1 -0
  29. package/dist/component-definitions.d.ts +1 -0
  30. package/dist/component-definitions.d.ts.map +1 -1
  31. package/dist/component-definitions.js +1 -0
  32. package/dist/component-definitions.js.map +1 -1
  33. package/dist/data-table.d.ts +229 -5
  34. package/dist/data-table.d.ts.map +1 -1
  35. package/dist/data-table.js +242 -6
  36. package/dist/data-table.js.map +1 -1
  37. package/dist/data-workflows.d.ts +70 -0
  38. package/dist/data-workflows.d.ts.map +1 -0
  39. package/dist/data-workflows.js +109 -0
  40. package/dist/data-workflows.js.map +1 -0
  41. package/dist/dataviz.d.ts +7 -6
  42. package/dist/dataviz.d.ts.map +1 -1
  43. package/dist/dataviz.js +7 -6
  44. package/dist/dataviz.js.map +1 -1
  45. package/dist/gooey-navigation.d.ts +7 -0
  46. package/dist/gooey-navigation.d.ts.map +1 -1
  47. package/dist/gooey-navigation.js +10 -0
  48. package/dist/gooey-navigation.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/media-transport.d.ts +16 -0
  54. package/dist/media-transport.d.ts.map +1 -0
  55. package/dist/media-transport.js +17 -0
  56. package/dist/media-transport.js.map +1 -0
  57. package/dist/recipes.d.ts +1 -0
  58. package/dist/recipes.d.ts.map +1 -1
  59. package/dist/recipes.js +1 -0
  60. package/dist/recipes.js.map +1 -1
  61. package/dist/showcase.d.ts.map +1 -1
  62. package/dist/showcase.js +3 -1
  63. package/dist/showcase.js.map +1 -1
  64. package/dist/social-compositions.d.ts +19 -0
  65. package/dist/social-compositions.d.ts.map +1 -0
  66. package/dist/social-compositions.js +33 -0
  67. package/dist/social-compositions.js.map +1 -0
  68. package/dist/streaming-answer.d.ts +18 -0
  69. package/dist/streaming-answer.d.ts.map +1 -0
  70. package/dist/streaming-answer.js +61 -0
  71. package/dist/streaming-answer.js.map +1 -0
  72. package/dist/task-controls.d.ts +31 -0
  73. package/dist/task-controls.d.ts.map +1 -0
  74. package/dist/task-controls.js +25 -0
  75. package/dist/task-controls.js.map +1 -0
  76. package/dist/toggle-feedback.d.ts +22 -0
  77. package/dist/toggle-feedback.d.ts.map +1 -0
  78. package/dist/toggle-feedback.js +32 -0
  79. package/dist/toggle-feedback.js.map +1 -0
  80. package/dist/transfer-task.d.ts +13 -0
  81. package/dist/transfer-task.d.ts.map +1 -0
  82. package/dist/transfer-task.js +13 -0
  83. package/dist/transfer-task.js.map +1 -0
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/dist/version.js.map +1 -1
  87. package/docs/authoring-tools.md +31 -0
  88. package/docs/calendar.md +3 -1
  89. package/docs/catalog-freeze.json +5 -0
  90. package/docs/chart.md +206 -0
  91. package/docs/collection-editing.md +83 -0
  92. package/docs/communication.md +35 -0
  93. package/docs/data-table.md +56 -6
  94. package/docs/data-workflows.md +26 -0
  95. package/docs/generated/component-maturity.md +6 -5
  96. package/docs/generated/renderer-evidence.json +95 -5
  97. package/docs/generated/renderer-evidence.md +6 -3
  98. package/docs/generated/showcase-manifest.json +66 -3
  99. package/docs/gooey-navigation.md +20 -0
  100. package/docs/library-gap-analysis.md +4 -2
  101. package/docs/media-transport.md +37 -0
  102. package/docs/social-compositions.md +51 -0
  103. package/docs/streaming-answer.md +57 -0
  104. package/docs/task-controls.md +38 -0
  105. package/docs/toggle-feedback.md +56 -0
  106. package/docs/transfer-task.md +33 -0
  107. package/docs/usage/README.md +15 -4
  108. package/docs/usage/components/activity-chip.md +2 -2
  109. package/docs/usage/components/button.md +39 -0
  110. package/docs/usage/components/calendar.md +4 -2
  111. package/docs/usage/components/chart.md +277 -0
  112. package/docs/usage/components/data-table.md +96 -18
  113. package/docs/usage/components/screen-layout.md +3 -1
  114. package/docs/usage/components/search-screen.md +1 -0
  115. package/docs/usage/components/tabs.md +25 -0
  116. package/docs/usage/components/toast.md +1 -1
  117. package/docs/usage/compositions/action-feedback.md +18 -2
  118. package/docs/usage/compositions/activity-status.md +2 -2
  119. package/docs/usage/compositions/authoring-tools.md +90 -0
  120. package/docs/usage/compositions/collection-editing.md +110 -0
  121. package/docs/usage/compositions/communication.md +100 -0
  122. package/docs/usage/compositions/data-workflows.md +91 -0
  123. package/docs/usage/compositions/media-transport.md +87 -0
  124. package/docs/usage/compositions/social-compositions.md +91 -0
  125. package/docs/usage/compositions/streaming-answer.md +97 -0
  126. package/docs/usage/compositions/task-controls.md +93 -0
  127. package/docs/usage/compositions/toggle-feedback.md +84 -0
  128. package/docs/usage/compositions/transfer-task.md +86 -0
  129. package/docs/usage/compositions/web-additions.md +2 -2
  130. package/docs/usage/screens/common-shell.md +4 -0
  131. package/package.json +67 -1
@@ -88,12 +88,12 @@ const feedback = resolveSaveFeedback({
88
88
 
89
89
  ```tsx
90
90
  // Web
91
- <Button loading={feedback.pending} disabled={!feedback.canSubmit} onClick={save}>{feedback.pending ? pendingLabel : saveLabel}</Button>
91
+ <Button loading={feedback.pending} disabled={!feedback.canSubmit} completion={{ confirmed: feedback.currentSaved, label: savedLabel }} onClick={save}>{saveLabel}</Button>
92
92
  ```
93
93
 
94
94
  ```tsx
95
95
  // Native
96
- <Button loading={feedback.pending} disabled={!feedback.canSubmit} onPress={save}>{feedback.pending ? pendingLabel : saveLabel}</Button>
96
+ <Button loading={feedback.pending} disabled={!feedback.canSubmit} completion={{ confirmed: feedback.currentSaved, label: savedLabel }} onPress={save}>{saveLabel}</Button>
97
97
  ```
98
98
 
99
99
  Web은 `@hjmds/react`의 해당 granular entry, Native는 `@hjmds/react-native` entry를 쓴다.
@@ -111,3 +111,19 @@ Button의 실행 콜백은 Web `onClick`, Native `onPress`로 연결한다. 위
111
111
  | --- | --- | --- |
112
112
  | 배치/테마 | Stack과 HjmProvider | Stack과 HjmNativeProvider |
113
113
  | 좁은 폭 | 줄바꿈·단일 내용 | 같은 순서, OS 화면 검증은 별도 |
114
+
115
+
116
+ ## 확정 라벨의 크기 예약 (npm 1.21.0 · 2026-10-15)
117
+
118
+ Lucian 001의 저장→진행→완료 프레임 유지를 기존 Button의 `completion` 옵션으로 보완한다.
119
+ 기본 행동 문자열은 항상 같은 children으로 두고, 완료 문자열은 `completion.label`로 처음부터
120
+ 알린다. `confirmed`는 `resolveSaveFeedback.currentSaved`처럼 **현재 입력과 마지막 확정
121
+ 응답**을 비교한 결과다. 요청 진행 중 편집했다면 이전 응답이 성공해도 완료 라벨을 표시하지
122
+ 않는다. 저장 중에는 중앙 로딩만 보이고, 실패하면 같은 기본 행동 문자열로 돌아온다.
123
+ 서버가 확정하기 전에는 완료를 표시하지 않으며 자동 성공 타이머·중복 mutation 엔진을 넣지 않는다.
124
+
125
+ Web은 두 라벨을 겹쳐 측정하고 Native는 두 라벨의 고유 폭과 최대 높이를 미리 예약한다.
126
+ 자세한 초기 Native 행 예약과 문자열 제한은 [Button](../components/button.md#저장-확정-라벨과-프레임-예약-npm-1210--2026-10-15)을 따른다.
127
+ 기존 결과 영역이 live region을 소유한다. Button이 별도 자동 알림을 만들지 않아 저장 중·완료
128
+ 안내가 중복 발표되지 않는다. Showcase는 같은 공개 Button API를 사용하며, 예제 요청임을
129
+ 계속 밝힌다. 실제 제품은 자신의 네트워크 요청·정규화된 확정 응답을 기존 session에 전달한다.
@@ -1,12 +1,12 @@
1
1
  # 백그라운드 작업 상태
2
2
 
3
3
  - 단계: 구성
4
- - 상태: 실험
4
+ - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 미게시(1.17.2 이후)
7
7
  - 검토일: 2026-10-13
8
8
  - 근거: [ActivityChip 계약](../../activity-chip.md), [Toast 패널](../../toast.md#패널-toast--미게시1172-이후), 양 Showcase `activity-status-preview.tsx`, `showcase/shared/activity-status.ts`
9
- - 스토리북: `실험/구성/피드백과 복구/백그라운드 작업 상태`
9
+ - 스토리북: `배포/구성/피드백과 복구/백그라운드 작업 상태`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
@@ -0,0 +1,90 @@
1
+ # 베지어·단축키 편집 도구
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-15
8
+ - 근거: [편집 도구 계약](../../authoring-tools.md), `src/authoring-tools.ts`, 양 renderer `src/authoring-tools.tsx`
9
+ - 스토리북: `배포/구성/직접 조작과 모션/베지어 이징 편집`, Web `배포/구성/직접 조작과 모션/단축키 조합 기록`
10
+
11
+ ShortcutRecorder는 Web 전용이다. Native는 BezierEditor만 지원한다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 제품 안의 모션 편집기에서 두 제어점의 곡선을 조정하거나 웹 앱의 사용자 단축키를 실제로 등록할 때 쓴다. 단순 조합 표시는 기존 TextFormat을 사용한다. 제품 토큰 저장·전역 키 등록 서비스는 소비 제품이 소유한다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | BezierEditor | 네 좌표 controlled 편집·실제 미리 보기 | [계약](../../authoring-tools.md) |
22
+ | ShortcutRecorder | 실제 키 조합 기록·검사·등록 요청 | [계약](../../authoring-tools.md) |
23
+ | Slider | 좌표별 값과 조절 동작 | [Slider](../components/slider.md) |
24
+ | Button | 미리 보기·기록·등록·취소 | [Button](../components/button.md) |
25
+
26
+ ## 배치
27
+
28
+ ```text
29
+ [곡선·두 제어점]
30
+ [x1 Slider] [y1 Slider] [x2 Slider] [y2 Slider]
31
+ [이동 미리 보기 영역]
32
+ [미리 보기]
33
+
34
+ [현재 등록값 / 기록 중 포커스 영역]
35
+ [기록] [초안 등록] [취소]
36
+ [상태 안내 / 중앙 등록 오류]
37
+ ```
38
+
39
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
40
+ | --- | --- | --- | --- |
41
+ | 바깥 틀 | Stack | 편집 패널 본문 | md |
42
+ | 곡선 | SVG | 좌표 위 | 본문 폭, 높이 180 |
43
+ | 좌표 | Slider 4개 | 세로 흐름 | 기존 Slider 계약 |
44
+ | 키 기록 | 포커스 group | 버튼 위 | 최소 높이 44, 내용 중앙 |
45
+ | 오류 | Text alert | 구성 내부 | 가로·세로 중앙 |
46
+
47
+ ## 흐름과 상태
48
+
49
+ 1. BezierEditor는 controlled 초안을 네 좌표 모두 편집하고 버튼으로 현재 곡선의 움직임을 재생한다.
50
+ 2. 초안을 제품 토큰에 자동 기록하지 않는다. 별도 저장 흐름으로 연결한다.
51
+ 3. ShortcutRecorder 기록 버튼으로 실제 키를 받는다. 예약·충돌·잘못된 조합은 등록하지 않는다.
52
+ 4. 확인한 초안을 등록하면 실제 서비스 Promise를 기다린다. 실패 시 초안을 보존한다.
53
+
54
+ | 상태 | 모습 | 포커스·알림 |
55
+ | --- | --- | --- |
56
+ | 기본 | controlled 값 표시 | Slider·버튼 기존 동작 |
57
+ | 키 기록 | 포커스 영역·기록 안내 | Escape 취소, Tab/blur 종료 |
58
+ | 진행 중 | 등록 버튼 로딩 | 중복 등록 차단 |
59
+ | 실패 | 기존 값과 초안 보존 | 중앙 alert |
60
+
61
+ ## 코드 골격
62
+
63
+ ```tsx
64
+ // Web
65
+ import { BezierEditor, ShortcutRecorder } from "@hjmds/react/authoring-tools";
66
+ <BezierEditor value={curve} onValueChange={setCurve} labels={curveLabels} formatCoordinate={formatNumber} />;
67
+ <ShortcutRecorder value={registeredShortcut} onRegister={registerActualShortcut} platform={platform}
68
+ conflicts={existingShortcuts} reserved={platformReservations} labels={shortcutLabels} formatShortcut={formatShortcut} />;
69
+ ```
70
+
71
+ ```tsx
72
+ // Native
73
+ import { BezierEditor } from "@hjmds/react-native/authoring-tools";
74
+ <BezierEditor value={curve} onValueChange={setCurve} labels={curveLabels} formatCoordinate={formatNumber} />;
75
+ // ShortcutRecorder is unsupported on Native.
76
+ ```
77
+
78
+ 실행 예제는 Web `showcase/web/src/patterns/AuthoringTools.previews.tsx`, Native `showcase/native/src/authoring-tools-preview.tsx`다. Web 예제는 등록 callback에서 실제 keydown listener를 설치하고 실행 횟수를 보여 준다.
79
+
80
+ ## 플랫폼 차이
81
+
82
+ | 항목 | Web | Native |
83
+ | --- | --- | --- |
84
+ | 곡선 | CSS cubic-bezier / WAAPI | Animated / Easing.bezier |
85
+ | 단축키 | KeyboardEvent·제품 등록 callback | 지원하지 않음 |
86
+ | 백그라운드 | visibilitychange로 재생 중단 | AppState로 중단 |
87
+
88
+ ## 접근성과 검증
89
+
90
+ 각 좌표의 이름·값·증감 안내를 제품이 제공한다. reduced motion에서는 preview 이동을 재생하지 않는다. 키 조합 표시도 제품의 언어·레이아웃을 따른다. 계약 계산, Web 키 이벤트와 Promise 실패, Native 좌표 변경·preview host 호출을 검사한다. 실제 Native 기기와 제품별 예약키·전역 키 서비스 검증은 소비 단계에서 확인한다.
@@ -0,0 +1,110 @@
1
+ # 목록 편집과 복구
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-15
8
+ - 근거: [목록 편집과 복구 계약](../../collection-editing.md), `src/collection-editing.ts`, 양 renderer의 `src/collection-editing.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/제목 바로 편집`, `배포/구성/입력과 작성/반복 입력`, `배포/구성/선택과 필터/선택 항목 묶음 작업`, `배포/구성/피드백과 복구/검색 필터 완화`, `배포/구성/선택과 필터/표 열 설정`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 목록의 제목을 바로 고치거나, 입력 행을 늘리거나, 여러 항목에 같은 행동을 하거나, 빈 검색에서 조건을 풀거나, 표의 열을 설정할 때 쓴다.
14
+ Lucian 예제 전수 검토에서 재사용할 실익이 있는 다섯 작업을 기존 HJM 구성으로 연결했다.
15
+ 다섯 API를 한 화면에 모두 넣는 것이 아니라 사용자 과제에 해당하는 하나를 고른다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | InlineEdit | 값 표시 → 같은 자리 입력·저장·취소 | [계약](../../collection-editing.md) |
22
+ | RepeatableFieldGroup | stable ID로 묶은 입력 행 추가·삭제 | [FieldGroup](field-group.md) |
23
+ | SelectionActionBar | 선택 개수·묶음 행동·작업별 복구 | [보관과 실행 취소](action-recovery-undo.md) |
24
+ | FilterRecovery | 실제 결과가 나오는 필터 완화 제안 | [검색 화면](../components/search-screen.md) |
25
+ | TableColumnSettings | 보이는 열·순서를 제어 | [DataTable](../components/data-table.md) |
26
+
27
+ ## 배치
28
+
29
+ ```text
30
+ 제목 편집 반복 입력 묶음 행동
31
+ 값 이름 [입력] N개 선택
32
+ [수정] [이 행 삭제] [보관] [해제]
33
+ ↓ [사람 추가] [방금 작업 복구]
34
+ [입력]
35
+ [취소] [저장]
36
+
37
+ 필터 완화: 자기 본문 영역 중앙 열 설정: 각 열의 선택 아래 앞/뒤 이동
38
+ 제목·설명 [✓ 이름] (고정)
39
+ [주제 해제 · 실제 N개 보기] [✓ 점수] [앞으로] [뒤로]
40
+ 실패 안내 제어된 실제 DataTable
41
+ ```
42
+
43
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
44
+ | --- | --- | --- | --- |
45
+ | 바깥 틀 | 제품 화면 host | 기존 본문·시트·패널 안; host가 스크롤·safe area·키보드를 소유 | 기존 Container/ScreenLayout 규격 |
46
+ | 입력/행 | TextField, FieldGroup | 값과 같은 자리; 반복 행은 위→아래 | `collectionEditingRecipe.gap` = `md` |
47
+ | 행동 | Button | 입력 아래 취소→저장, 행 아래 삭제, 행 묶음 마지막 추가 | `collectionEditingRecipe.actionGap` = `sm`, 좁으면 줄바꿈 |
48
+ | 선택 바 | SelectionActionBar | 대상 목록 바로 아래 또는 제품 footer 슬롯 | 제품이 고정 위치 결정; 화면 전체 좌표를 새로 소유하지 않음 |
49
+ | 상태 | Text·Button | 해당 구성 영역 중앙 | 임의 상단 margin 없이 가로·세로 중앙 정렬 |
50
+
51
+ ## 흐름과 상태
52
+
53
+ 1. 제품이 ID·문구·확정 데이터와 callbacks를 전달한다.
54
+ 2. 편집·선택·제안 클릭에서만 변경을 시작한다. blur는 저장하지 않고 제안은 자동 적용하지 않는다.
55
+ 3. 비동기 callback의 resolve/reject를 기다린다. 성공 결과·선택 해제·항목 복구는 제품 데이터로 반영한다.
56
+ 4. 실패하면 기존 입력·필터·항목을 유지하고 같은 위치에서 다시 실행할 수 있다.
57
+
58
+ | 상태 | 모습 | 포커스·알림 |
59
+ | --- | --- | --- |
60
+ | 기본 | 값/입력/선택 요약/제안/열 선택 | stable ID로 기존 입력 host 유지 |
61
+ | 진행 중 | 실행한 버튼 내부 로딩, 경쟁 행동 잠금 | 중복 실행 차단; 카드 높이를 임의로 접지 않음 |
62
+ | 실패 | 중앙 실패 문구, 재실행 가능한 행동 | Web alert / Native assertive live region; 초안 유지 |
63
+
64
+ ## 코드 골격
65
+
66
+ ```tsx
67
+ // Web. Native는 @hjmds/react-native/collection-editing에서 같은 공개 이름을 가져온다.
68
+ import { InlineEdit, TableColumnSettings } from "@hjmds/react/collection-editing";
69
+
70
+ <InlineEdit id={note.id} value={note.title} label={t("note.title")}
71
+ editLabel={t("edit")} saveLabel={t("save")} cancelLabel={t("cancel")}
72
+ errorLabel={t("saveFailed")} onSave={saveConfirmedTitle} />
73
+
74
+ <TableColumnSettings label={t("table.columns")} columns={settings}
75
+ earlierLabel={column => t("table.earlier", { name: column.label })}
76
+ laterLabel={column => t("table.later", { name: column.label })}
77
+ onColumnsChange={setSettings} />
78
+ // DataTable: settings.filter(c => c.visible).map(c => originalColumnsById[c.id])
79
+ ```
80
+
81
+ ```tsx
82
+ // Native
83
+ import { InlineEdit } from "@hjmds/react-native/collection-editing";
84
+
85
+ <InlineEdit id={note.id} value={note.title} label={t("note.title")}
86
+ editLabel={t("edit")} saveLabel={t("save")} cancelLabel={t("cancel")}
87
+ errorLabel={t("saveFailed")} onSave={saveConfirmedTitle} />
88
+ ```
89
+
90
+ 다섯 구성의 완전한 실행 원본:
91
+
92
+ - Web: `showcase/web/src/patterns/CollectionEditing.previews.tsx`
93
+ - Native: `showcase/native/src/collection-editing-preview.tsx`
94
+
95
+ 각 예제는 공개 import로 구성한다. 제품이 showcase 소스를 import하지 않는다.
96
+
97
+ ## 플랫폼 차이
98
+
99
+ | 항목 | Web | Native |
100
+ | --- | --- | --- |
101
+ | 제목 확정 | Enter(IME 제외)·저장, Escape·취소 | TextInput submit·저장, 취소 버튼 |
102
+ | 행 포커스 | focusRef를 HTML 입력 ref에 연결 | focusRef를 TextInput ref에 연결 |
103
+ | 다중 선택 공급원 | DataTable 또는 CheckboxGroup | CheckboxGroup 등; Native DataTable의 단일 행 강조와 구분 |
104
+
105
+ ## 함정
106
+
107
+ - 원본 데모의 고정 성공·전체 복구를 복사하지 않는다. 실패를 callback에서 삼키면 HJM은 성공으로 해석한다.
108
+ - `onAdd`가 반환한 ID와 새 field ID가 같아야 한다. 입력 값을 배열 index에 저장하지 않는다.
109
+ - 표 원본 descriptor의 pinned·width·sort를 보존한다. 설정 화면의 잠금은 서버 권한 검증을 대체하지 않는다.
110
+ - npm 1.21.0 API다. 소비 제품의 실제 설치 버전을 확인한다.
@@ -0,0 +1,100 @@
1
+ # 펼치는 검색과 실제 메시지 답장
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-15
8
+ - 근거: [연결 계약](../../communication.md), `src/communication.ts`, 양 renderer `src/communication.tsx`, `src/screens.tsx`
9
+ - 스토리북: `배포/구성/선택과 필터/펼치는 상단 검색`, `배포/구성/입력과 작성/메시지 답장 연결`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 상단 검색 버튼에서 검색 영역을 펼치거나 채팅의 실제 메시지를 선택해 답장을 전송할 때 쓴다. 이미 전체 화면으로 제공하는 검색에는 SearchScreen을 바로 쓴다. 입력 자동 높이·채팅 swipe 엔진·최근 검색 저장을 새로 만들지 않는다.
14
+
15
+ ## 구성 요소
16
+
17
+ | 컴포넌트 | 역할 | 지침 |
18
+ | --- | --- | --- |
19
+ | ExpandableSearch | 펼침 상태·실제 요청 Promise·검색 화면 연결 | [계약](../../communication.md) |
20
+ | SearchScreen | 제안·확정·최근 검색·debounce | [SearchScreen](../components/search-screen.md) |
21
+ | ReplyMessage | 실제 ID와 기존 답장 동작 연결 | [계약](../../communication.md) |
22
+ | ReplyComposer | 답장 대상·실제 전송 Promise·실패 보존 | [계약](../../communication.md) |
23
+ | MessageComposer | 기존 자동 높이·선택적 near-limit ring/count | [MessageComposer](../components/message-composer.md) |
24
+
25
+ ## 배치
26
+
27
+ ```text
28
+ [검색 펼치기] → [검색 닫기]
29
+ [기존 SearchScreen 검색어·제안·결과]
30
+ [오류: 검색 영역 중앙]
31
+
32
+ [실제 메시지·답장 동작]
33
+ [실제 답장 저자/발췌·취소]
34
+ [자동 높이 MessageComposer · 전송]
35
+ [임박한 길이 ring/count] [실패: 구성 내부 중앙]
36
+ ```
37
+
38
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
39
+ | --- | --- | --- | --- |
40
+ | 바깥 틀 | Stack | 검색·입력 영역 | sm/md |
41
+ | 펼침·닫기 | Button | 상단 | 기존 버튼 계약 |
42
+ | 검색 | SearchScreen | 펼친 본문 | 기존 검색 영역 구조 |
43
+ | 메시지 | ChatMessage | 기존 timeline | 기존 메시지 구조 |
44
+ | 입력 | MessageComposer | timeline 아래 | 기존 자동 높이 유지 |
45
+ | 실패 | Text alert | 해당 구성 안 | 내용 묶음 중앙 |
46
+
47
+ ## 흐름과 상태
48
+
49
+ 1. 제품은 펼침·query를 관리하고 실제 검색 Promise를 넘긴다. SearchScreen이 debounce와 제안 확정을 맡는다.
50
+ 2. 입력 변경·닫기는 이전 요청을 abort하고 늦은 응답을 무시한다. Enter/제안 확정에서만 최근 검색을 기록한다.
51
+ 3. ReplyMessage는 실제 ID를 제품에 전달한다. 제품은 그 ID를 replyTarget으로 보관한다.
52
+ 4. ReplyComposer는 전송 시작 시점의 ID와 초안을 실제 전송 callback에 넘긴다. 성공 확인과 controlled 값 삭제는 제품이 맡는다.
53
+
54
+ | 상태 | 모습 | 포커스·알림 |
55
+ | --- | --- | --- |
56
+ | 기본 | 검색 버튼 또는 메시지·입력 | 기존 버튼·메시지 계약 |
57
+ | 펼침 | 기존 검색 화면 | 검색 입력 포커스 |
58
+ | 진행 중 | 실제 검색/전송 로딩 | 중복 전송 차단, 동일 입력 보존 |
59
+ | 실패 | 입력·대상 보존 | 영역 중앙 alert |
60
+ | 임박한 길이 | 실제 ring/count | maxLength와 동일 단위 |
61
+
62
+ ## 코드 골격
63
+
64
+ ```tsx
65
+ // Web
66
+ import { ExpandableSearch, ReplyMessage, ReplyComposer } from "@hjmds/react/communication";
67
+ <ExpandableSearch expanded={expanded} onExpandedChange={setExpanded} query={query} onQueryChange={setQuery}
68
+ onSearch={searchActualDocuments} renderResult={renderDocuments} labels={searchLabels} />;
69
+ <ReplyMessage messageId={message.id} direction="incoming" author={message.author} timestamp={message.time}
70
+ replyLabel={replyLabel} onReply={selectActualMessage} swipeAction="reply">{message.body}</ReplyMessage>;
71
+ <ReplyComposer value={draft} onValueChange={setDraft} label={inputLabel} sendLabel={sendLabel}
72
+ replyTarget={selectedMessage} onReplyCancel={clearReply} cancelReplyLabel={cancelReplyLabel} errorLabel={errorLabel}
73
+ onSend={sendActualMessage} maxLength={limit} lengthFeedback={lengthLabels} />;
74
+ ```
75
+
76
+ ```tsx
77
+ // Native
78
+ import { ExpandableSearch, ReplyMessage, ReplyComposer } from "@hjmds/react-native/communication";
79
+ <ExpandableSearch expanded={expanded} onExpandedChange={setExpanded} query={query} onQueryChange={setQuery}
80
+ onSearch={searchActualDocuments} renderResult={renderDocuments} labels={searchLabels} />;
81
+ <ReplyMessage messageId={message.id} direction="incoming" author={message.author} timestamp={message.time}
82
+ replyLabel={replyLabel} onReply={selectActualMessage} swipeAction="timestamp">{message.body}</ReplyMessage>;
83
+ <ReplyComposer value={draft} onValueChange={setDraft} label={inputLabel} sendLabel={sendLabel}
84
+ replyTarget={selectedMessage} onReplyCancel={clearReply} cancelReplyLabel={cancelReplyLabel} errorLabel={errorLabel}
85
+ onSend={sendActualMessage} maxLength={limit} lengthFeedback={lengthLabels} />;
86
+ ```
87
+
88
+ 실행 예제는 Web `showcase/web/src/patterns/Communication.previews.tsx`, Native `showcase/native/src/communication-preview.tsx`다. 검색은 실제 로컬 문서를 필터링하고 메시지는 로컬 수신 adapter에서 성공·실패를 확인한다. 원격 서버에 전송한 결과로 해석하지 않는다.
89
+
90
+ ## 플랫폼 차이
91
+
92
+ | 항목 | Web | Native |
93
+ | --- | --- | --- |
94
+ | 검색 포커스 | 펼침 입력·닫기 버튼으로 복원 | 입력 focus·닫기 키보드 해제 |
95
+ | 답장 대안 | Alt+방향키 | 접근성 reply action |
96
+ | 시간 swipe | reply drag 대신 시간 표시 | 동일 |
97
+
98
+ ## 접근성과 검증
99
+
100
+ 실제 메시지 ID와 보이는 발췌를 분리한다. 가로 swipe와 세로 timeline 스크롤은 기존 ChatMessage의 기준을 따른다. `swipeAction`으로 답장/시간 중 드래그 소유자를 정한다. 길이 안내는 선택 속성이고 기존 자동 높이를 대체하지 않는다. 실제 서버 receipt·기기 키보드·제품 데이터와 npm 게시 여부는 소비 단계에서 확인한다.
@@ -0,0 +1,91 @@
1
+ # 데이터 해석과 가져오기
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-15
8
+ - 근거: [데이터 구성 계약](../../data-workflows.md), `src/data-workflows.ts`, 양 renderer의 `src/data-workflows.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/가져오기 열 연결`, `배포/구성/정보 표시/전환 퍼널`, `배포/구성/정보 표시/코호트 잔존율`, `배포/구성/정보 표시/상관 행렬`, `배포/구성/정보 표시/목표 달성 요약`, `배포/구성/정보 표시/분포 구간 탐색`
10
+
11
+ ## 언제 쓰나
12
+
13
+ CSV 열 연결, 순차 퍼널, 가입 시점별 잔존율, 지표 상관, 목표, 분포 구간 탐색에 쓴다.
14
+ 단순 표/통계/막대 그래프만 필요하면 기존 DataTable/Statistic/BarChart를 직접 쓴다.
15
+ 같은 구성의 비율 계산과 검증을 제품마다 복사하지 않기 위해 도입했다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | ImportFieldMapping | Select, DataTable, Button | 제품 parser·실제 importer와 controlled mapping |
22
+ | ConversionFunnel | BarChart, Button, Text | 제품 순차 count·이탈 사유 |
23
+ | CohortRetentionTable / CorrelationMatrix | DataTable | 실제 분모/관측값·상관값 |
24
+ | TargetSummary | Statistic, Progress | 실제값·목표 |
25
+ | HistogramExplorer | BarChart, Button | 원본 samples·범위·선택 구간 수 |
26
+
27
+ ## 배치
28
+
29
+ ```text
30
+ 가져오기: 원본별 선택 → 검증 → 변환 미리보기 → 실제 가져오기
31
+ 퍼널: 실제 단계 막대 → 단계 선택 → 선택한 단계 설명
32
+ 표: 고정 행 제목 | 기간/변수 열 (가로 스크롤)
33
+ 목표: 실제 수치·설명 → 목표가 있을 때 진행 막대
34
+ 분포: 구간 수 선택 → 동일 y축 분포 → 관측/누락/범위 밖 요약
35
+ ```
36
+
37
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
38
+ | --- | --- | --- | --- |
39
+ | 바깥 틀 | Stack | 기존 본문·카드 안 | md, 목표는 sm |
40
+ | 선택/행동 | Button/Select | 데이터 위 또는 연결 마지막 | Stack inline sm wrap |
41
+ | 상태 | Text | 해당 구성 중앙 | 중앙 정렬, 임의 상단 margin 없음 |
42
+ | 표 | DataTable | 본문 폭 안 스크롤 | 첫 열 pinned start; 대각선도 실제 셀 |
43
+
44
+ ## 흐름과 상태
45
+
46
+ 1. 제품이 실제 데이터와 formatter를 전달한다.
47
+ 2. 가져오기 연결/분포 구간 수는 제품의 controlled 상태로 반영한다.
48
+ 3. 중복 연결·필수 누락이면 미리보기/실행을 막고 실제 원인을 표시한다.
49
+ 4. 가져오기 resolve까지 중복 실행을 차단한다. reject하면 연결은 유지하고 오류 뒤 같은 행동으로 재시도한다.
50
+
51
+ | 상태 | 모습 | 포커스·알림 |
52
+ | --- | --- | --- |
53
+ | 기본 | 실제 데이터와 controlled 선택 | 실제 표/차트의 기존 접근성 계약 |
54
+ | 진행 중 | 가져오기 버튼 내부 로딩 | 중복 실행 차단, 연결 선택 잠금 |
55
+ | 실패 | 연결 유지·중앙 오류 | Web alert / Native assertive, 같은 행동 재시도 |
56
+
57
+ ## 코드 골격
58
+
59
+ ```tsx
60
+ // Web
61
+ import { TargetSummary } from "@hjmds/react/data-workflows";
62
+ <TargetSummary label={t("goal.title")} actual={actual} target={target}
63
+ formatActual={formatCount} summaryLabel={formatGoalSummary} />;
64
+ ```
65
+
66
+ ```tsx
67
+ // Native
68
+ import { TargetSummary } from "@hjmds/react-native/data-workflows";
69
+ <TargetSummary label={t("goal.title")} actual={actual} target={target}
70
+ formatActual={formatCount} summaryLabel={formatGoalSummary} />;
71
+ ```
72
+
73
+ 실행 예제는 Web `showcase/web/src/patterns/DataWorkflows.previews.tsx`, Native `showcase/native/src/data-workflows-preview.tsx`다.
74
+ 각각 공개 진입점을 소비한다. 제품에서 showcase 원본을 import하지 않는다.
75
+
76
+ ## 플랫폼 차이
77
+
78
+ | 항목 | Web | Native |
79
+ | --- | --- | --- |
80
+ | 계산·데이터 | 공유 contracts | 공유 contracts |
81
+ | 연결 선택 | Select popover | Select 창, dismissLabel 필수 |
82
+ | 차트 host | SVG | react-native-svg optional peer |
83
+ Native 차트는 기존 react-native-svg optional peer를 사용한다. 새 제스처나 자체 차트 엔진을 추가하지 않는다.
84
+
85
+ ## 함정
86
+
87
+ - null 미래 관측을 0%로 채우거나 목표 0을 100%로 만들지 않는다.
88
+ - 순차가 아닌 지표를 퍼널에 넣지 않는다. 실제 원인 데이터 없이 이탈 사유를 생성하지 않는다.
89
+ - 히스토그램 전체 선택에서 같은 domain을 유지한다. 누락값·범위 밖 count를 제품 summaryLabel에 표현한다.
90
+ - mapping 변경만으로 저장을 실행하지 않는다. 서버 확정·권한·멱등성은 실제 importer 책임이다.
91
+ - npm 1.21.0에서 제공하며 소비 앱의 실제 설치 버전을 확인한다.
@@ -0,0 +1,87 @@
1
+ # 미디어 재생 제어
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-15
8
+ - 근거: [미디어 제어 계약](../../media-transport.md), `src/media-transport.ts`, 양 renderer `src/media-transport.tsx`
9
+ - 스토리북: `배포/구성/직접 조작과 모션/미디어 재생 제어`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 실제 오디오/영상 엔진의 재생·일시 정지·유한 길이 탐색을 같은 구조로 제공할 때 쓴다.
14
+ 모달에서 영상 host를 표시하는 Dialog나 실제 플레이어 엔진을 대체하지 않는다.
15
+ 단순 이동 버튼이나 일반 수치 입력은 기존 IconButton/Slider를 직접 쓴다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | MediaTransport | 관측 상태와 실제 callback 연결 | [계약](../../media-transport.md) |
22
+ | IconButton | 실제 재생 상태로 모핑하는 제어 | [IconButton](../components/icon-button.md) |
23
+ | Slider | 탐색 초안→실제 seek 확정 | [Slider](../components/slider.md) |
24
+
25
+ ## 배치
26
+
27
+ ```text
28
+ [실제 재생/일시 정지] 현재 시간 / 실제 전체 길이
29
+ [──── 실제 위치 탐색 ────] (길이를 알 때만)
30
+ 제어 실패: 해당 영역 중앙 → 같은 행동으로 재시도
31
+ ```
32
+
33
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
34
+ | --- | --- | --- | --- |
35
+ | 바깥 틀 | Stack | 기존 영상/오디오 host 아래 | md |
36
+ | 제어·시간 | IconButton/Text | inline, 세로 중앙 | sm, 아이콘 24 |
37
+ | 탐색 | Slider | 제어 아래 본문 폭 | 기존 Slider 규격 |
38
+ | 실패 | Text | 구성 내부 중앙 | 임의 상단 여백 없음 |
39
+
40
+ ## 흐름과 상태
41
+
42
+ 1. 제품 엔진 이벤트에서 state를 갱신한다. 클릭은 실제 엔진 callback만 호출한다.
43
+ 2. 실제 playing 값에 따라 버튼 이름·모핑을 갱신한다.
44
+ 3. 탐색 초안은 드래그/키 입력이 끝나면 onSeek에 전달한다. 성공·실패 뒤 관측 위치로 돌아온다.
45
+ 4. 실패한 요청은 현재 상태를 유지한다. 중복 요청은 차단한다.
46
+
47
+ | 상태 | 모습 | 포커스·알림 |
48
+ | --- | --- | --- |
49
+ | 기본 | 실제 playing 아이콘·시간·가능한 탐색 | 기존 버튼·Slider 계약 |
50
+ | 진행 중 | 같은 버튼 내부 로딩·탐색 잠금 | 크기 유지, 중복 실행 차단 |
51
+ | 실패 | 관측값 유지·중앙 오류 | Web alert / Native assertive |
52
+
53
+ ## 코드 골격
54
+
55
+ ```tsx
56
+ // Web
57
+ import { MediaTransport } from "@hjmds/react/media-transport";
58
+ <MediaTransport state={observedMedia} labels={mediaLabels} formatTime={formatTime}
59
+ onPlay={playActualMedia} onPause={pauseActualMedia} onSeek={seekActualMedia} />;
60
+ ```
61
+
62
+ ```tsx
63
+ // Native
64
+ import { MediaTransport } from "@hjmds/react-native/media-transport";
65
+ <MediaTransport state={observedMedia} labels={mediaLabels} formatTime={formatTime}
66
+ onPlay={playActualMedia} onPause={pauseActualMedia} onSeek={seekActualMedia} />;
67
+ ```
68
+
69
+ 실행 예제는 Web `showcase/web/src/patterns/media-transport-preview.tsx`, Native `showcase/native/src/media-transport-preview.tsx`다.
70
+ 제품에서 showcase 원본을 import하지 않고 자체 엔진 이벤트를 공개 API에 연결한다.
71
+
72
+ ## 플랫폼 차이
73
+
74
+ | 항목 | Web | Native |
75
+ | --- | --- | --- |
76
+ | 실제 예제 엔진 | HTMLVideoElement | 기존 Expo Video |
77
+ | 모핑 | SVG d 직접 반영 | react-native-svg host 직접 반영 |
78
+ | 탐색 확정 | seeked/error | 엔진 setter/이벤트 계약 |
79
+ | 숨김 | document visibility | AppState + host active |
80
+
81
+ ## 함정
82
+
83
+ - 클릭만으로 playing을 바꾸거나 타이머로 시간을 더하지 않는다.
84
+ - duration null/0에 가짜 분모나 임의 길이를 넣지 않는다.
85
+ - active=false는 컨트롤 수명이다. 플레이어 pause/dispose도 제품 adapter에서 수행한다.
86
+ - 다른 미디어 엔티티로 전환할 때 key를 바꾸고 이전 플레이어·요청을 정리한다.
87
+ - Native 재생 모듈 없음 안내를 실제 재생 성공으로 기록하지 않는다. 새 API는 npm 1.21.0에서 제공한다.
@@ -0,0 +1,91 @@
1
+ # 팔로우 프로필과 행사 참석 응답
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.21.0
7
+ - 검토일: 2026-10-15
8
+ - 근거: [사회적 행동 계약](../../social-compositions.md), 양 renderer의 `src/social-compositions.tsx`
9
+ - 스토리북: `배포/구성/정보 표시/팔로우 프로필`, `배포/구성/선택과 필터/행사 참석 응답`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 프로필 팔로우나 행사 참석 응답의 실제 요청과 수치의 실패 복구가 필요할 때 사용한다.
14
+ 화면 틀이 필요하면 ProfileScreen의 summary/본문에 넣고, 단순 정보만 보여주면 기존 Card를 쓴다.
15
+ Web은 기존 AnimatedStatistic과 같은 `@number-flow/react` peer가 필요하다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | FollowProfileCard | Card · Avatar · AnimatedStatistic · Button으로 프로필·팔로워·controlled follow 요청 | [Card](../components/card.md) |
22
+ | EventAttendanceCard | Card · Avatar · AnimatedStatistic · Button으로 행사·참석·캘린더 연결 | [Avatar](../components/avatar.md) |
23
+
24
+ ## 배치
25
+
26
+ ```text
27
+ 팔로우 프로필 행사 참석 응답
28
+ [Avatar] 이름 · 소개 제목 · 소개
29
+ 팔로워 수 날짜/시간
30
+ [팔로우 / 팔로잉] 참석 인원 → 확인된 참가자 Avatar + 이름
31
+ 응답 제목 → [참석] [미정] [불참]
32
+ [캘린더 연결]
33
+ 오류: 해당 카드 내부 중앙 오류: 해당 카드 내부 중앙
34
+ ```
35
+
36
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
37
+ | --- | --- | --- | --- |
38
+ | 바깥 틀 | Card | 본문·목록·ProfileScreen 안 | 기존 Card recipe |
39
+ | 프로필 header | Avatar · Card title/description | 상단 leading → 이름·소개 | 기존 Card와 Avatar 기본 크기 |
40
+ | 본문 | AnimatedStatistic · Button · Text | 수치 → 주요 행동 → 오류 | Stack gap=md (`spacing.md`, 16px) |
41
+ | 행사 | Text · AnimatedStatistic · Avatar · Button | 날짜 → 인원 → 참가자 → 응답 → 캘린더 → 오류 | Stack gap=md (`spacing.md`, 16px) |
42
+ | 참가자·응답 행 | Stack | inline, 폭이 모자라면 wrap | gap=xs (`spacing.xs`, 8px) |
43
+ | 오류 | Text | 자신의 안내 영역 가로·세로 중앙 | 임의 상단 margin 없음 |
44
+
45
+ ## 흐름과 상태
46
+
47
+ 1. 제품이 확인한 `state`, 선택지, 문구, locale과 실제 async callback을 전달한다.
48
+ 2. 요청 중에만 제안한 선택/수치를 표시하며 중복 mutation을 막는다.
49
+ 3. 제품은 callback에서 서버 확정 snapshot으로 state를 갱신한 뒤 resolve해야 한다. 성공 snapshot이 예측과 달라도 제품 값이 우선한다.
50
+ 4. 참가자 avatar 목록에는 서버가 확인한 사람만 넣으며 HJM이 가상의 참석자를 추가하지 않는다.
51
+
52
+ | 상태 | 모습 | 포커스·알림 |
53
+ | --- | --- | --- |
54
+ | 기본 | 확인된 선택·수치·참가자 | Button의 기존 선택·키보드 계약 |
55
+ | 진행 중 | 캡처한 제안 선택·수치, 행동 잠금 | 로딩은 버튼 안, 수치 전환은 AnimatedStatistic |
56
+ | 실패 | 현재 confirmed state로 복구, 중앙 오류 | Web alert / Native assertive, 같은 행동 재시도 |
57
+ | 캘린더 완료 | 실제 callback fulfilled 후 완료 label | 같은 mount에서 중복 추가 차단 |
58
+ | 엔티티 변경 | 새 profileId/eventId의 상태 | 이전 응답은 새 카드에 반영하지 않음 |
59
+
60
+ 캘린더 callback은 권한·실제 저장 결과를 책임진다. 기기 저장 전 완료를 반환하지 않는다.
61
+ 사용자 로그인, API idempotency, 캐시 갱신, 이전 엔티티 응답의 캐시 배치는 제품이 소유한다.
62
+
63
+ ## 코드 골격
64
+
65
+ ```tsx
66
+ // Web
67
+ import { FollowProfileCard } from "@hjmds/react/social-compositions";
68
+ <FollowProfileCard profileId={profile.id} name={profile.name} avatar={profileAvatar}
69
+ state={confirmedFollowState} locale={locale} labels={followLabels}
70
+ onFollowChange={async following => {
71
+ const confirmed = await api.setFollow(profile.id, following);
72
+ updateProfileCache(profile.id, confirmed);
73
+ }} />;
74
+ ```
75
+
76
+ ```tsx
77
+ // Native
78
+ import { EventAttendanceCard } from "@hjmds/react-native/social-compositions";
79
+ <EventAttendanceCard eventId={event.id} title={event.title} dateLabel={dateLabel}
80
+ state={confirmedAttendance} choices={localizedResponseChoices}
81
+ participants={confirmedParticipants} locale={locale} labels={attendanceLabels}
82
+ onResponseChange={async responseId => {
83
+ const confirmed = await api.respond(event.id, responseId);
84
+ updateEventCache(event.id, confirmed);
85
+ }}
86
+ onAddToCalendar={async () => { await calendarAdapter.saveConfirmedEvent(event); }} />;
87
+ ```
88
+
89
+ 두 플랫폼은 같은 props와 shared resolver를 사용한다. `avatar` 슬롯만 플랫폼 이미지 host에 맞춘다.
90
+ 실행되는 전체 예제는 Web `showcase/web/src/patterns/social-compositions-preview.tsx`,
91
+ Native `showcase/native/src/social-compositions-preview.tsx`에 있다. 예제 어댑터를 제품 API로 복사하지 않는다.