@hjmds/design-contracts 1.13.1 → 1.14.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 (117) hide show
  1. package/dist/agreement.d.ts +7 -1
  2. package/dist/agreement.d.ts.map +1 -1
  3. package/dist/agreement.js +14 -3
  4. package/dist/agreement.js.map +1 -1
  5. package/dist/behaviors.d.ts +1 -1
  6. package/dist/catalog.d.ts +4 -0
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/content-transition.d.ts +17 -0
  9. package/dist/content-transition.d.ts.map +1 -1
  10. package/dist/content-transition.js +21 -0
  11. package/dist/content-transition.js.map +1 -1
  12. package/dist/date-entry.d.ts +71 -0
  13. package/dist/date-entry.d.ts.map +1 -0
  14. package/dist/date-entry.js +78 -0
  15. package/dist/date-entry.js.map +1 -0
  16. package/dist/document-resource.d.ts +104 -0
  17. package/dist/document-resource.d.ts.map +1 -0
  18. package/dist/document-resource.js +77 -0
  19. package/dist/document-resource.js.map +1 -0
  20. package/dist/effect-surface.d.ts +6 -1
  21. package/dist/effect-surface.d.ts.map +1 -1
  22. package/dist/effect-surface.js +5 -2
  23. package/dist/effect-surface.js.map +1 -1
  24. package/dist/field-group.d.ts +47 -0
  25. package/dist/field-group.d.ts.map +1 -0
  26. package/dist/field-group.js +92 -0
  27. package/dist/field-group.js.map +1 -0
  28. package/dist/image.d.ts +17 -0
  29. package/dist/image.d.ts.map +1 -1
  30. package/dist/image.js +19 -0
  31. package/dist/image.js.map +1 -1
  32. package/dist/internal/effect-noise.d.ts +2 -0
  33. package/dist/internal/effect-noise.d.ts.map +1 -0
  34. package/dist/internal/effect-noise.js +4 -0
  35. package/dist/internal/effect-noise.js.map +1 -0
  36. package/dist/progressive-blur.d.ts +32 -0
  37. package/dist/progressive-blur.d.ts.map +1 -0
  38. package/dist/progressive-blur.js +28 -0
  39. package/dist/progressive-blur.js.map +1 -0
  40. package/dist/reference-controls.d.ts +32 -0
  41. package/dist/reference-controls.d.ts.map +1 -0
  42. package/dist/reference-controls.js +28 -0
  43. package/dist/reference-controls.js.map +1 -0
  44. package/dist/scroll-progress.d.ts +7 -1
  45. package/dist/scroll-progress.d.ts.map +1 -1
  46. package/dist/scroll-progress.js +21 -2
  47. package/dist/scroll-progress.js.map +1 -1
  48. package/dist/text-annotation.d.ts +43 -0
  49. package/dist/text-annotation.d.ts.map +1 -0
  50. package/dist/text-annotation.js +137 -0
  51. package/dist/text-annotation.js.map +1 -0
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist/version.js.map +1 -1
  55. package/docs/agreement.md +14 -0
  56. package/docs/dialog.md +16 -1
  57. package/docs/effect-surface.md +19 -3
  58. package/docs/generated/component-maturity.md +1 -1
  59. package/docs/generated/renderer-evidence.json +3 -3
  60. package/docs/generated/renderer-evidence.md +1 -1
  61. package/docs/generated/showcase-manifest.json +1 -1
  62. package/docs/image.md +17 -0
  63. package/docs/optional-adapters.md +25 -0
  64. package/docs/popover.md +15 -0
  65. package/docs/rating.md +6 -1
  66. package/docs/reference-controls.md +40 -0
  67. package/docs/task-list.md +15 -1
  68. package/docs/text-annotation.md +85 -0
  69. package/docs/usage/README.md +18 -1
  70. package/docs/usage/components/agreement.md +13 -3
  71. package/docs/usage/components/alert-dialog.md +7 -1
  72. package/docs/usage/components/avatar.md +33 -1
  73. package/docs/usage/components/card.md +7 -1
  74. package/docs/usage/components/carousel.md +5 -1
  75. package/docs/usage/components/chat-message.md +11 -1
  76. package/docs/usage/components/chip.md +5 -0
  77. package/docs/usage/components/content-transition.md +28 -4
  78. package/docs/usage/components/dialog.md +18 -1
  79. package/docs/usage/components/effect-surface.md +4 -2
  80. package/docs/usage/components/empty-state.md +10 -2
  81. package/docs/usage/components/field.md +23 -0
  82. package/docs/usage/components/form.md +6 -1
  83. package/docs/usage/components/image-comparison.md +82 -0
  84. package/docs/usage/components/image.md +81 -2
  85. package/docs/usage/components/keyboard-avoiding.md +6 -1
  86. package/docs/usage/components/link.md +3 -1
  87. package/docs/usage/components/list-row.md +3 -1
  88. package/docs/usage/components/list.md +6 -1
  89. package/docs/usage/components/message-composer.md +5 -0
  90. package/docs/usage/components/popover.md +7 -1
  91. package/docs/usage/components/progress.md +28 -0
  92. package/docs/usage/components/progressive-blur.md +113 -0
  93. package/docs/usage/components/rating.md +74 -0
  94. package/docs/usage/components/search-field.md +10 -0
  95. package/docs/usage/components/search-screen.md +5 -0
  96. package/docs/usage/components/segmented-control.md +17 -0
  97. package/docs/usage/components/statistic.md +11 -0
  98. package/docs/usage/components/tags-input.md +5 -0
  99. package/docs/usage/components/toast.md +5 -0
  100. package/docs/usage/components/upload-item.md +3 -1
  101. package/docs/usage/compositions/action-feedback.md +77 -0
  102. package/docs/usage/compositions/adaptive-content.md +81 -0
  103. package/docs/usage/compositions/context-toolbar.md +85 -0
  104. package/docs/usage/compositions/date-entry.md +108 -0
  105. package/docs/usage/compositions/document-resource.md +124 -0
  106. package/docs/usage/compositions/field-group.md +104 -0
  107. package/docs/usage/compositions/illustrated-outcome.md +91 -0
  108. package/docs/usage/compositions/live-list.md +104 -0
  109. package/docs/usage/compositions/optional-adapters.md +1 -1
  110. package/docs/usage/compositions/origin-dialog.md +108 -0
  111. package/docs/usage/compositions/selection-motion.md +73 -0
  112. package/docs/usage/compositions/texture-comparison.md +86 -0
  113. package/docs/usage/compositions/upload-recovery.md +80 -0
  114. package/docs/usage/compositions/video-dialog.md +100 -0
  115. package/docs/usage/screens/flow-onboarding.md +5 -3
  116. package/docs/usage/screens/product-bento.md +117 -0
  117. package/package.json +37 -1
@@ -118,3 +118,13 @@ import { SearchField } from "@hjmds/react-native/inputs";
118
118
  | 진행 중 입력 | 입력 가능(`aria-busy`) | 입력 가능(`accessibilityState.busy`). 2026-10-06까지 Native는 `busy`인 동안 입력을 무시했다(1.12.1 이후 미게시) |
119
119
  | 입력 요소 | `<input type="search">`, 원시 `onChange`도 전달 | `TextInput` |
120
120
  | 배치 | `layoutStyle`·`fieldClassName`(필드 틀), `style`은 안쪽 input | `layoutStyle` |
121
+
122
+
123
+ ### 고정 아이콘과 큰 글자
124
+
125
+ 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
126
+
127
+ ### 브라우저 지우기 중복 방지
128
+
129
+ Web은 type=search를 유지하지만 브라우저 기본 cancel 장식은 숨긴다. 2026-10-07 빈 상태 복구
130
+ 실측에서 ×가 두 개 보였기 때문이다. 실제 지우기는 HJM 버튼의 clearLabel·onClear·초점 복귀로 한다.
@@ -219,3 +219,8 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
219
219
  떨어졌다. utilverse는 `onSubmit`에서 `Keyboard.dismiss()`를 불렀다(2026-10-06). 1.13.1부터 SearchScreen이 닫으므로 그 호출을 지운다.
220
220
  - 1.13.0 이하에서는 `contentInset="none"`이면 `scroll` 줄이 전혀 넓어지지 않아 host 여백(Container·Sheet)에서 잘렸다. 1.13.1부터 `hostGutter`로 맞춘다.
221
221
  - 시트 안 SearchScreen은 Sheet `size`를 `auto` 밖으로 주고 `scrollable` 없이 넣는다. 1.13.0 이하 Native는 이때 화면이 0pt가 됐다([Sheet 함정](sheet.md#함정)).
222
+
223
+
224
+ ### 고정 아이콘과 큰 글자
225
+
226
+ 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
@@ -144,3 +144,20 @@ Native는 `@hjmds/react-native/inputs`에서 같은 prop을 사용한다. `prese
144
144
  1.13.1부터는 레일 안에서 한 줄로 남으므로 그 분기를 지운다. 1.13.0에 머무는 제품만 우회를 유지한다.
145
145
  - 레일(가로 스크롤)은 바깥이 만든다. SearchScreen 안이면 `filtersOverflow="scroll"`, 그 밖이면 제품의 가로 ScrollView다.
146
146
  `pills`가 스스로 스크롤하지 않는 이유는 레일 안에 두 번째 가로 스크롤을 겹치지 않기 위해서다.
147
+
148
+
149
+ ### 선택 배경 이동
150
+
151
+ 2026-10-07 Animated Background 비교에서 시각적 선택만 있는 외부 예제를 그대로 교체하면 radio/checked 의미를 잃는 것을 확인했다.
152
+ `selectionMotion="slide"`는 기존 선택 엔진·키보드·접근성 이름·입력 위치를 유지하고 배경만 측정 위치로 이동한다.
153
+ Web은 항목과 바깥 선택 영역의 크기를 함께 관찰한다. 2026-10-07 RTL pills에서 항목 너비는
154
+ 그대로인 채 부모 폭만 바뀌면 선택 배경이 이전 위치에 남는 오류를 수정했다. 크기 변경에는
155
+ 이동 모션을 재생하지 않고 새 위치에 맞춘다.
156
+ 기본은 `"none"`이며 opt-in 실험이다. 별도 버튼/선택 상태를 만들어 기존 라디오를 대체하지 않는다.
157
+
158
+ - Web/Native 모두 `connected`와 `pills`에 적용한다. 큰 글자·RTL·줄바꿈에서는 실제 항목 위치를 측정한다.
159
+ - 최초 배치와 resize는 즉시 맞추며 선택이 바뀔 때만 HJM `motion.normal` 시간으로 이동한다.
160
+ - 모션 감소와 비활성 화면에서는 이동 효과를 중지한다. 동작 중인 입력·선택 값은 그대로다.
161
+ - 조작 가능 영역·글자는 움직이거나 확대하지 않는다. 새 사용 예제는 [선택 배경 이동](../compositions/selection-motion.md)이다.
162
+
163
+ 2026-10-07 큰 글자 UI 확인에서 Web 2배 글자 줄은 40px인데 pill 배경은 28px였다. Native처럼 세로 padding에도 pills.inset(spacing.xs=8px)을 적용해 배경이 전체 줄을 감싸게 했다. 고정 높이를 늘리는 대신 줄 높이를 따라가므로 3배·줄바꿈도 유지한다.
@@ -121,3 +121,14 @@ import { Statistic } from "@hjmds/react-native/data-display";
121
121
  - Web `AnimatedStatistic`은 reduced motion, RTL, 라틴 숫자가 아닌 numbering system, `ar`·`fa`·`he`·`ur` locale,
122
122
  `scientific`·`engineering` 표기에서는 애니메이션 없이 `Intl` 결과 문자열만 그린다.
123
123
  - `AnimatedStatistic`의 `descriptor`에는 `value`를 넣지 않는다. 값은 `value` prop의 숫자로 받는다.
124
+
125
+ ### 외부 숫자 효과 대조
126
+
127
+ 2026-10-07 Number Ticker 공식 소스와 기본 데모를 대조했다. 진입 시 중간 숫자를 표시하는
128
+ 효과와 실제 값 변경은 구분한다. 현재 AnimatedStatistic의 Intl locale과 RTL·비라틴 숫자·
129
+ 지수 표기 fallback을 유지하며 별도 count-up 엔진을 추가하지 않는다. 숫자가 올라가는 효과를
130
+ 실제 집계 과정으로 오해시키지 않기 위해 제품은 확정한 값을 전달한다.
131
+
132
+ 양쪽 움직이는 수치 Storybook에 소수와 음수(de-DE), 비라틴 숫자(ar-EG), 지수 표기(en-US),
133
+ 동작 줄이기와 RTL 비교를 제공한다. 소수·음수 예제는 기록 개수가 아닌 측정값이다.
134
+ Web은 자리 단위 전환, Native는 지표 전체 전환이며 같은 시각 효과를 보장하지 않는다.
@@ -109,3 +109,8 @@ import { TagsInput } from "@hjmds/react-native/tags-input";
109
109
  | 후보 이동 | ArrowUp/Down(끝에서 순환) | 후보를 버튼으로 누름 |
110
110
  | ref | `forwardRef`(`input`) | 없음 |
111
111
  | 스타일 prop | `className`, `layoutStyle` | `layoutStyle`(`style`은 deprecated) |
112
+
113
+
114
+ ### 고정 아이콘과 큰 글자
115
+
116
+ 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
@@ -143,3 +143,8 @@ function useProfileSavedToast() {
143
143
  어댑터는 render 밖에서 만들거나 memo한다.
144
144
  - ToastRegion 배치는 `layoutStyle={{ flex: 1 }}`로 쓴다. Native 예제도 이 경로를 사용한다.
145
145
  - Web `Toast` 단독 렌더는 나머지 HTML 속성(id·data-*·이벤트)을 루트에 전달한다(미게시(1.12.1 이후). 1.12.1은 `className`만 전달). `role`·`aria-labelledby`·`aria-describedby`·`data-tone`·`data-state`는 Toast가 정하므로 덮이지 않는다. 배치는 Provider를 쓴다.
146
+
147
+
148
+ ### 고정 아이콘과 큰 글자
149
+
150
+ 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-07
8
8
  - 근거: [UploadItem](../../upload-item.md), `src/upload-item.ts`(`uploadItemRecipe`)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/업로드 항목`
10
10
 
@@ -84,6 +84,8 @@ import { UploadItem } from "@hjmds/react-native/upload-item";
84
84
 
85
85
  ## 꼭 지킬 것
86
86
 
87
+ - 이미 게시된 문서를 열거나 내려받는 목록에 `success` 상태를 전용하지 않는다. 이 API의 행동은 업로드 취소·재시도이며 다운로드 행동이 아니다. Web 다운로드는 [Link](link.md), 미리보기와 여러 행동은 [Card](card.md)를 대조한다. 2026-10-07 파일 사례 조사에서 다운로드와 업로드의 초기 대응표가 같은 계약으로 오인될 수 있어 구분했다.
88
+
87
89
  - `progress`에 100을 곱해 넘기지 않는다. renderer가 내부 Progress에 `value={progress * 100}`으로 바꾼다.
88
90
  - `uploading` 상태에서 `onCancel`이, `error` 상태에서 `onRetry`가 없으면 렌더 중 `TypeError`가 난다.
89
91
  - 바이트 포맷(`sizeLabel`)·상태 문구·`message`·업로드 요청·재시도 로직은 제품 소유다. 행 모양·상태 색·액션 결정은 HJM 소유다.
@@ -0,0 +1,77 @@
1
+ # 버튼 완료 피드백
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: 공개 API를 사용하는 `showcase/*/reference-adoption-previews.tsx`
9
+ - 스토리북: `배포/구성/피드백과 복구/버튼 완료 피드백`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 입력을 유지하며 저장의 진행·성공·재시도 가능 실패를 보여 줄 때 쓴다. 별도 신규 wrapper API가 아니라 아래 공개 컴포넌트의 조합 규격이다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | Button · TextField · ContentTransition · action-session | 입력·표현·행동의 역할 분리 | [입력/동작](../components/button.md), [상태/전환](../components/content-transition.md) |
22
+
23
+ ## 배치
24
+
25
+ ```text
26
+ [제목]
27
+ [기록 제목 입력]
28
+ [저장 / 다시 저장 버튼]
29
+ [진행·실패·성공 결과]
30
+ [실패 체험 스위치 + 서버 요청 없음 안내]
31
+ ```
32
+
33
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
34
+ | --- | --- | --- | --- |
35
+ | 바깥 틀 | Stack | 화면의 본문 흐름 | gap md=16px, 전체 폭 |
36
+ | 내용 | 해당 공개 API | 입력과 행동 사이 | 자체 recipe 크기, 줄바꿈 허용 |
37
+ | 행동 | Button/기존 컨트롤 | 관련 항목 바로 뒤 | inline일 때 wrap, md=16px |
38
+
39
+ ## 흐름과 상태
40
+
41
+ 1. 입력→요청→실제 결과에 따라 성공/실패. 다시 저장은 클릭한 시점의 현재 초안을 새 요청으로 보낸다.
42
+ 2026-10-07 실패 후 제목 수정·재저장 확인에서 기존 문서의 “같은 입력 재시도”와 구현이
43
+ 다른 것을 확인했다. 편집 가능한 초안에 맞춰 현재 값 저장을 유지한다. 실패한 요청의
44
+ 원래 값 재시도가 필요하면 별도 행동에서 action-session.retry를 연결하고 대상 값을 알린다.
45
+ 2. 서버 응답·파일 권한·문구·브랜드는 제품이 전달한다. Showcase의 예제 응답과 고정 데이터를 가져오지 않는다.
46
+
47
+ | 상태 | 모습 | 포커스·알림 |
48
+ | --- | --- | --- |
49
+ | 기본 | 입력과 사용 가능한 행동 | 조작 이름은 제품 언어 |
50
+ | 진행 중 | 실제 작업 상태에 맞는 loading 또는 열린 도구 | 중복 요청 차단, 입력 유지 |
51
+ | 실패 | 문구와 가능한 재시도 | 원래 입력/파일 유지, 결과 안내 |
52
+
53
+ ## 코드 골격
54
+
55
+ ```tsx
56
+ // Web
57
+ <Button loading={busy} onClick={save}>{busy ? pendingLabel : saveLabel}</Button>
58
+ ```
59
+
60
+ ```tsx
61
+ // Native
62
+ <Button loading={busy} onPress={save}>{busy ? pendingLabel : saveLabel}</Button>
63
+ ```
64
+
65
+ Web은 `@hjmds/react`의 해당 granular entry, Native는 `@hjmds/react-native` entry를 쓴다.
66
+ Button의 실행 콜백은 Web `onClick`, Native `onPress`로 연결한다. 위 골격의 도메인 함수·변수는 제품이 제공한다.
67
+
68
+ 예제 응답은 useDemoAction의 인위 지연이며 실제 네트워크·영구 저장이 아니다.
69
+ 완료 문구는 응답 확정 뒤 표시한다. 저장 중 입력 편집이 가능하므로, 저장 결과는 현재
70
+ 초안으로 바꿔 쓰지 말고 응답 값으로 표시한다. 실패 체험 설정은 요청 중 바꾸지 않는다.
71
+
72
+ ## 플랫폼 차이
73
+
74
+ | 항목 | Web | Native |
75
+ | --- | --- | --- |
76
+ | 배치/테마 | Stack과 HjmProvider | Stack과 HjmNativeProvider |
77
+ | 큰 글자·좁은 폭 | 줄바꿈·단일 내용 | 같은 순서, OS 화면 검증은 별도 |
@@ -0,0 +1,81 @@
1
+ # 높이가 이어지는 패널
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: 공개 API를 사용하는 `showcase/*/reference-adoption-previews.tsx`
9
+ - 스토리북: `배포/구성/직접 조작과 모션/높이가 이어지는 패널`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 패널의 길이가 달라질 때 아래 행동이 새 높이로 이동해야 하는 작은 내용 영역에 쓴다. 별도 신규 wrapper API가 아니라 아래 공개 컴포넌트의 조합 규격이다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | SegmentedControl · ContentTransition · Surface · TextField | 입력·표현·행동의 역할 분리 | [입력/동작](../components/segmented-control.md), [상태/전환](../components/content-transition.md) |
22
+
23
+ ## 배치
24
+
25
+ ```text
26
+ [요약 / 상세 단일 선택]
27
+ [높이가 바뀌는 콘텐츠 패널]
28
+ [전환 바깥의 메모 입력: 유지]
29
+ ```
30
+
31
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
32
+ | --- | --- | --- | --- |
33
+ | 바깥 틀 | Stack | 화면의 본문 흐름 | gap md=16px, 전체 폭 |
34
+ | 내용 | 해당 공개 API | 입력과 행동 사이 | 자체 recipe 크기, 줄바꿈 허용 |
35
+ | 행동 | Button/기존 컨트롤 | 관련 항목 바로 뒤 | inline일 때 wrap, md=16px |
36
+
37
+ ## 흐름과 상태
38
+
39
+ 1. 선택→내용 교체→입력은 바깥에서 유지
40
+ 2. 서버 응답·파일 권한·문구·브랜드는 제품이 전달한다. Showcase의 예제 응답과 고정 데이터를 가져오지 않는다.
41
+
42
+ | 상태 | 모습 | 포커스·알림 |
43
+ | --- | --- | --- |
44
+ | 기본 | 입력과 사용 가능한 행동 | 조작 이름은 제품 언어 |
45
+ | 진행 중 | 실제 작업 상태에 맞는 loading 또는 열린 도구 | 중복 요청 차단, 입력 유지 |
46
+ | 실패 | 문구와 가능한 재시도 | 원래 입력/파일 유지, 결과 안내 |
47
+
48
+ ## 코드 골격
49
+
50
+ ```tsx
51
+ // Web
52
+ <ContentTransition stateKey={panel} animateHeight><Panel /></ContentTransition>
53
+ ```
54
+
55
+ ```tsx
56
+ // Native
57
+ <ContentTransition stateKey={panel} animateHeight><Panel /></ContentTransition>
58
+ ```
59
+
60
+ Web은 `@hjmds/react`의 해당 granular entry, Native는 `@hjmds/react-native` entry를 쓴다.
61
+ 선택은 SegmentedControl의 onValueChange로 연결한다. 패널 바깥 입력은 unmount하지 않는다.
62
+ 2026-10-07 Web 390px·큰 글자에서 상세→요약 방향키 연속 전환 뒤 단일 내용과 초안 보존을
63
+ 확인했다. 이 확인은 프레임 속도나 Native 성능 동등성의 근거가 아니다.
64
+
65
+ ## 플랫폼 차이
66
+
67
+ | 항목 | Web | Native |
68
+ | --- | --- | --- |
69
+ | 배치/테마 | Stack과 HjmProvider | Stack과 HjmNativeProvider |
70
+ | 큰 글자·좁은 폭 | 줄바꿈·단일 내용 | 동일 순서와 스크롤 host 필요 |
71
+
72
+
73
+ ### Native 입력과 화면 끝
74
+
75
+ 2026-10-07 큰 글자에서 상세 패널이 늘어나면 키보드 아래로 메모가 밀려났고,
76
+ 스크롤 없는 Showcase canvas에서는 다시 접근할 수 없었다. Native 예제는
77
+ ScreenLayout의 본문 스크롤과 바깥 KeyboardAvoiding으로 사용 가능한 높이를 확보한다.
78
+ Storybook의 기존 gutter 때문에 contentInset=none을 쓰고, 마지막 입력의 테두리가
79
+ 스크롤 경계에 걸리지 않도록 본문 끝에 spacing.md(16) 여백을 둔다.
80
+ 본문 조합 자체에 두 번째 scroll view를 만들지 말고 제품의 기존 화면 host를 재사용한다.
81
+ KeyboardAvoiding은 아래 여백을 제공하며 포커스된 필드를 자동 탐색하는 API는 아니다.
@@ -0,0 +1,85 @@
1
+ # 입력을 유지하는 도구
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: 공개 API를 사용하는 `showcase/*/reference-adoption-previews.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/입력을 유지하는 도구`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 작성 중인 입력을 보존한 채 선택적 도구를 펼쳐야 할 때 쓴다. 별도 신규 wrapper API가 아니라 아래 공개 컴포넌트의 조합 규격이다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | TextField · Collapsible · SegmentedControl | 입력·표현·행동의 역할 분리 | [입력](../components/field.md), [접기](../components/collapsible.md), [단일 선택](../components/segmented-control.md) |
22
+
23
+ ## 배치
24
+
25
+ ```text
26
+ [작성 중인 입력: 항상 유지]
27
+ [도구 펼침/접힘 버튼 · 현재 선택]
28
+ [기본 | 인용 | 강조] 열린 동안만 표시
29
+ [선택한 표현 안내: 항상 유지]
30
+ ```
31
+
32
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
33
+ | --- | --- | --- | --- |
34
+ | 바깥 틀 | Stack | 화면의 본문 흐름 | gap md=16px, 전체 폭 |
35
+ | 도구 | Collapsible | 입력 바로 아래 | trigger 최소 높이44px, 공개 recipe 사용 |
36
+ | 선택 | SegmentedControl presentation=pills | 열린 도구 내용 | 최소 높이44px, 항목 간8px, 좁으면 줄바꿈 |
37
+ | 결과 안내 | Text | 도구 아래 | Web status/Native polite |
38
+
39
+ ## 흐름과 상태
40
+
41
+ 1. 입력→도구 펼침→단일 표현 선택→접힘; 입력은 도구 바깥에 유지. 선택 값도 접히는 내용의 바깥에서 소유한다.
42
+ 2026-10-07 실제 조작에서 개별 selected Button이 독립 토글로 안내되는 것을 확인해,
43
+ 묶음 이름·radio 의미·방향키 이동을 제공하는 SegmentedControl을 사용한다.
44
+ 여러 서식을 동시에 켜는 편집기는 ToggleGroup을 쓰며 이 단일 선택 예제를 복사하지 않는다.
45
+ 2. 서버 응답·파일 권한·문구·브랜드는 제품이 전달한다. Showcase의 예제 응답과 고정 데이터를 가져오지 않는다.
46
+
47
+ | 상태 | 모습 | 포커스·알림 |
48
+ | --- | --- | --- |
49
+ | 기본 | 접힌 도구: 입력·펼침 버튼·현재 선택 안내 | 숨긴 선택지는 탐색 대상에서 제거 |
50
+ | 진행 중 | 도구를 펼쳐 단일 선택; 비동기 pending 없음 | Web Tab으로 선택 진입, 방향키로 변경 |
51
+ | 선택 변경 | 정확히 하나 선택, 안내 갱신 | 초안 유지, 선택 값은 바깥 상태에 저장 |
52
+ | 다시 접힘/펼침 | 마지막 선택과 초안 복원 | Web 접기 버튼에 포커스 유지 |
53
+ | 실패 | 예제에 서버 작업 없음; 제품 저장 실패는 별도 연결 | 실제 저장 실패 시에도 초안·선택은 보존 |
54
+
55
+ ## 코드 골격
56
+
57
+ ```tsx
58
+ // Web
59
+ <TextField label={draftLabel} value={draft} onValueChange={setDraft} />
60
+ <Collapsible open={open} onOpenChange={setOpen} trigger={toolsLabel}>
61
+ <SegmentedControl label={formatLabel} presentation="pills" items={formats}
62
+ value={format} onValueChange={setFormat} />
63
+ </Collapsible>
64
+ ```
65
+
66
+ ```tsx
67
+ // Native
68
+ <TextField label={draftLabel} value={draft} onValueChange={setDraft} />
69
+ <Collapsible open={open} onOpenChange={setOpen} trigger={toolsLabel}>
70
+ <SegmentedControl label={formatLabel} presentation="pills" items={formats}
71
+ value={format} onValueChange={setFormat} />
72
+ </Collapsible>
73
+ ```
74
+
75
+ Web은 `@hjmds/react`의 해당 granular entry, Native는 `@hjmds/react-native` entry를 쓴다.
76
+ 초안과 선택 값은 접히는 내용 바깥에서 소유한다. 예제는 선택·입력 보존을 보여 주며,
77
+ 본문 서식 변환·영구 저장·실패 복구는 구현하지 않는다. 이 동작이 필요한 제품은
78
+ 실제 편집 모델과 저장 상태를 연결하고 따로 검증한다.
79
+
80
+ ## 플랫폼 차이
81
+
82
+ | 항목 | Web | Native |
83
+ | --- | --- | --- |
84
+ | 배치/테마 | Stack과 HjmProvider | Stack과 HjmNativeProvider |
85
+ | 큰 글자·좁은 폭 | 줄바꿈·단일 내용 | 같은 순서, OS 화면 검증은 별도 |
@@ -0,0 +1,108 @@
1
+ # 날짜 직접 입력
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: [날짜 입력 조사](../../../../../docs/qa/2026-10-07-date-entry-reference.md), `src/date-entry.ts`
9
+ - 스토리북: `배포/구성/입력과 작성/날짜 직접 입력`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 사용자가 알고 있는 날짜를 직접 입력할 때 쓴다. DatePicker는 달력에서 날짜를 선택하는 API이므로
16
+ 부분 연도·월 이름을 편집하는 초안을 담지 않는다. 새 DateEntry는 Field의 optional extension이며
17
+ 기존 TextField를 합성한다. 달력·언어·시간대 계산은 제품이 맡는 기존 Calendar 경계를 유지한다.
18
+
19
+ ## 구성 요소
20
+
21
+ | 컴포넌트 | 역할 | 지침 |
22
+ | --- | --- | --- |
23
+ | DateEntry | 날짜 초안·필드 순서·오류 연결 | 이 문서 |
24
+ | TextField | 각 날짜 조각 입력·포커스·오류 | [Field](../components/field.md) |
25
+ | Button | 제품의 확인/저장 | [Button](../components/button.md) |
26
+
27
+ ## 배치
28
+
29
+ ```text
30
+ 그룹 이름
31
+ 설명(선택)
32
+ [연도] [월] [일] ← 제품 order
33
+ 오류 안내 한 번 ← 입력 위, 해당 칸 테두리로 연결
34
+ [확인] ← 제품 행동
35
+ 결과/서버 상태 ← 제품 소유
36
+ ```
37
+
38
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
39
+ | --- | --- | --- | --- |
40
+ | 바깥 틀 | Web fieldset / Native View | 제품 폼 안 | 최소 폭 0, 테두리 없는 그룹 |
41
+ | 그룹 이름 | Web legend / Native Text label | 맨 위 | 아래 spacing.sm 12 |
42
+ | 입력 | TextField 3개 | order 순서 | 간격 spacing.md 16, Web 최소 10ch 자동 줄바꿈, Native 기준 spacing.xxxl × 3 × textScale |
43
+ | 오류 | 그룹 안내 + TextField invalid | 입력 위 한 번 | 해당 필드만 오류 테두리, Web 설명 ID·Native hint 연결 |
44
+ | 확인 | 제품 Button | 그룹 다음 | DateEntry 내부에 저장 버튼을 넣지 않음 |
45
+
46
+ ## 흐름과 상태
47
+
48
+ 1. value는 `{year, month, day}` 원문 문자열이다. onValueChange는 한 필드만 바꾼 새 초안을 전달한다.
49
+ 2. order는 세 필드가 중복 없이 한 번씩 나온 배열이다. RTL만으로 날짜 순서를 추측하지 않는다.
50
+ 3. required 기본 false. 전체 공백은 optional이면 오류가 없고 일부만 비면 optional이어도 incomplete다.
51
+ 4. 모든 조각이 있으면 parse가 valid/value 또는 incomplete·invalid/code/fields를 반환한다. 입력을
52
+ 자동 정규화하거나 다음 필드로 자동 이동하지 않는다. parse는 동결된 복사본을 받는다.
53
+ 5. showErrors 기본 false. 제품은 제출/blur 정책에 맞춰 켠다. 오류 문구는 formatIssue로 지역화한다.
54
+ 6. 목적이 birthdate일 때만 날짜 조각 자동완성을 요청한다. 일반 date는 off다. 실제 자동완성은 OS/브라우저 소유다.
55
+ 7. monthInput 기본 text는 월 이름 입력을 허용한다. 숫자만 받는 제품은 numeric을 명시한다.
56
+
57
+ | 상태 | 모습 | 포커스·알림 |
58
+ | --- | --- | --- |
59
+ | 기본 | 세 입력과 설명 | 자동 초점 이동 없음 |
60
+ | 진행 중 | 미완성 원문 유지 | 일반 Tab/터치로 이동, validation 노출은 제품 제어 |
61
+ | 실패 | 그룹 오류 한 번과 해당 필드의 오류 테두리 | Web 연결된 오류 설명, Native 그룹+조각 이름·오류 hint, iOS 그룹 안내 한 번 |
62
+ | 확인 | 제품에 전달한 valid 값 | 확인 UI/서버 저장은 제품 소유 |
63
+ | 비활성·읽기 전용 | 편집 차단 | 호스트가 늦게 edit 이벤트를 보내도 callback 차단 |
64
+
65
+ ## 코드 골격
66
+
67
+ ```tsx
68
+ // Web
69
+ import { DateEntry } from "@hjmds/react/date-entry";
70
+ // Resolver: @hjmds/design-contracts/date-entry.
71
+ <DateEntry value={draft} onValueChange={setDraft}
72
+ order={["year", "month", "day"]}
73
+ labels={{ label: t("date.label"), year: t("date.year"), month: t("date.month"), day: t("date.day") }}
74
+ required showErrors={submitted} parse={parseProductDate} formatIssue={formatDateIssue}
75
+ onBlur={part => markTouched(part)} />
76
+ ```
77
+
78
+ ```tsx
79
+ // Native
80
+ import { DateEntry } from "@hjmds/react-native/date-entry";
81
+ <DateEntry value={draft} onValueChange={setDraft} order={["year", "month", "day"]}
82
+ labels={localizedLabels} parse={parseProductDate} formatIssue={formatDateIssue}
83
+ required showErrors={submitted} />
84
+ ```
85
+
86
+ parse는 순수 함수다. 네트워크 요청·Date.now·초안 변경을 넣지 않는다. 날짜 체계·허용 범위·
87
+ 로케일별 숫자와 월 이름은 제품이 결정한다. valid.value는 제품이 정한 날짜 문자열이며
88
+ resolveDateEntryDraft는 실제 날짜 유효성을 다시 계산하지 않는다. 저장 실패 시 draft를 유지한다.
89
+
90
+ ## 플랫폼 차이
91
+
92
+ | 항목 | Web | Native |
93
+ | --- | --- | --- |
94
+ | 그룹 | fieldset/legend | 각 필드의 접근성 이름에 그룹 포함 |
95
+ | 자동완성 | bday 조각 | Android birthdate 조각 + iOS 명시적 textContentType |
96
+
97
+
98
+ Web은 fieldset/legend와 필드별 label·aria-describedby를 사용한다. Native는 그룹 이름을 각 입력의
99
+ 접근성 이름에 포함하며 세 입력을 하나의 접근성 노드로 합치지 않는다. Native 날짜 자동완성은
100
+ Android의 `birthdate-year/month/day`와 iOS의 명시적 `birthdateYear/Month/Day` content type을 연결한다. Web은 `bday-year/month/day`다. Web className, 양쪽 layoutStyle을 지원한다.
101
+
102
+ ## 함정
103
+
104
+ - 서버 저장 완료와 valid 초안을 구분한다. 편집하면 이전 확인 결과를 무효화한다.
105
+ - Showcase의 Gregorian/영어 월 파서는 예제 정책이며 HJM 기본 파서가 아니다.
106
+ - Calendar로 부분 입력을 강제로 변환하거나 NumberField로 교체하면 원문 보존 계약이 깨진다.
107
+ - Web 390px 다크/RTL/2배 글자와 iOS 2배 글자에서 오류 복구를 확인했다. 제품 팔레트·스크린리더·자동완성 실제 검증은 남아 있다. 실험 등록은 승격·게시가 아니다.
108
+ - Native 화면 호스트는 키보드 inset을 처리하는 ScrollView 등으로 하단 행동에 접근할 수 있어야 한다. DateEntry 내부에 중첩 스크롤을 만들지 않는다.
@@ -0,0 +1,124 @@
1
+ # 문서와 파일
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: [파일 원본 대조](../../../../../docs/qa/2026-10-07-file-reference.md), `src/document-resource.ts`
9
+ - 스토리북: `배포/구성/정보 표시/문서와 파일`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 이름·형식·크기와 미리보기·내보내기·별도 메뉴를 함께 제공하는 문서에 쓴다.
16
+ 단순 다운로드 링크는 Link, 업로드 진행은 UploadItem을 사용한다. 파일 읽기·저장·권한·공유와
17
+ 성공 영수증은 제품이 소유한다. DocumentResource는 controlled 상태의 배치와 버튼을 소유한다.
18
+
19
+ ## 구성 요소
20
+
21
+ | 컴포넌트 | 역할 | 지침 |
22
+ | --- | --- | --- |
23
+ | DocumentResource | metadata·상태·독립 행동 구성 | 이 문서 |
24
+ | Surface/Stack | 바깥 틀·세로 배치 | [Surface](../components/surface.md), [Stack](../components/stack.md) |
25
+ | Text | 파일명·형식·크기·오류 | [Text](../components/text.md) |
26
+ | Button | 미리보기·저장·재시도 | [Button](../components/button.md) |
27
+
28
+ ## 배치
29
+
30
+ ```text
31
+ ┌────────────────────────────┐
32
+ │ 파일명 │
33
+ │ 형식 / 크기(각 별도 줄) │
34
+ │ 설명 │
35
+ │ 미리보기 또는 상태 안내 │
36
+ │ [본문 보기] │
37
+ │ [미리보기 재시도] — 오류 시 │
38
+ │ [저장 / 재시도] │
39
+ │ 저장 상태·오류 │
40
+ │ moreAction(선택) │
41
+ └────────────────────────────┘
42
+ ```
43
+
44
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
45
+ | --- | --- | --- | --- |
46
+ | 바깥 틀 | Surface | 제품 목록/상세 안 | padding md, radius lg |
47
+ | 모든 영역 | Stack | 위→아래 | gap sm, spacing.sm 12 |
48
+ | 파일명 | Text | 최상단 | strong, heading 역할을 자동 부여하지 않음 |
49
+ | 형식·크기 | Text | 이름 아래 | caption/muted, 제품 문자열 그대로 |
50
+ | 행동 | Button | 미리보기 아래 | 세로 배치, Native growWithContent |
51
+
52
+ 파일명은 줄바꿈한다. 미리보기 크기와 내용은 제품 host가 공급하며, 카드 전체를 링크/버튼으로
53
+ 감싸지 않는다. `moreAction`도 독립 행동이다. 상위 화면이 스크롤을 소유한다.
54
+
55
+ Web에서 초점을 가진 미리보기 재시도 버튼이 제거되면 같은 문서의 사용 가능한 미리보기
56
+ 버튼으로 복귀하며, 없으면 저장 버튼으로 옮긴다. 2026-10-07 실제 브라우저에서 제거된 버튼의
57
+ 초점이 body로 떨어진 회귀에 따른 규칙이다. 제품이 다른 문서로 바꾸거나 사용자가 이미 다른
58
+ 요소에 초점을 둔 경우에는 자동 이동하지 않는다. Native 스크린리더 복구는 별도 검증 대상이다.
59
+
60
+ Showcase에는 긴 파일명 전환과 미리보기 준비 중·실패·없음 조작이 있다. 이름을 전환하면
61
+ 기존 action-session 결과를 reset한다. 미리보기 로딩은 저장을 자동으로 잠그지 않는다.
62
+ 저장 잠금 토글은 미리보기와 그 재시도를 유지하며 저장·저장 재시도만 잠근다.
63
+
64
+ ## 흐름과 상태
65
+
66
+ 1. 양 renderer의 `/document-resource`에서 DocumentResource를 import한다. root export는 없다.
67
+ 2. descriptor id는 파일 revision을 구분한다. name 필수, formatLabel/sizeLabel/description은 선택이다.
68
+ 3. preview는 none/loading/ready/error이며 error에는 message와 retryable을 준다. retryable이면
69
+ onRetryPreview도 필수다. ready일 때만 preview 노드를 표시한다. onPreview는 선택이다.
70
+ 4. save는 idle/pending/started/saved/cancelled/error다. error의 retryable이 true이면 onRetrySave가
71
+ 필수이고 false면 일반 저장 버튼으로 우회하지 않는다. 새 시도 허용은 제품의 상태 판단이다.
72
+ 5. labels의 열 문구를 모두 현지화한다. 비어 있는 문구·잘못된 상태는 TypeError다.
73
+ 6. onSave/onRetrySave는 제품 action-session에 연결한다. 중복 실행은 session이 막으며 파일 교체·
74
+ unmount에서 reset으로 이전 결과를 분리한다. reset은 실제 OS/서버 작업 취소가 아니다.
75
+ 7. 브라우저 anchor 시작이나 Native 공유 sheet 완료만으로 saved를 넣지 않는다. 실제 host가
76
+ 확인한 저장 결과만 saved다. 예제 Web은 Blob 다운로드 시작, Native는 텍스트 공유 예제다.
77
+
78
+ | 상태 | 모습 | 포커스·알림 |
79
+ | --- | --- | --- |
80
+ | 기본 | metadata·각 버튼 | 자동 초점 없음 |
81
+ | 진행 중 | 저장 버튼 pending/loading | 같은 버튼 위치 유지, 제품 세션이 재실행 차단 |
82
+ | 실패 | 해당 오류·허용된 재시도 | 미리보기 실패가 저장을 자동으로 막지 않음 |
83
+ | 시작/완료/취소 | 각각 다른 상태 문구 | Web status, Native live region; iOS 실제 알림 검증 대기 |
84
+ | 비활성 | 기본·재시도 행동 비활성 | 제품 moreAction도 같은 정책을 공급해야 함 |
85
+
86
+ `saveDisabled`는 기본 false이며 저장·저장 재시도만 막는다. 결과를 읽고 검토해야 내보낼 수
87
+ 있는 제품에서 사용한다. `disabled`는 미리보기까지 막으므로 검토 대기 상태에 대신 쓰지 않는다.
88
+ 2026-10-07 Utilverse PhotoOutputCard 조사에서 이 구분이 필요했다. 잠금은 진행 중인 OS 작업의
89
+ 취소나 권한 검사를 대신하지 않는다. 제품은 onSave 실행 직전에도 유효한 권한·검토 상태를 확인하고,
90
+ `moreAction`에 별도 공유 행동을 넣었다면 그 행동에도 제품의 잠금 정책을 연결한다.
91
+
92
+ ## 코드 골격
93
+
94
+ ```tsx
95
+ // Web
96
+ import { DocumentResource } from "@hjmds/react/document-resource";
97
+ <DocumentResource descriptor={documentState} labels={localizedLabels}
98
+ preview={previewHost} onPreview={openPreview} onSave={startExport}
99
+ onRetryPreview={retryPreview} onRetrySave={retryExport} />;
100
+ ```
101
+
102
+ ```tsx
103
+ // Native
104
+ import { DocumentResource } from "@hjmds/react-native/document-resource";
105
+ <DocumentResource descriptor={documentState} labels={localizedLabels}
106
+ preview={previewHost} onPreview={openPreview} onSave={startExport}
107
+ onRetryPreview={retryPreview} onRetrySave={retryExport} />;
108
+ ```
109
+
110
+ ## 플랫폼 차이
111
+
112
+ | 항목 | Web | Native |
113
+ | --- | --- | --- |
114
+ | 바깥 의미 | group + 파일명 | accessible=false로 자식 조작 유지 |
115
+ | 내보내기 host | anchor/download, 파일 API 등 제품 선택 | OS 저장·공유·권한 adapter를 제품이 공급 |
116
+ | pending/error 알림 | status/alert | live region/alert, iOS 기기 낭독 미검증 |
117
+
118
+ ## 함정
119
+
120
+ Showcase의 첫 실패는 합성 fixture이며 운영 서버 실패가 아니다. Native 텍스트 공유는 파일 저장
121
+ 검증을 대신하지 않는다. HJM이 제품의 실패를 저장 성공으로 해석하지 않도록 action-session의
122
+ success와 host 결과 started/saved/cancelled를 따로 연결한다. 오류를 내부 상태로 기록하고
123
+ resolve하는 제품 세션은 Promise 완료만으로 성공을 판단하지 말고 세션 snapshot의 결과·오류를 읽는다. 제품 metadata에 URL·서버 오류 원문을
124
+ 자동 노출하지 않는다. 아직 실험이며 전체 환경 검증·승격·npm 게시·소비 적용은 별도다.