@hjmds/design-contracts 1.26.0 → 1.27.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 (278) hide show
  1. package/dist/agreement.d.ts +1 -0
  2. package/dist/agreement.d.ts.map +1 -1
  3. package/dist/agreement.js +2 -1
  4. package/dist/agreement.js.map +1 -1
  5. package/dist/auth-screen.d.ts +1 -1
  6. package/dist/behaviors.d.ts +1 -1
  7. package/dist/calendar.d.ts +1 -0
  8. package/dist/calendar.d.ts.map +1 -1
  9. package/dist/calendar.js +4 -1
  10. package/dist/calendar.js.map +1 -1
  11. package/dist/catalog.d.ts +78 -30
  12. package/dist/catalog.d.ts.map +1 -1
  13. package/dist/catalog.js +10 -5
  14. package/dist/catalog.js.map +1 -1
  15. package/dist/chart.d.ts +2 -0
  16. package/dist/chart.d.ts.map +1 -1
  17. package/dist/chart.js +1 -1
  18. package/dist/chart.js.map +1 -1
  19. package/dist/component-definitions.d.ts +1 -0
  20. package/dist/component-definitions.d.ts.map +1 -1
  21. package/dist/component-definitions.js +1 -0
  22. package/dist/component-definitions.js.map +1 -1
  23. package/dist/component-recipes.d.ts +53 -6
  24. package/dist/component-recipes.d.ts.map +1 -1
  25. package/dist/component-recipes.js +64 -10
  26. package/dist/component-recipes.js.map +1 -1
  27. package/dist/data-table.d.ts +11 -0
  28. package/dist/data-table.d.ts.map +1 -1
  29. package/dist/data-table.js +5 -0
  30. package/dist/data-table.js.map +1 -1
  31. package/dist/description-list.d.ts +3 -0
  32. package/dist/description-list.d.ts.map +1 -1
  33. package/dist/description-list.js +5 -1
  34. package/dist/description-list.js.map +1 -1
  35. package/dist/design-profile-layout.d.ts +28 -0
  36. package/dist/design-profile-layout.d.ts.map +1 -1
  37. package/dist/design-profile-layout.js +27 -3
  38. package/dist/design-profile-layout.js.map +1 -1
  39. package/dist/design-profile.d.ts.map +1 -1
  40. package/dist/design-profile.js +40 -8
  41. package/dist/design-profile.js.map +1 -1
  42. package/dist/field-group.d.ts +13 -0
  43. package/dist/field-group.d.ts.map +1 -1
  44. package/dist/field-group.js +17 -2
  45. package/dist/field-group.js.map +1 -1
  46. package/dist/foundations.d.ts +24 -11
  47. package/dist/foundations.d.ts.map +1 -1
  48. package/dist/foundations.js +40 -7
  49. package/dist/foundations.js.map +1 -1
  50. package/dist/heading.d.ts +5 -5
  51. package/dist/image-choice.d.ts +23 -0
  52. package/dist/image-choice.d.ts.map +1 -0
  53. package/dist/image-choice.js +26 -0
  54. package/dist/image-choice.js.map +1 -0
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/masonry.d.ts +8 -0
  60. package/dist/masonry.d.ts.map +1 -1
  61. package/dist/masonry.js +12 -1
  62. package/dist/masonry.js.map +1 -1
  63. package/dist/number-field.d.ts +2 -2
  64. package/dist/number-field.d.ts.map +1 -1
  65. package/dist/number-field.js +4 -2
  66. package/dist/number-field.js.map +1 -1
  67. package/dist/qr-code-recipe.d.ts +1 -0
  68. package/dist/qr-code-recipe.d.ts.map +1 -1
  69. package/dist/qr-code-recipe.js +3 -1
  70. package/dist/qr-code-recipe.js.map +1 -1
  71. package/dist/recipes.d.ts +1 -1
  72. package/dist/recipes.d.ts.map +1 -1
  73. package/dist/recipes.js +1 -1
  74. package/dist/recipes.js.map +1 -1
  75. package/dist/result.d.ts +9 -0
  76. package/dist/result.d.ts.map +1 -1
  77. package/dist/result.js +5 -0
  78. package/dist/result.js.map +1 -1
  79. package/dist/screen-patterns.d.ts +139 -2
  80. package/dist/screen-patterns.d.ts.map +1 -1
  81. package/dist/screen-patterns.js +140 -1
  82. package/dist/screen-patterns.js.map +1 -1
  83. package/dist/sheet.d.ts +15 -1
  84. package/dist/sheet.d.ts.map +1 -1
  85. package/dist/sheet.js +20 -1
  86. package/dist/sheet.js.map +1 -1
  87. package/dist/showcase.d.ts +27 -27
  88. package/dist/showcase.js +27 -27
  89. package/dist/showcase.js.map +1 -1
  90. package/dist/tag.d.ts +10 -2
  91. package/dist/tag.d.ts.map +1 -1
  92. package/dist/tag.js +3 -2
  93. package/dist/tag.js.map +1 -1
  94. package/dist/tags-input.d.ts +6 -0
  95. package/dist/tags-input.d.ts.map +1 -1
  96. package/dist/tags-input.js +3 -1
  97. package/dist/tags-input.js.map +1 -1
  98. package/dist/text-formats.d.ts +2 -2
  99. package/dist/top.d.ts +2 -2
  100. package/dist/transfer-list.d.ts +1 -1
  101. package/dist/transfer-list.d.ts.map +1 -1
  102. package/dist/transfer-list.js +2 -1
  103. package/dist/transfer-list.js.map +1 -1
  104. package/dist/upload-item.d.ts +1 -10
  105. package/dist/upload-item.d.ts.map +1 -1
  106. package/dist/upload-item.js +1 -2
  107. package/dist/upload-item.js.map +1 -1
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/dist/version.js.map +1 -1
  111. package/docs/activity-chip.md +4 -3
  112. package/docs/catalog-freeze.json +5 -2
  113. package/docs/chart.md +1 -1
  114. package/docs/consumer-policy.md +13 -6
  115. package/docs/description-list.md +4 -0
  116. package/docs/design-profile.md +28 -2
  117. package/docs/font-roles.md +1 -1
  118. package/docs/generated/component-maturity.md +5 -4
  119. package/docs/generated/renderer-evidence.json +92 -6
  120. package/docs/generated/renderer-evidence.md +5 -3
  121. package/docs/generated/showcase-manifest.json +49 -1
  122. package/docs/gravity-letters.md +7 -2
  123. package/docs/masonry.md +4 -2
  124. package/docs/screen-patterns.md +5 -2
  125. package/docs/showcase.md +7 -9
  126. package/docs/stable-promotion.md +13 -1
  127. package/docs/usage/README.md +40 -10
  128. package/docs/usage/SELECTION.md +30 -4
  129. package/docs/usage/STANDARD.md +11 -2
  130. package/docs/usage/components/account-switcher.md +98 -0
  131. package/docs/usage/components/activity-chip.md +4 -4
  132. package/docs/usage/components/badge.md +1 -1
  133. package/docs/usage/components/bezier-editor.md +99 -0
  134. package/docs/usage/components/bottom-cta.md +2 -1
  135. package/docs/usage/components/button.md +26 -12
  136. package/docs/usage/components/carousel.md +4 -4
  137. package/docs/usage/components/chart.md +1 -0
  138. package/docs/usage/components/chat-message.md +18 -12
  139. package/docs/usage/components/chat-screen.md +6 -3
  140. package/docs/usage/components/cohort-retention-table.md +83 -0
  141. package/docs/usage/components/collapsible.md +7 -2
  142. package/docs/usage/components/comment-thread-row.md +102 -0
  143. package/docs/usage/components/comment-thread-screen.md +27 -13
  144. package/docs/usage/components/composition-layout.md +1 -0
  145. package/docs/usage/components/context-menu.md +32 -15
  146. package/docs/usage/components/conversation-row.md +11 -4
  147. package/docs/usage/components/conversion-funnel.md +85 -0
  148. package/docs/usage/components/correlation-matrix.md +80 -0
  149. package/docs/usage/components/data-table.md +4 -0
  150. package/docs/usage/components/description-list.md +2 -1
  151. package/docs/usage/components/editor-screen.md +9 -5
  152. package/docs/usage/components/effect-surface.md +4 -2
  153. package/docs/usage/components/empty-state.md +2 -2
  154. package/docs/usage/components/event-attendance-card.md +104 -0
  155. package/docs/usage/components/expandable-search.md +105 -0
  156. package/docs/usage/components/file-picker.md +5 -0
  157. package/docs/usage/components/filter-recovery.md +97 -0
  158. package/docs/usage/components/follow-profile-card.md +100 -0
  159. package/docs/usage/components/heading.md +6 -2
  160. package/docs/usage/components/histogram-explorer.md +89 -0
  161. package/docs/usage/components/icon-button.md +18 -2
  162. package/docs/usage/components/icon-tile.md +12 -4
  163. package/docs/usage/components/image-choice-group.md +81 -0
  164. package/docs/usage/components/import-field-mapping.md +94 -0
  165. package/docs/usage/components/inline-edit.md +105 -0
  166. package/docs/usage/components/keyboard-avoidance-context.md +60 -0
  167. package/docs/usage/components/keyboard-avoiding.md +5 -1
  168. package/docs/usage/components/list-detail-screen.md +4 -4
  169. package/docs/usage/components/list.md +1 -1
  170. package/docs/usage/components/masonry.md +6 -7
  171. package/docs/usage/components/media-selection-screen.md +7 -5
  172. package/docs/usage/components/media-transport.md +104 -0
  173. package/docs/usage/components/message-composer.md +13 -3
  174. package/docs/usage/components/moderation-screen.md +5 -2
  175. package/docs/usage/components/notification-inbox-screen.md +3 -3
  176. package/docs/usage/components/notification-row.md +64 -8
  177. package/docs/usage/components/onboarding-screen.md +1 -1
  178. package/docs/usage/components/overview-screen.md +9 -3
  179. package/docs/usage/components/pagination.md +18 -3
  180. package/docs/usage/components/password-rules-field.md +90 -0
  181. package/docs/usage/components/permission-screen.md +1 -1
  182. package/docs/usage/components/photo-source-sheet.md +1 -1
  183. package/docs/usage/components/popover.md +6 -3
  184. package/docs/usage/components/pricing-choice.md +85 -0
  185. package/docs/usage/components/profile-summary.md +56 -8
  186. package/docs/usage/components/progress.md +3 -3
  187. package/docs/usage/components/qr-code.md +5 -9
  188. package/docs/usage/components/repeatable-field-group.md +92 -0
  189. package/docs/usage/components/reply-composer.md +83 -0
  190. package/docs/usage/components/reply-message.md +85 -0
  191. package/docs/usage/components/result.md +3 -3
  192. package/docs/usage/components/saved-items-screen.md +7 -5
  193. package/docs/usage/components/screen-layout.md +21 -10
  194. package/docs/usage/components/search-field.md +1 -1
  195. package/docs/usage/components/section.md +2 -2
  196. package/docs/usage/components/selection-action-bar.md +89 -0
  197. package/docs/usage/components/settings-screen.md +13 -11
  198. package/docs/usage/components/sheet.md +19 -7
  199. package/docs/usage/components/shortcut-recorder.md +77 -0
  200. package/docs/usage/components/split-action.md +94 -0
  201. package/docs/usage/components/streaming-answer.md +94 -0
  202. package/docs/usage/components/switch.md +6 -2
  203. package/docs/usage/components/table-column-settings.md +88 -0
  204. package/docs/usage/components/target-summary.md +79 -0
  205. package/docs/usage/components/text.md +11 -5
  206. package/docs/usage/components/toggle-feedback-button.md +94 -0
  207. package/docs/usage/components/top-bar.md +8 -12
  208. package/docs/usage/components/transfer-task.md +87 -0
  209. package/docs/usage/compositions/action-recovery-optimistic.md +18 -14
  210. package/docs/usage/compositions/action-recovery-save.md +18 -14
  211. package/docs/usage/compositions/action-recovery-undo.md +18 -14
  212. package/docs/usage/compositions/activity-status.md +1 -1
  213. package/docs/usage/compositions/adaptive-content.md +2 -1
  214. package/docs/usage/compositions/collection-detail.md +6 -5
  215. package/docs/usage/compositions/collection-editing.md +13 -6
  216. package/docs/usage/compositions/command-records.md +5 -4
  217. package/docs/usage/compositions/communication.md +7 -5
  218. package/docs/usage/compositions/compound-controls.md +7 -3
  219. package/docs/usage/compositions/content-transition-comparison.md +10 -5
  220. package/docs/usage/compositions/data-layouts.md +12 -16
  221. package/docs/usage/compositions/data-workflows.md +7 -4
  222. package/docs/usage/compositions/date-time-selection.md +5 -5
  223. package/docs/usage/compositions/design-profile-comparison.md +22 -6
  224. package/docs/usage/compositions/disclosure.md +14 -9
  225. package/docs/usage/compositions/document-resource.md +1 -1
  226. package/docs/usage/compositions/environment-matrix.md +16 -12
  227. package/docs/usage/compositions/expo-interactions.md +9 -8
  228. package/docs/usage/compositions/family-drawer.md +9 -5
  229. package/docs/usage/compositions/field-group.md +7 -3
  230. package/docs/usage/compositions/interaction-adapters.md +11 -9
  231. package/docs/usage/compositions/interaction-flow-apply.md +17 -15
  232. package/docs/usage/compositions/interaction-flow-search.md +11 -7
  233. package/docs/usage/compositions/live-list.md +12 -8
  234. package/docs/usage/compositions/navigation-bar-collection.md +8 -3
  235. package/docs/usage/compositions/optional-motion.md +2 -2
  236. package/docs/usage/compositions/paper-surface.md +11 -6
  237. package/docs/usage/compositions/purpose-input-comment.md +10 -4
  238. package/docs/usage/compositions/purpose-input-message.md +10 -4
  239. package/docs/usage/compositions/reference-first.md +4 -4
  240. package/docs/usage/compositions/reference-review.md +4 -4
  241. package/docs/usage/compositions/reference-settings.md +4 -4
  242. package/docs/usage/compositions/selection-motion.md +2 -1
  243. package/docs/usage/compositions/social-compositions.md +7 -6
  244. package/docs/usage/compositions/stea-event-ticket.md +2 -4
  245. package/docs/usage/compositions/stea-order-progress.md +19 -21
  246. package/docs/usage/compositions/stea-pixel-empty.md +26 -26
  247. package/docs/usage/compositions/task-controls.md +7 -2
  248. package/docs/usage/compositions/texture-comparison.md +5 -5
  249. package/docs/usage/compositions/toast-layout.md +7 -3
  250. package/docs/usage/compositions/toggle-feedback.md +1 -1
  251. package/docs/usage/compositions/visual-foundations.md +10 -5
  252. package/docs/usage/compositions/web-additions.md +5 -1
  253. package/docs/usage/screens/common-chat.md +5 -3
  254. package/docs/usage/screens/common-comments.md +14 -9
  255. package/docs/usage/screens/common-conversation-list.md +109 -0
  256. package/docs/usage/screens/common-inbox.md +5 -3
  257. package/docs/usage/screens/common-login.md +7 -4
  258. package/docs/usage/screens/common-profile.md +16 -12
  259. package/docs/usage/screens/common-saved.md +12 -8
  260. package/docs/usage/screens/common-search.md +16 -8
  261. package/docs/usage/screens/common-settings.md +12 -11
  262. package/docs/usage/screens/common-shell.md +12 -10
  263. package/docs/usage/screens/dashboard.md +59 -56
  264. package/docs/usage/screens/discovery-gallery.md +18 -9
  265. package/docs/usage/screens/flow-collection.md +10 -6
  266. package/docs/usage/screens/flow-editor.md +9 -6
  267. package/docs/usage/screens/flow-media.md +12 -7
  268. package/docs/usage/screens/flow-moderation.md +9 -3
  269. package/docs/usage/screens/flow-onboarding.md +13 -8
  270. package/docs/usage/screens/flow-permission.md +5 -3
  271. package/docs/usage/screens/landing.md +20 -14
  272. package/docs/usage/screens/mockup-studio.md +100 -88
  273. package/docs/usage/screens/notification-settings.md +9 -7
  274. package/docs/usage/screens/product-bento.md +22 -10
  275. package/docs/usage/screens/reference-comparison.md +2 -2
  276. package/docs/usage/tokens/font-roles.md +18 -2
  277. package/docs/usage/tokens/typography.md +22 -11
  278. package/package.json +7 -1
@@ -2,10 +2,10 @@
2
2
 
3
3
  - 단계: 구성
4
4
  - 상태: 배포
5
- - 지원: Web
5
+ - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
7
  - 검토일: 2026-10-15
8
- - 근거: [Toast](../../toast.md), `showcase/web/src/components/ToastLayout.stories.tsx`, `src/component-recipes.ts`(`toastRecipe`), `src/toast.ts`, `packages/react/src/styles.css`(`.hjm-toast-viewport`, `.hjm-toast`), `packages/react/src/toast.tsx`
8
+ - 근거: [Toast](../../toast.md), `showcase/web/src/components/ToastLayout.stories.tsx`, `showcase/native/src/ToastLayout.stories.tsx`, `showcase/shared/toast-layout.ts`, `src/component-recipes.ts`(`toastRecipe`), `src/toast.ts`, `packages/react/src/styles.css`(`.hjm-toast-viewport`, `.hjm-toast`), `packages/react/src/toast.tsx`
9
9
  - 스토리북: `배포/구성/비교와 검증/토스트 배치 비교`
10
10
 
11
11
  공통 구조·배치·상태·배지의 소유권은 [화면과 구성 선택](../SELECTION.md#소유권)을 따른다. 앱은 데이터·문구·브랜드·추가 기능을 공개 슬롯과 콜백으로 연결한다. 아래 코드 골격을 앱별 기본 구조 복제의 근거로 사용하지 않는다. 전용 구성 API가 없는 콘텐츠 묶음은 [CompositionLayout](../components/composition-layout.md)의 제목·배지·행동·본문·상태·footer 슬롯을 사용한다.
@@ -51,7 +51,7 @@ Toast 카드 한 장의 내부 배치(톤 배지·제목·설명·닫기·실행
51
51
  | --- | --- | --- | --- |
52
52
  | 바깥 틀 | `ToastProvider` viewport(앱 루트 하나) | 화면 콘텐츠·스크롤 위에 `position: fixed`로 떠 있다. 콘텐츠 스크롤과 무관하고 콘텐츠 여백을 바꾸지 않는다. 안전 영역은 viewport가 `env(safe-area-inset-*)`로 직접 피한다. 하단 고정 바는 `bottomOffset`으로 피한다. 모바일 Web 화면 키보드는 따로 피하지 않는다 | 가장자리 `max(spacing.md 16, safe-area)`, `layer.toast` 1000, viewport는 포인터를 통과시킨다(`pointer-events: none`, 카드만 받음) |
53
53
  | viewport | `ToastProvider` | 화면 고정(`position: fixed`), 기본 하단 가운데. `top`·`top-start`·`top-end`·`bottom-start`·`bottom-end` 선택 | 가장자리 `max(spacing.md 16, safe-area)`, 최대 폭 `min(420, 100vw - 32)`, 카드 사이 `spacing.sm` 12. 하단이면 새 카드가 아래부터 쌓인다 |
54
- | 카드 | `Toast` | viewport 안 | 최소 높이 3.5rem, padding 위아래 `spacing.sm` 12·시작 `spacing.md` 16·끝 `spacing.xs` 8, 열 간격 `spacing.sm` 12, radius `lg` 16 |
54
+ | 카드 | `Toast` | viewport 안. 단독 controlled 사용에서도 카드가 최대 폭 `min(420, 100%)`(Native `toastRecipe.surface.maxWidth` 420)을 지킨다 | 최소 높이 3.5rem, padding 위아래 `spacing.sm` 12·시작 `spacing.md` 16·끝 `spacing.xs` 8, 열 간격 `spacing.sm` 12, radius `lg` 16 |
55
55
  | 배지 | tone 아이콘 | 첫 줄 시작 | 지름 `toastRecipe.icon.badgeDiameter` 32 |
56
56
  | 문구 | 제목 + 설명 | 첫 줄 가운데, 남은 폭 전부 | 제목–설명 `spacing.xxs` 4, `word-break: keep-all`, 잘림 없음 |
57
57
  | 닫기 | IconButton | 첫 줄 끝 | `control.minTouchTarget` 44 × 44 |
@@ -128,3 +128,7 @@ toast.publish({
128
128
  - 카드 색·배지·radius를 `className`으로 덮지 않는다. 톤 색은 `toastRecipe.tones`가 정한다.
129
129
  - 화면마다 Provider를 새로 만들지 않는다. 같은 진행을 갱신할 때는 같은 `id`로 다시 `publish`한다.
130
130
  - 현재 스토리는 `Toast` 카드만 직접 그리고 viewport(`ToastProvider`)를 띄우지 않는다. 화면 위 위치·안전 영역·`bottomOffset`은 스토리로 확인되지 않고 위 표의 값은 `.hjm-toast-viewport` CSS에서 확인했다.
131
+
132
+ ## 근거
133
+
134
+ - 2026-10-17 구성 리뷰: 최대 폭이 viewport에만 있어 단독 Toast가 데스크톱에서 1200px로 늘어났고, Native stable인데 배치 비교가 Web에만 있었다. 그래서 카드가 폭을 소유하게 하고 같은 descriptor로 Native 스토리를 추가했다.
@@ -83,4 +83,4 @@ import { ToggleFeedbackButton } from "@hjmds/react-native/toggle-feedback";
83
83
 
84
84
  북마크는 같은 API에 `effect="ribbon"`을 준다. 실제 실행 예제는
85
85
  Web `showcase/web/src/patterns/toggle-feedback-preview.tsx`, Native `showcase/native/src/toggle-feedback-preview.tsx`다.
86
- 예시 callback은 명시적 fixture이며 제품 서버나 기기 성능 검증이 아니다.
86
+ 예시 callback은 명시적 fixture이며 제품 서버나 기기 성능 검증이 아니다. 예제의 "다음 요청 실패시키기"는 구성 아래 "미리보기 도구" 묶음의 예제 전용 도구이며 제품 화면 요소가 아니다.
@@ -24,7 +24,8 @@
24
24
  | Avatar + `createBlobatarFallback` | 사진이 없을 때 seed로 정해지는 얼굴, `expression: "happy"` | [Avatar](../components/avatar.md) |
25
25
  | TextTransition | 문장이 바뀔 때 `fade`·`rise`·`slide`·`scale` 전환 | [TextTransition](../components/text-transition.md) |
26
26
  | Heading | 효과 표면 안 제목, 얼굴 묶음 제목(`level2`) | [Heading](../components/heading.md) |
27
- | Button | 효과 켜기(`selected`)·다음 장면 | [Button](../components/button.md) |
27
+ | Switch | 배경 움직임 켜고 끄기(`presentation="row"`) | [Switch](../components/switch.md) |
28
+ | Button | 다음 장면 | [Button](../components/button.md) |
28
29
  | `ScrollView` + `Container`(Native) | 바깥 틀. 세로 스크롤·위아래 여백, 좌우 gutter | [Container](../components/container.md), [화면 여백과 너비](../tokens/layout.md) |
29
30
 
30
31
  비교 결과로 고르는 기준이다.
@@ -41,7 +42,7 @@
41
42
  ```text
42
43
  Native 화면(ScrollView, 위아래 spacing.lg 20) > Container gutter 16(폭 < 600) · 20(폭 ≥ 600)
43
44
  배경 효과 (Stack gap spacing.xl 24)
44
- [ 배경 움직임 ] ← selected 토글
45
+ 배경 움직임 [ ○] ← Switch row
45
46
  ┌──── EffectSurface (mesh) ────────────────────┐
46
47
  │ 안쪽 여백 spacing.xl 24(두 플랫폼 같음) │
47
48
  │ mesh (label) │
@@ -60,7 +61,7 @@ Native 화면(ScrollView, 위아래 spacing.lg 20) > Container gutter 16(폭 < 6
60
61
 
61
62
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
62
63
  | --- | --- | --- | --- |
63
- | 바깥 틀 | Web: HJM ScreenLayout의 본문 > Stack. Native: `ScrollView` > `Container` > Stack | 이 구성은 바깥 폭·여백을 정하지 않는다. Native는 `ScrollView` 안 [Container](../components/container.md)에 둔다. 입력이 없어 키보드 처리는 없다. 안전 영역은 화면 골격(내비게이션 헤더·탭 바)이 맡는다 | Native ScrollView 위아래 `spacing.lg` 20, Container gutter 폭 600 미만 `compact` 16 · 이상 `regular` 20. 묶음 사이 Stack `gap="xl"` 24 |
64
+ | 바깥 틀 | Web: `Container size="reading"` > Stack. Native: `ScrollView` > `Container size="reading"` > Stack | 이 구성은 바깥 폭·여백을 정하지 않는다. Native는 `ScrollView` 안 [Container](../components/container.md)에 둔다. 입력이 없어 키보드 처리는 없다. 안전 영역은 화면 골격(내비게이션 헤더·탭 바)이 맡는다 | Native ScrollView 위아래 `spacing.lg` 20, Container gutter 폭 600 미만 `compact` 16 · 이상 `regular` 20. 묶음 사이 Stack `gap="xl"` 24 |
64
65
  | 효과 표면 | EffectSurface > 안쪽 틀 > Stack `gap="lg"` | 세로로 쌓음(비교용. 실제 화면은 히어로 하나) | EffectSurface는 여백이 없다. 안쪽 틀 여백 `spacing.xl` 24(두 플랫폼 같음, [랜딩](../screens/landing.md) 히어로와 같다), 내용 사이 `spacing.lg` 20, `intensity` 0.4(예) |
65
66
  | 아이콘 행 | Icon `size="lg"` + Text | 가로(Stack `axis="inline"`) | 아이콘 `lg` 28, 사이 `spacing.md` 16 |
66
67
  | 얼굴 행 | Avatar ×2 + Text | 가로(Stack `axis="inline"`) | 두 플랫폼 48(Web `size="large"`, Native `size={48}`), 사이 `spacing.md` 16 |
@@ -68,7 +69,7 @@ Native 화면(ScrollView, 위아래 spacing.lg 20) > Container gutter 16(폭 < 6
68
69
 
69
70
  ## 흐름과 상태
70
71
 
71
- 1. "배경 움직임"을 켜면 모든 EffectSurface `active`가 켜져 천천히 움직인다. 다시 누르면 멈춘다.
72
+ 1. "배경 움직임" Switch를 켜면 모든 EffectSurface `active`가 켜져 천천히 움직인다. 끄면 멈춘다. 켜고 끄기는 [선택 조립 규칙](../SELECTION.md#조립-규칙필수)대로 `selected` Button이 아니라 Switch다.
72
73
  2. "다음 장면"을 누르면 네 preset의 문장이 동시에 다음 문장으로 바뀐다.
73
74
 
74
75
  - 장면 문구 키는 순서 있는 상수 배열로 둔다(`sceneKeys[index]`). 번호로 키를 조립하지 않는다.
@@ -78,7 +79,7 @@ Native 화면(ScrollView, 위아래 spacing.lg 20) > Container gutter 16(폭 < 6
78
79
  | 기본 | 효과 정지(`active` 기본 `false`) | 효과 층은 장식 |
79
80
  | 진행 중 | — (요청이 없다). 문장 전환 중에는 이전 문장이 나가고 새 문장이 들어온다 | — |
80
81
  | 실패 | — (요청이 없다). 프로필 사진이 없거나 로드에 실패하면 Avatar가 `renderFallback`(Blobatar)으로 바꾼다 | Avatar 이름이 읽힌다 |
81
- | 움직임 켬 | 층이 `period` 주기(기본 12초)로 움직인다 | 토글 버튼 `selected` 상태로 알린다 |
82
+ | 움직임 켬 | 층이 `period` 주기(기본 12초)로 움직인다 | Switch의 켜짐 상태로 알린다 |
82
83
  | reduced motion | 효과·전환 모두 즉시·정지 | — |
83
84
  | 사진 없음 | Blobatar 얼굴, 같은 seed는 같은 얼굴 | 그림은 장식, Avatar 이름이 읽힌다 |
84
85
  | 다크 | 효과 색이 `primary`·`contentBrand`·`surfaceAccent` 테마 값을 따른다 | — |
@@ -185,3 +186,7 @@ seed·문구·아이콘 세트·얼굴 목록은 제품 소유다. `createBlobat
185
186
  - EffectSurface는 화면 위쪽 히어로 하나에만 둔다. 스토리의 네 장 세로 쌓기는 비교용이다.
186
187
  - 2026-10-06 예제 검수에서 Container·Stack·Heading을 두 플랫폼에 맞췄다. 효과 안쪽 여백은 spacing.xxl(32), 얼굴은 64, 제목은 level2로 동일하다. 행은 큰 글자에서 줄바꿈한다.
187
188
  - Native EffectSurface는 AppState·reducedMotion을 반영한다. 백그라운드 중단과 내비게이션으로 가려진 화면의 중단은 다르므로, 마운트된 채 가려지는 제품 화면·목록은 `visible`을 연결한다. Storybook 정지 화면만으로 이 제품 연결을 검증했다고 보고하지 않는다.
189
+
190
+ ## 근거
191
+
192
+ - 2026-10-17 구성 리뷰: Web Container가 content 폭(1200)과 compact gutter로 고정돼 데스크톱에서 Switch 이름과 토글이 양 끝으로 멀어졌다. 그래서 읽기 폭과 창 크기별 gutter로 맞췄다.
@@ -62,7 +62,7 @@ Native 화면에서 같은 일이 필요하면 이 구성을 옮기지 않는다
62
62
  | 바깥 틀 | HJM ScreenLayout의 본문 | 세 스토리 모두 바깥 폭·여백을 정하지 않는다. 페이지 본문은 [Container](../components/container.md)가 정한다. Affix만 가장 가까운 스크롤 조상(문서 또는 제품의 `overflow: auto` 영역)에 붙는다. Web 전용이라 안전 영역·화면 키보드 처리는 없다 | Container gutter 폭 600 미만 `compact` 16 · 이상 `regular` 20 |
63
63
  | 색 입력 줄 | `ColorPicker` 색 견본 + HEX | fieldset 첫 줄, 좁으면 줄바꿈(`flex-wrap`) | 입력 최소 높이 `colorPickerRecipe.minTargetSize` 44, 색 견본 폭 3rem, 줄 간격 0.75rem |
64
64
  | 불투명도·미리보기·프리셋 | `ColorPicker` 내부 | 색 입력 아래 순서대로 | 각 위 여백 0.75rem, 프리셋 사이 0.5rem, 프리셋 최소 높이 44 |
65
- | 선택값 | live 문구 | ColorPicker 바로 아래 | HJM 구성 레이아웃 소유 |
65
+ | 선택값 | live 문구 | ColorPicker 바로 아래, 같은 Stack | `spacing.md` 16. 스토리는 `Container size="reading"`(최대 720) |
66
66
  | 워터마크 | `Watermark` | 감싼 본문 전체를 덮는 오버레이(`inset: 0`) | `tileWidth` 240 · `tileHeight` 160 · `rotate` -22 · `opacity` 0.12(기본값), 색 `--hjm-color-text-sub` |
67
67
  | 고정 행동 | `Affix` | 스크롤 조상 상단 + `offset` | `offset` 기본 0, 띄울 때는 spacing 토큰 값(스토리 `spacing.xs` 8). 스크롤 조상보다 높으면 고정하지 않고 흐름에 남는다 |
68
68
  | 저장 실패 | `Notice` danger | 저장 버튼 위(Watermark 본문 안, Affix 아래 본문 첫머리) | 버튼과 `spacing.md` 16 |
@@ -146,3 +146,7 @@ import { Stack, Text } from "@hjmds/react/layout";
146
146
  - Affix `offset`이 음수이거나 유한하지 않으면 `TypeError`를 던진다.
147
147
  - Watermark 범위(`tileWidth` ≥ 80, `tileHeight` ≥ 60, `rotate` -90~90, `opacity` 0~1, 줄 1~3개·각 120자 이하)를 벗어나면 `TypeError`다.
148
148
  - 현재 스토리의 저장은 메모리 상태만 확정하며 실제 파일·서버 저장이나 진행 중·실패 경로는 없다. 성공 뒤 `aria-disabled`로 중복 실행을 막아 키보드 초점과 sticky 안의 버튼 노드를 유지한다. 제품의 저장·실패 경로는 위 코드 골격처럼 제품이 공급한다.
149
+
150
+ ## 근거
151
+
152
+ - 2026-10-17 구성 리뷰: ColorPicker가 데스크톱 폭 전체로 늘어나고 결과 문구가 Fragment로 붙어 간격이 정해져 있지 않았다. 고정 실행 영역의 스크롤 host는 HJM에 높이를 받는 공개 스크롤 영역이 없어 스토리 fixture로 남긴다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 구성 API 지침 참조
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/채팅`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
9
  - 스토리북: `배포/화면/소통/채팅`
10
10
 
@@ -46,8 +46,6 @@ host: 남은 높이·safe area·키보드
46
46
 
47
47
  ### 첫 메시지부터 실패 복구까지
48
48
 
49
- 2026-10-13 전수 점검에서 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
50
-
51
49
  | 상황 | 연결 | 유지·복구 |
52
50
  | --- | --- | --- |
53
51
  | 첫 메시지 | empty + composer | 미게시(1.18.0 이후): 첫 입력을 허용한다. 로딩·오류·권한 제한은 composer를 숨긴다 |
@@ -140,3 +138,7 @@ HJM `Timeline`(이벤트 연표)과 `VirtualList`(고정 `rowHeight`)는 높이
140
138
  - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크와 실패/복구를 각각 확인한다.
141
139
  - 긴 대화를 `scroll="screen"` + map으로 그리지 않는다. Native는 `FlatList`(기본 `scroll="content"`, 답장 원문 이동은 `scrollToIndex`), Web은 가변 높이 가상 목록이다. Web 예제의 `scroll="screen"`은 짧은 고정 기록이라서다.
142
140
  - 실패 복구 스토리(`실패와 초안 복구`)는 다음 전송 실패를 예약하는 데모 도구로 연다.
141
+
142
+ ## 근거
143
+
144
+ - 첫 메시지부터 실패 복구까지 상황 표: 2026-10-13 전수 점검 — 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 구성 API 지침 참조
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/댓글`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
9
  - 스토리북: `배포/화면/소통/댓글`
10
10
 
@@ -19,8 +19,7 @@ CommentThreadScreen을 사용해 댓글 흐름을 구성한다. 제품이 데이
19
19
 
20
20
  짧은 뒤로/제목 행은 `header`, 장소 소개·원문·필터는 `threadHeader`, 추가 페이지 버튼은
21
21
  `threadFooter`, 작성기는 `composer`에 둔다. threadHeader와 댓글은 하나의 본문 스크롤을
22
- 공유한다. 소개를 고정 header 안의 별도 스크롤로 제한하지 않는다. Spint의 소개가 잘려
23
- 스팟 선택이 숨는 문제(2026-10-08)를 해결하기 위한 배치다.
22
+ 공유한다. 소개를 고정 header 안의 별도 스크롤로 제한하지 않는다.
24
23
 
25
24
 
26
25
  ```text
@@ -38,7 +37,8 @@ host: 남은 높이·safe area·키보드
38
37
 
39
38
  | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
40
39
  | --- | --- | --- | --- |
41
- | 작업 | CommentThreadScreen 공개 행동 슬롯 | 좋아요와 세로 점 더보기는 댓글 오른쪽의 같은 행; 답글은 본문 아래; 보내기는 하단 composer | 같은 표면에 경쟁하는 primary 하나만 |
40
+ | 작업 | CommentThreadScreen 공개 행동 슬롯 | 좋아요와 세로 점 더보기는 댓글 오른쪽의 같은 행(더보기는 `commentMenu`로 HJM이 행 끝에 그린다); 답글은 본문 아래; 보내기는 하단 composer | 같은 표면에 경쟁하는 primary 하나만 |
41
+ | 댓글 메뉴 | `commentMenu` 항목 | 세로 점 버튼 → 메뉴(답글 달기 → 신고, 내 댓글이면 삭제는 danger로 끝) | 항목·권한은 제품, 순서·위치·이름 규칙은 HJM |
42
42
  | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
43
43
 
44
44
  ## 상태
@@ -53,8 +53,6 @@ host: 남은 높이·safe area·키보드
53
53
 
54
54
  ### 댓글 목록과 입력의 독립 복구
55
55
 
56
- 2026-10-13 전수 점검에서 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
57
-
58
56
  | 상황 | 연결 | 유지·복구 |
59
57
  | --- | --- | --- |
60
58
  | 답글 접기·펼치기 | expandedIds와 원문 데이터 분리 | 서버 순서를 보존하고 접어도 답글 초안을 지우지 않는다 |
@@ -128,6 +126,13 @@ import { MessageComposer } from "@hjmds/react-native/screens";
128
126
  - 빈 댓글에서도 작성창 유지; 실패한 전송은 초안 유지.
129
127
  - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
130
128
 
131
- 2026-10-07 하트 옆 더보기 요청에 따라 두 renderer의 `CommentThreadItem.actions`를
132
- 좋아요 바로 뒤에 둔다. Storybook 예제는 공개 Menu와 MoreVertical을 쓰며 메뉴의 답글/좋아요는
133
- 기존 controlled callback을 호출한다. 제품의 신고·삭제 권한이나 서버를 예제에서 추정하지 않는다.
129
+ 두 renderer의 `CommentThreadItem.actions`는 기존 호환 슬롯으로 좋아요 바로 뒤에 남는다. 더보기 메뉴는 `actions`로 조립하지 않고
130
+ `commentMenu`를 쓴다. Storybook 예제(`instagram-comments-preview`)는 원댓글에 "답글 달기", 남의 댓글에 "신고",
131
+ 내 댓글에 "삭제"(danger)를 넣고 기존 controlled callback을 호출한다. 제품의 신고·삭제 권한이나 서버를 예제에서 추정하지 않는다.
132
+
133
+ ## 근거
134
+
135
+ - 댓글 목록과 입력의 독립 복구 상황 표: 2026-10-13 전수 점검 — 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
136
+ - threadHeader가 댓글과 본문 스크롤 공유: 2026-10-08 Spint에서 고정 header 안의 소개가 잘려 스팟 선택이 숨었다.
137
+ - `actions`를 좋아요 바로 뒤에 둠: 2026-10-07 하트 옆 더보기 요청.
138
+ - 더보기 메뉴를 HJM `commentMenu`로: 2026-10-17 사용자 결정. 2026-10-15 규칙(메시지·댓글의 공통 구조는 HJM 소유)과 화면 검토 S4#7(예제·제품이 `actions`로 메뉴 버튼을 직접 조립)에 따른다.
@@ -0,0 +1,109 @@
1
+ # 대화 목록
2
+
3
+ - 단계: 화면
4
+ - 상태: 실험
5
+ - 지원: Web · Native
6
+ - 적용: 구성 API 지침 참조
7
+ - 검토일: 2026-10-17
8
+ - 근거: 2026-10-16 사용자가 제공한 인스타그램 iOS DM 목록·길게 누르기 캡처. 행은 [ConversationRow](../components/conversation-row.md), 행 메뉴는 [ContextMenu](../components/context-menu.md)(Native canonical은 같은 날 추가, 미게시)
9
+ - 스토리북: `실험/화면/소통/대화 목록`
10
+
11
+ 공통 구조·배치·상태·배지의 소유권은 [화면과 구성 선택](../SELECTION.md#소유권)을 따른다. 앱은 데이터·문구·브랜드·추가 기능을 공개 슬롯과 콜백으로 연결한다. 아래 코드 골격을 앱별 기본 구조 복제의 근거로 사용하지 않는다.
12
+
13
+ ## 목적
14
+
15
+ DM·채팅방 목록에서 상대·마지막 메시지·시각·읽지 않음을 훑고, 행을 눌러 대화로 들어가며, 길게 눌러 행 단위 동작(읽지 않음 표시·알림 해제·차단·삭제)을 고른다.
16
+
17
+ ## 영역 구조
18
+
19
+ ```text
20
+ host: 남은 높이·safe area
21
+ └─ 헤더(제목·작성 버튼) → notice(검색·안내) → 대화 행 목록(가상화)
22
+ ```
23
+
24
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
25
+ | --- | --- | --- | --- |
26
+ | 바깥 틀 | ScreenLayout(`scroll="content"`, `contentInset="body-edge"`) | route 본문 | 헤더·notice는 화면 recipe padding 16, 본문 좌우는 0(행이 여백 소유) |
27
+ | 검색 | SearchField(라벨 "대화 검색", 검색 아이콘, placeholder "이름이나 메시지 검색") | `notice` | 화면 recipe itemGap 12. 결과가 없으면 `state.empty` |
28
+ | 행 | 라우팅 host(Pressable·링크) + ConversationRow | 본문 목록 | 아바타 56, 행 padding 가로 16(`conversationRowRecipe.paddingInline`)·세로 10, 화면 끝에서 아바타까지 16 |
29
+ | 행 메뉴 | ContextMenu | 행 host를 감싼다 | Native 카드 폭 최대 280, 행 48, 아이콘 24 |
30
+
31
+ ## 버튼과 행동 위치
32
+
33
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
34
+ | --- | --- | --- | --- |
35
+ | 대화 열기 | 행 host 탭(link) | 행 전체 | 하나 |
36
+ | 행 동작 | ContextMenu 항목 | 길게 누름(Web은 우클릭·Shift+F10, Native는 접근성 동작) | 일반 동작 먼저, `tone: "danger"`(차단·삭제)는 끝 |
37
+ | 새 대화 | IconButton | 헤더 `actions` | 하나 |
38
+
39
+ ## 상태
40
+
41
+ | 상태 | 화면 모습 | 행동 |
42
+ | --- | --- | --- |
43
+ | 기본 | 행 목록 | 행 탭·길게 누름 |
44
+ | 로딩 | `state.loading`이 본문을 교체 | 없음 |
45
+ | 빈 | `state.empty` + 첫 대화 시작 안내 | `stateAction`으로 새 대화 |
46
+ | 오류 | 최초 조회 실패는 `state.error`, 새로고침 실패는 목록 유지 + `notice` | 재시도 |
47
+
48
+ ## 사용하는 지침
49
+
50
+ | 지침 | 쓰는 곳 |
51
+ | --- | --- |
52
+ | [ConversationRow](../components/conversation-row.md) | 행 표시 |
53
+ | [ContextMenu](../components/context-menu.md) | 행 길게 누르기 메뉴 |
54
+ | [ScreenLayout](../components/screen-layout.md) | 바깥 틀·상태 |
55
+
56
+ ## 코드 골격
57
+
58
+ ```tsx
59
+ // Native
60
+ import { ConversationRow } from "@hjmds/react-native/conversation-row";
61
+ import { ContextMenu } from "@hjmds/react-native/context-menu";
62
+
63
+ <ContextMenu accessibilityLabel={t("dm.rowMenu", { name })} onAction={id => runRowAction(room, id)}
64
+ items={[{ id: "unread", label: t("dm.markUnread"), icon: <MailOpen /> }, { id: "mute", label: t("dm.mute"), icon: <BellOff /> },
65
+ { id: "block", label: t("dm.block"), tone: "danger", icon: <Ban /> }, { id: "delete", label: t("dm.delete"), tone: "danger", icon: <Trash2 /> }]}>
66
+ <Pressable accessibilityRole="link" accessibilityLabel={summary} onPress={openRoom}>
67
+ <ConversationRow title={name} preview={last} metadata={time} unread={unread} avatar={<Avatar source={photo} size={56} decorative />} />
68
+ </Pressable>
69
+ </ContextMenu>
70
+ ```
71
+
72
+ ```tsx
73
+ // Web
74
+ import { ConversationRow } from "@hjmds/react/conversation-row";
75
+ import { ContextMenu } from "@hjmds/react/context-menu";
76
+
77
+ <ContextMenu accessibilityLabel={t("dm.rowMenu", { name })} onAction={id => runRowAction(room, id)}
78
+ items={[{ id: "unread", label: t("dm.markUnread"), textValue: t("dm.markUnread"), icon: <MailOpen size={20} /> },
79
+ { id: "delete", label: t("dm.delete"), textValue: t("dm.delete"), tone: "danger", icon: <Trash2 size={20} /> }]}>
80
+ <Link href={roomHref} aria-label={summary}><ConversationRow title={name} preview={last} metadata={time} unread={unread} avatar={avatar} /></Link>
81
+ </ContextMenu>
82
+ ```
83
+
84
+ ## 큰 글자·다크·좁은 폭
85
+
86
+ | 조건 | 바뀌는 것 |
87
+ | --- | --- |
88
+ | 큰 글자 | 제목·미리보기가 커져도 한 줄 말줄임, 시각·읽지 않음 점은 유지 |
89
+ | 다크 | 행·메뉴 카드가 테마 `bg`·`surface`를 따른다. 위험 항목은 `danger` 글자색 |
90
+ | 좁은 폭 | 미리보기만 줄어든다. Native 메뉴 카드는 화면 폭 − 32와 280 중 작은 폭 |
91
+
92
+ ## 플랫폼 차이
93
+
94
+ | 항목 | Web | Native |
95
+ | --- | --- | --- |
96
+ | 메뉴 열기 | 우클릭·길게 누름·Shift+F10 | 길게 누름, 보조기술은 행의 "메뉴" 동작 |
97
+ | 메뉴 표시 | 포인터 위치 Popover | 배경 어둡게 + 누른 위치 근처 카드 |
98
+ | 메뉴 아이콘 | 항목 `icon`, 24 칸, `currentColor`로 항목 색을 따른다 | 항목 `icon`, 24 칸, 색은 제품이 테마 색으로 넘긴다 |
99
+
100
+ ## 함정
101
+
102
+ - 행 host를 ContextMenu 안에 하나만 둔다. Native ContextMenu는 그 host에 길게 누르기와 접근성 동작을 덧붙이므로, 별도 Pressable로 다시 감싸면 탭이 먹힌다.
103
+ - 위험 동작(차단·삭제)은 확인 대화상자를 제품이 띄운다. 메뉴 항목 선택 즉시 삭제하지 않는다.
104
+
105
+ ## 근거
106
+
107
+ - 헤더 새 대화 IconButton·`notice` 검색·Web 메뉴 아이콘: 2026-10-17 화면 검토(S5)에서 예제가 새 대화 행동 없이, `notice`에 데모 안내 문구를 두고, Web과 Native 문구·메뉴 모양이 갈라진 것을 확인했다. 예제 데이터·문구는 `showcase/shared/conversation-list.ts` 하나를 두 플랫폼이 읽는다.
108
+ - 검색 아이콘·placeholder, 행동 결과 줄의 위치: 같은 날 재검증에서 notice로 옮긴 SearchField가 아이콘도 placeholder도 없는 빈 상자였고, 데모 행동 결과("미리보기 도구")가 기본 스토리 화면 위에 여백 없이 붙어 있었다. 아이콘은 Web `renderSearchIcon`·Native `renderLeading`(lucide `Search`)이고, 결과 줄은 다른 화면처럼 도구 스토리("행동 결과 확인")에서만 보인다.
109
+ - 본문 `contentInset="body-edge"`: 2026-10-17 사용자 결정. 화면 검토 S5#9에서 본문 padding 16과 행 padding 16이 겹쳐 아바타가 약 32pt 안쪽에서 시작했다(인스타그램 DM 약 16). 기존 `none`은 앱 사용처가 "host가 모든 여백 소유"로 써서 의미를 바꿀 수 없었다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 구성 API 지침 참조
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/알림함`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
9
  - 스토리북: `배포/화면/소통/알림함`
10
10
 
@@ -46,8 +46,6 @@ host: 남은 높이·safe area·키보드
46
46
 
47
47
  ### 조회와 읽음 저장 분리
48
48
 
49
- 2026-10-13 전수 점검에서 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
50
-
51
49
  | 상황 | 연결 | 유지·복구 |
52
50
  | --- | --- | --- |
53
51
  | 전체 0건 | empty | 필터는 유지하고 불필요한 다시 시도 버튼을 만들지 않는다 |
@@ -131,3 +129,7 @@ import { NotificationInboxScreen, NotificationItem } from "@hjmds/react-native/s
131
129
  44px 아바타와 썸네일, 우하단 20px 종류 배지, 2줄 미리보기와 6px 읽지 않음 점의 배치는 HJM이 소유한다.
132
130
  제품은 바깥 링크/Pressable 한 곳에 이동·disabled·전체 접근성 이름을 제공한다. 행 내부에 별도 버튼을 넣지 않는다.
133
131
  단순 제목/설명 알림은 기존 `NotificationItem`을 그대로 사용한다. 두 API는 같은 알림함의 콘텐츠 밀도에 따른 선택이다.
132
+
133
+ ## 근거
134
+
135
+ - 조회와 읽음 저장 분리 상황 표: 2026-10-13 전수 점검 — 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 구성 API 지침 참조
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/로그인`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
9
  - 스토리북: `배포/화면/계정/로그인`
10
10
 
@@ -46,8 +46,6 @@ host: 남은 높이·safe area·키보드
46
46
 
47
47
  ### 제공자 목록과 로그인 요청 분리
48
48
 
49
- 2026-10-13 전수 점검에서 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
50
-
51
49
  | 상황 | 연결 | 유지·복구 |
52
50
  | --- | --- | --- |
53
51
  | 목록 조회 중 | 제품이 main 영역 상태 제공 | AuthScreenLayout에는 ScreenLayout.state prop이 없다 |
@@ -108,5 +106,10 @@ import { AuthScreenLayout } from "@hjmds/react-native/auth-screen";
108
106
 
109
107
  - 예제는 로그인 시도마다 작업 식별자를 두고 제공자 중복 실행을 차단한다. 취소·언마운트 후 늦게 도착한 타이머는 홈이나 오류를 열지 않는다. 취소 뒤 새 시도는 별개의 작업이다. 성공 뒤 홈에서 대화·알림으로 이동할 때 로그인 예제의 초기 오류·진행 상태를 넘기지 않는다. 실제 OAuth 취소를 검증한 것은 아니다.
110
108
  - 실패하면 버튼을 복원; 카드 크기 유지와 중복 요청 차단.
111
- - 스토리는 기본·어두운 테마, `로그인 중`, `로그인 실패와 다시 시도`(첫 시도 실패 → 다시 누르면 성공)다. 빈 화면·로그인 필요 같은 본문 상태는 이 화면에 해당하지 않아 2026-10-06 스토리에서 뺐다.
109
+ - 스토리는 기본·어두운 테마, `로그인 중`, `로그인 실패와 다시 시도`(첫 시도 실패 → 다시 누르면 성공)다. 빈 화면·로그인 필요 같은 본문 상태는 이 화면에 해당하지 않아 스토리에 두지 않는다.
112
110
  - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크와 실패/복구를 각각 확인한다.
111
+
112
+ ## 근거
113
+
114
+ - 제공자 목록과 로그인 요청 분리 상황 표: 2026-10-13 전수 점검 — 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
115
+ - 빈 화면·로그인 필요 스토리 없음: 2026-10-06 해당하지 않는 본문 상태 스토리를 뺐다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 구성 API 지침 참조
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screen-flows.tsx`(`ProfileScreen`·`EditorScreen`), 예제 `showcase/*/screen-flow-previews.tsx`(`AccountFlowPreview`)·`saved-profile-previews.tsx`(`ProfileEditFields`), 기본 얼굴 `showcase/web/src/patterns/ProfileStudio.previews.tsx`·`showcase/native/src/profile-face-preview.tsx`·`showcase/shared/profile-studio.ts`. 2026-10-06 사용자 승인으로 실험 `공통 화면/프로필`을 배포하면서 같은 프로필 수정을 직접 조립으로 보이던 배포 `화면/프로필 편집`과 합쳤다(API 기반 우선, Web id `patterns-profile-studio` 보존, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
9
  - 스토리북: `배포/화면/계정/프로필`
10
10
 
@@ -16,8 +16,7 @@
16
16
  보기와 수정은 같은 route 안에서 바뀌고, 수정 저장은 footer primary 하나, 저장하지 않고 닫으면 버리기 확인을 띄운다.
17
17
  스토리는 `기본`(보기에서 수정·계정 메뉴·게시물 상세까지 직접 조작), `프로필 수정`(수정 화면을 바로 연 상태), `기본 얼굴 고르기`,
18
18
  `불러오는 중`·`비어 있음`·`오류`·`로그인 필요`, `실패와 복구`(다음 저장 실패 → 다시 저장)다.
19
- 2026-10-06 같은 예제를 그리던 `기본 흐름/프로필과 계정`(실험 정리 단계)과 배포 직접 조립 `프로필 편집`을 이 항목으로 합쳤고,
20
- 프로필 편집 고유의 기본 얼굴(사진이 없을 때의 Blobatar) 고르기는 `기본 얼굴 고르기` 스토리와 아래 [기본 얼굴 고르기](#기본-얼굴-고르기) 절로 옮겼다.
19
+ 기본 얼굴(사진이 없을 때의 Blobatar) 고르기는 `기본 얼굴 고르기` 스토리와 아래 [기본 얼굴 고르기](#기본-얼굴-고르기) 절에 있다.
21
20
  사용자 데이터·저장·로그아웃은 제품이 공급하며 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
22
21
 
23
22
  ## 영역 구조
@@ -65,14 +64,14 @@
65
64
  제품은 이 고르기를 수정 화면 사진 영역(또는 사진 고르기 Sheet)에 넣는다.
66
65
 
67
66
  ```text
68
- [◉][◯][◯][◯] ← 얼굴 선택 Button ghost + selected > Avatar 40, wrap gap sm 12
67
+ [◉][◯][◯][◯] ← ImageChoiceGroup selectionMode="single", 타일 72, 사이 spacing.xs 8
69
68
  미리보기 Avatar 64 renderFallback = 고른 얼굴
70
69
  ```
71
70
 
72
71
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
73
72
  | --- | --- | --- | --- |
74
73
  | 바깥 틀 | 수정 화면 본문(EditorScreen) 안 | 사진 영역 | 위 표의 수정 화면을 따른다 |
75
- | 얼굴 줄 | Stack `axis="inline" wrap gap="sm"` > Button `tone="ghost"` `selected` > Avatar(`createBlobatarFallback({ seed })`) | 사진 아래 | 사이 `spacing.sm` 12, 선택지 Avatar 40, 미리보기 64 |
74
+ | 얼굴 줄 | [ImageChoiceGroup](../components/image-choice-group.md) `selectionMode="single"`, 항목 `image` = `createBlobatarFallback({ seed })({ size: imageChoiceRecipe.tileSize, decorative: true })`, 이름은 항목 `label` | 사진 아래 | 타일 `imageChoiceRecipe.tileSize` 72, 사이 `imageChoiceRecipe.gap`(`spacing.xs` 8), 미리보기 64 |
76
75
 
77
76
  ## 버튼과 행동 위치
78
77
 
@@ -80,13 +79,13 @@
80
79
  | --- | --- | --- | --- |
81
80
  | 프로필 수정 | `edit` → Button ghost | 요약 아래 | 1 |
82
81
  | 계정 메뉴 | IconButton ghost(접근성 이름 "계정 메뉴") | 머리 끝(`actions`) | 1. 로그아웃·연결 계정은 메뉴 안 |
83
- | 게시물·저장됨 전환 | Button ghost + `selected` | 게시물 영역 맨 위 | 2. `selected`는 primary로 세지 않는다 |
84
- | 게시물 열기 | 사진 칸 전체 | 격자 | 칸마다 1. 상세는 ScreenLayout + `leading` 뒤로 |
82
+ | 게시물·저장됨 전환 | [SegmentedControl](../components/segmented-control.md)(label "프로필 보기") | 게시물 영역 맨 위 | 2개 항목, 단일 선택. 선택 상태를 ghost Button으로 흉내 내지 않는다 |
83
+ | 게시물 열기 | 사진 칸 전체 | 격자 | 칸마다 1. 상세는 ScreenLayout + `exit` 뒤로 아이콘 |
85
84
  | 로그아웃 | 계정 Sheet `footer` Button ghost → AlertDialog `confirm` | Sheet 아래 | 1. 확인 뒤 실행 |
86
85
  | 저장 | EditorScreen `submit` → Button primary(`pending`이면 `loading`) | 수정 footer 끝 | 1. 바뀐 것이 없거나 이름이 비면 비활성 |
87
- | 닫기 | EditorScreen `cancel` → Button ghost(머리 `leading`) | 수정 머리 앞 | 1. dirty면 `discard` 확인(danger) |
86
+ | 닫기 | EditorScreen `cancel` → 헤더 시작 쪽 × 아이콘(`exit` `close`) | 수정 머리 앞 | 1. dirty면 `discard` 확인(danger) |
88
87
  | 사진 변경 | Button `secondary` `small` → Sheet | 수정 본문 사진 아래 | 1 |
89
- | 기본 얼굴 고르기 | Button ghost + `selected` > Avatar | 사진 영역 | 얼굴 수만큼 |
88
+ | 기본 얼굴 고르기 | ImageChoiceGroup `selectionMode="single"` | 사진 영역 | 얼굴 수만큼, 단일 선택 |
90
89
  | 다시 시도 | `stateAction` Button | 상태 안내 아래 | 오류·로그인 필요일 때 1 |
91
90
 
92
91
  ## 상태
@@ -106,8 +105,6 @@
106
105
 
107
106
  ### 프로필과 게시물 상태 분리
108
107
 
109
- 2026-10-13 전수 점검에서 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
110
-
111
108
  | 상황 | 연결 | 유지·복구 |
112
109
  | --- | --- | --- |
113
110
  | 프로필 조회 전 | 지역화된 기본 title + loading | user가 없을 때 handle·summary를 평가하지 않는다 |
@@ -233,7 +230,7 @@ editing ? <EditorScreen title={t("profile.edit")} submitPlacement="footer" dirty
233
230
  - `createBlobatarFallback`은 seed가 바뀔 때만 다시 만들도록 `useMemo`로 묶는다.
234
231
  - Storybook은 실제 서버·세션·라우터 연동 증거가 아니다. 기본·어두운 테마와 실패와 복구를 각각 확인한다.
235
232
 
236
- 2026-10-13 실제 계정 예제 검수: 실패한 프로필 초안은 유지하고 성공 뒤에만 요약을 갱신한다. 계정 Sheet와 로그아웃 AlertDialog의 returnFocusRef는 계정 메뉴 행동을 가리킨다. 로그아웃을 확정하면 열림 상태도 함께 정리하므로 예제를 다시 열 때 확인창이 되살아나지 않는다. 이는 로컬 예제 상태이며 실제 세션 종료·Native OS 초점/발표 검증과 구분한다.
233
+ 계정 예제 상태: 실패한 프로필 초안은 유지하고 성공 뒤에만 요약을 갱신한다. 계정 Sheet와 로그아웃 AlertDialog의 returnFocusRef는 계정 메뉴 행동을 가리킨다. 로그아웃을 확정하면 열림 상태도 함께 정리하므로 예제를 다시 열 때 확인창이 되살아나지 않는다. 이는 로컬 예제 상태이며 실제 세션 종료·Native OS 초점/발표 검증과 구분한다.
237
234
 
238
235
  ## 프로필 요약
239
236
 
@@ -244,3 +241,10 @@ editing ? <EditorScreen title={t("profile.edit")} submitPlacement="footer" dirty
244
241
  단일 팔로우 카드는 `FollowProfileCard`의 요청/실패 계약을 유지하고 다중 통계를 위한 대체 엔진으로 복제하지 않는다.
245
242
 
246
243
  `headingLevel`은 화면의 제목 계층에 맞춰 선택한다(기본 2). 아바타 72px 원형 프레임과 수치·행동 배치는 HJM이 소유한다.
244
+
245
+ ## 근거
246
+
247
+ - 프로필과 게시물 상태 분리 상황 표: 2026-10-13 전수 점검 — 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
248
+ - 한 항목 구성: 2026-10-06 같은 예제를 그리던 `기본 흐름/프로필과 계정`(실험 정리 단계)과 배포 직접 조립 `프로필 편집`을 합쳤고, 프로필 편집 고유의 기본 얼굴 고르기를 별도 스토리·절로 옮겼다.
249
+ - 계정 예제 상태(초안 유지·returnFocusRef·로그아웃 정리): 2026-10-13 실제 계정 예제 검수.
250
+ - 게시물·저장됨 전환과 기본 얼굴 고르기 컴포넌트: 2026-10-17 화면 검토(S6)에서 지침의 "Button ghost + selected"가 선택 규칙(선택 상태 ghost 버튼 흉내 금지)과 실제 예제(SegmentedControl, ImageChoiceGroup)와 다른 것을 확인해 실제 구현에 맞췄다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 구성 API 지침 참조
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/저장한 항목`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
9
  - 스토리북: `배포/화면/콘텐츠/저장한 항목`
10
10
 
@@ -31,8 +31,8 @@ host: 남은 높이·safe area·키보드
31
31
 
32
32
  | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
33
33
  | --- | --- | --- | --- |
34
- | 새 컬렉션 | Button·ghost(내장, `labels.createCollection`) | 첫 화면 머리 오른쪽 actions 자리 | 하나. 누르면 `onCreateCollection` → 제품 Sheet(footer에 만들기 Button 하나) |
35
- | 뒤로 | Button·ghost(내장, `labels.back`) | 컬렉션·상세 머리 왼쪽 leading 자리 | 하나. 상세 → 격자 → 컬렉션 순으로 `onBack` |
34
+ | 새 컬렉션 | IconButton·ghost(내장, 아이콘 `createCollectionIcon`, 이름 `labels.createCollection`) | 첫 화면 머리 오른쪽 actions 자리 | 하나. 누르면 `onCreateCollection` → 제품 Sheet(footer에 만들기 Button 하나) |
35
+ | 뒤로 | 헤더 ‹ 아이콘(내장 `exit`, 이름 `labels.back`) | 컬렉션·상세 헤더 시작 쪽 | 하나. 상세 → 격자 → 컬렉션 순으로 `onBack` |
36
36
  | 게시물 열기 | 격자 칸(내장 버튼) | 게시물 격자 | 칸마다 하나, `onOpenItem` |
37
37
  | 저장 해제 | IconButton·ghost(제품이 `renderDetail`에 둔다) | 상세 본문 머리 행 오른쪽(왼쪽은 작성자) | 하나. 해제 후 상세를 닫고 격자로 복귀 |
38
38
  | 실행 취소 | Button·secondary·small | `notice`(상태 문구 오른쪽) | 해제 직후 하나 |
@@ -50,8 +50,6 @@ host: 남은 높이·safe area·키보드
50
50
 
51
51
  ### 저장 해제와 목록 복귀
52
52
 
53
- 2026-10-13 전수 점검에서 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
54
-
55
53
  | 상황 | 연결 | 유지·복구 |
56
54
  | --- | --- | --- |
57
55
  | collectionId 없음 | 컬렉션 개요 | null의 전체 게시물 보기와 구분한다 |
@@ -79,7 +77,7 @@ import { SavedItemsScreen } from "@hjmds/react/saved-items";
79
77
  {...(collectionId !== undefined ? { collectionId } : {})}
80
78
  selectedItemId={selectedItemId} labels={labels} state={state}
81
79
  onOpenCollection={openCollection} onOpenItem={openItem} onBack={goBack}
82
- onCreateCollection={openCreateSheet}
80
+ onCreateCollection={openCreateSheet} createCollectionIcon={<AddGlyph />}
83
81
  notice={undoNotice}
84
82
  renderThumbnail={renderThumbnail}
85
83
  renderDetail={(item) => <Stack gap="md">
@@ -101,7 +99,7 @@ import { SavedItemsScreen } from "@hjmds/react-native/saved-items";
101
99
  {...(collectionId !== undefined ? { collectionId } : {})}
102
100
  selectedItemId={selectedItemId} labels={labels} state={state}
103
101
  onOpenCollection={openCollection} onOpenItem={openItem} onBack={goBack}
104
- onCreateCollection={openCreateSheet}
102
+ onCreateCollection={openCreateSheet} createCollectionIcon={<AddGlyph />}
105
103
  notice={undoNotice}
106
104
  renderThumbnail={renderThumbnail}
107
105
  renderDetail={(item) => <Stack gap="md">
@@ -138,6 +136,12 @@ import { SavedItemsScreen } from "@hjmds/react-native/saved-items";
138
136
  - 목록 비어 있음과 조회 실패를 구분하고 재시도 제공.
139
137
  - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
140
138
 
141
- - 새 컬렉션은 Sheet footer에 만들기 하나, 본문은 스크롤한다. Native는 `scrollable keyboardAvoidance`를 사용하고 사진 선택 Button은 `growWithContent`로 이미지 높이를 보존한다. 고정 44 높이에 사진을 넣으면 행이 겹친다(2026-10-06 시뮬레이터 재현).
139
+ - 새 컬렉션은 Sheet footer에 만들기 하나, 본문은 스크롤한다. Native는 `scrollable keyboardAvoidance`를 사용하고 사진 선택 Button은 `growWithContent`로 이미지 높이를 보존한다. 고정 44 높이에 사진을 넣으면 행이 겹친다.
142
140
 
143
141
  미게시(1.18.0 이후): 선택한 컬렉션 또는 전체 게시물이 0개면 SavedItemsScreen이 목록 pane의 empty 상태를 사용해 남은 본문 중앙에 안내한다. 제품의 loading/error/restricted 상태는 우선하며, 컬렉션 개요의 만들기 행동은 유지한다.
142
+
143
+ ## 근거
144
+
145
+ - 저장 해제와 목록 복귀 상황 표: 2026-10-13 전수 점검 — 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
146
+ - 새 컬렉션 IconButton: 2026-10-17 화면 검토 — 지침은 글자 버튼, 예제는 `+` 아이콘으로 달랐다. 머리 도구 행동은 아이콘이라는 [IconButton](../components/icon-button.md) 규칙에 맞춰 글자 버튼 대체 분기를 없앴다.
147
+ - 사진 선택 Button `growWithContent`: 2026-10-06 시뮬레이터에서 고정 44 높이에 사진을 넣어 행이 겹치는 것을 재현했다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 구성 API 지침 참조
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screen-flows.tsx`(`SearchScreen` 두 단계 검색 prop, 2026-10-06 사용자 위임 결정으로 미리보기 로직을 공개 API로 올림), 예제 `showcase/*/search-discovery-preview.tsx`·`showcase/shared/search-discovery.ts`. 2026-10-06 사용자 요청("실험에 있는 검색화면 UI도 개편해. 검색과 필터 있는 앱 서비스 따라해.")에 따라 Material 3 Search·Apple HIG search fields·Baymard·NN/g·eBay Playbook 필터 패턴과 국내 커머스·생활 서비스 모바일 웹의 **구조·상호작용만** 조사해 다시 설계했다(브랜드 색·로고·문구·자산은 가져오지 않았다). 2026-10-06 사용자 승인으로 실험 `공통 화면/검색`을 배포하면서 같은 일을 직접 조립하던 배포 `화면/검색`(내 기록 검색)을 대체했다(Web id `patterns-search` 보존, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
9
  - 스토리북: `배포/화면/검색/검색 결과와 필터`
10
10
 
@@ -12,16 +12,18 @@
12
12
 
13
13
  ## 목적
14
14
 
15
- 입력 중 제안 → 확정 → 결과·필터의 두 단계 검색 화면을 SearchScreen 하나로 구성한다. 단계 전환·확정 경로·섹션 배치·필터 시트 초안·0건 복구·포커스·개수 낭독은 SearchScreen이 소유하고 제품은 데이터·요청·문구·아이콘만 공급한다(2026-10-06 전에는 이 모두가 Storybook 미리보기 코드에만 있었다). 스토리는 상태별 정지 화면(`입력 중`·`결과`·`필터 적용`·`필터 시트`·`불러오는 중`·`결과 없음`·`오류`·`로그인 필요`)과 `실패와 복구`(다음 검색 실패 → 다시 시도)를 보여 주고, `어두운 테마`·`큰 글자`는 필터 적용 화면으로 연다. `기본`(검색 전)에서 검색 전 → 입력 → 확정 → 필터 → 상세 → 돌아오기에서 검색어·조건·정렬·스크롤이 남는 흐름을 직접 조작한다. 2026-10-06 같은 예제를 그리던 `기본 흐름/검색과 필터`(복구 흐름)와 `구성/검색/여러 조건 적용과 초기화`(초안·적용 필터 시트)를 이 항목으로 합쳤다([옛 링크](../../../../../docs/STORYBOOK_NAVIGATION.md#실험-정리와-합친-항목-2026-10-06)). 데이터·제안·권한·문구는 제품이 공급하며 예제의 메모리 검색을 운영 검색으로 취급하지 않는다.
15
+ 입력 중 제안 → 확정 → 결과·필터의 두 단계 검색 화면을 SearchScreen 하나로 구성한다. 단계 전환·확정 경로·섹션 배치·필터 시트 초안·0건 복구·포커스·개수 낭독은 SearchScreen이 소유하고 제품은 데이터·요청·문구·아이콘만 공급한다. 스토리는 상태별 정지 화면(`입력 중`·`결과`·`필터 적용`·`필터 시트`·`불러오는 중`·`결과 없음`·`오류`·`로그인 필요`)과 `실패와 복구`(다음 검색 실패 → 다시 시도)를 보여 주고, `어두운 테마`·`큰 글자`는 필터 적용 화면으로 연다. `기본`(검색 전)에서 검색 전 → 입력 → 확정 → 필터 → 상세 → 돌아오기에서 검색어·조건·정렬·스크롤이 남는 흐름을 직접 조작한다. 데이터·제안·권한·문구는 제품이 공급하며 예제의 메모리 검색을 운영 검색으로 취급하지 않는다.
16
16
  배포의 옛 직접 조립 `내 기록 검색`(검색어를 칠 때마다 바로 거르는 SearchField + 카테고리 SegmentedControl + 결과 List + 상세 Sheet 한 세로 스크롤)도 이 항목으로 대체했다. 확정·제안·필터 시트가 필요 없는 수십 개짜리 내 목록을 그 자리에서 거르는 정도라면 화면 대신 [SearchField](../components/search-field.md)와 [SegmentedControl](../components/segmented-control.md)을 목록 위에 직접 두고, 결과 수는 상태 문구로 알린다.
17
17
 
18
18
  ## 영역 구조
19
19
 
20
+ `titleVisibility="hidden"`이고 별도 `header`·`description`이 없으면 `exit` 아이콘 → 검색창(남은 너비) → `actions`를 `spacing.sm` 간격의 한 줄에 세로 중앙 정렬한다. 이 배치는 SearchScreen이 소유한다. 제목이 보이는 화면과 명시적 host header는 기존 구조를 유지한다. 2026-10-17 화면 나가기 계약 통합으로 자유 `leading` 슬롯 대신 `exit`에 종류·지역화 이름·이동 콜백을 전달한다. Native는 `onPress`, Web은 `onClick`이며 HJM이 아이콘 버튼을 그린다. 이 API는 현재 개발 소스 기준이며 설치 버전의 제공 여부를 확인한다.
21
+
20
22
  좁은 폭(Web < 600, Native) — 결과 + 필터 적용:
21
23
 
22
24
  ```text
23
25
  ┌ safe area(host) ─────────────────────────────┐
24
- │ [←] 검색 │ 머리: leading IconButton + 제목. description 없음
26
+ │ [←] 검색 │ 머리: exit 아이콘 + 제목. description 없음
25
27
  │ [🔍 산책 (×)] │ ┐ 고정(notice): SearchField medium 44, label 숨김(placeholder)
26
28
  │ ↕ spacing.sm 12 │ │
27
29
  │ [☰ 필터 2][✓사진 있음][저장함][기간: 올해 ▾][종류 ▾] →│ ┘ trigger(HJM) + filters(제품), 한 줄 스크롤
@@ -71,7 +73,7 @@ Native · Web < 960: 아래 시트(size large) Web ≥ 960(expanded): 옆
71
73
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
72
74
  | --- | --- | --- | --- |
73
75
  | 바깥 틀 | SearchScreen(ScreenLayout) | route 본문, host가 남은 높이·safe area·키보드 처리 | 폭 최대 `layout.readingMaxWidth` 720, 좌우 `spacing.md` 16. host가 여백을 주면 `contentInset="none"` + 그 여백 이름 `hostGutter`(Container `gutter`, Sheet 안은 `regular`). Native 본문 `scrollProps={{ keyboardDismissMode: "on-drag" }}` |
74
- | 머리 | `title` + `leading`(IconButton 뒤로) | 맨 위 | `description`은 비운다(고정 영역 한 줄 절약) |
76
+ | 머리 | `title` + `exit`(공통 뒤로 아이콘) | 맨 위 | `description`은 비운다(고정 영역 한 줄 절약) |
75
77
  | 검색 입력 | 기본 SearchField(`queryLabel`·`queryLabelVisibility="hidden"`·`queryClearLabel`·`onSubmit`·`searching`) | 머리 아래, 고정 | `medium` 44 |
76
78
  | 필터 칩 줄 | HJM `filterSheet.trigger` Chip + 제품 `filters`(Stack inline gap `xs` > Chip `small`) | 검색 입력 아래, 고정, 결과 단계에만 | 입력과 `spacing.sm` 12, 칩 36(+Native hitSlop 4), 칩 사이 `spacing.xs` 8. `filtersOverflow="scroll"` |
77
79
  | 결과 머리 | `resultSummary`: 개수 Text(muted) + 정렬 Menu(Button ghost small) | 본문 첫 줄 | `justify="between"` wrap, 큰 글자에서 두 줄 |
@@ -116,8 +118,6 @@ Native · Web < 960: 아래 시트(size large) Web ≥ 960(expanded): 옆
116
118
 
117
119
  ### 제안·확정 결과·필터 개수의 요청 경계
118
120
 
119
- 2026-10-13 전수 점검에서 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
120
-
121
121
  | 상황 | 연결 | 유지·복구 |
122
122
  | --- | --- | --- |
123
123
  | 제안 조회 | onSearch(value, signal) | 중단된 응답은 무시하고 최근 검색에는 쓰지 않는다 |
@@ -281,7 +281,7 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
281
281
 
282
282
  ## 함정
283
283
 
284
- - 최근 검색을 `onSearch`에서 저장하면 debounce된 부분 입력("산", "산책")이 따로 쌓인다. 2026-10-06 개편 전 예제의 결함이다.
284
+ - 최근 검색을 `onSearch`에서 저장하면 debounce된 부분 입력("산", "산책")이 따로 쌓인다.
285
285
  - 0건 원인 판정(`resolveSearchEmptyCause`)은 `appliedFilters.items` 개수로 한다. 적용 조건을 `appliedFilters`로 넘기지 않으면 필터 때문인 0건도 검색어 때문으로 보고 칩 줄을 숨긴다.
286
286
  - 정렬을 필터 시트 안에 넣으면 정렬 하나 바꾸는 데 시트를 열고 적용해야 한다. 결과 머리의 Menu로 즉시 적용한다(개편 전 `검색과 필터` 예제의 문제).
287
287
  - Web Chip은 `onClick`을 자기 핸들러로 덮어써서 Menu `trigger`로 쓰면 메뉴가 열리지 않는다. 정렬 트리거는 Button을 쓴다.
@@ -290,7 +290,7 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
290
290
  - 최근 검색 저장소를 `onSearch`에 연결하지 않는다. SearchScreen은 확정만 `onSubmit`으로 보내므로 거기서만 저장한다.
291
291
  - Storybook은 실제 검색 서버·라우터 연동 증거가 아니다. Native 화면은 2026-10-06 개편에서 시뮬레이터로 확인하지 않았다(구조는 렌더 테스트로만 확인).
292
292
  - 시트 안 검색(채팅 도구 고르기 등)은 Sheet `size`를 `medium`·`large`로 주고 `scrollable` 없이 SearchScreen을 넣는다. SearchScreen은 `contentInset="none"`·`hostGutter="regular"`.
293
- 1.13.0 이하 Native는 이 조합에서 화면이 0pt가 되고 칩 줄이 시트 여백에서 잘렸다(2026-10-06 utilverse, [Sheet 함정](../components/sheet.md#함정)).
293
+ 1.13.0 이하 Native는 이 조합에서 화면이 0pt가 되고 칩 줄이 시트 여백에서 잘렸다([Sheet 함정](../components/sheet.md#함정)).
294
294
 
295
295
 
296
296
  ### 하위 목록 로딩 계약
@@ -299,3 +299,11 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
299
299
  사용한다. 위쪽 개수 Skeleton과 실제 결과 행은 화면이 배치하지만, 행의 density·두 줄 높이·슬롯은
300
300
  ListRow가 소유한다. 상태 이름은 첫 로딩 행 한 곳에만 제공한다. 검색 화면 안에서 임의 폭/간격의
301
301
  Skeleton 두 개로 결과 행을 다시 만들지 않는다.
302
+
303
+ ## 근거
304
+
305
+ - 제안·확정 결과·필터 개수의 요청 경계 상황 표: 2026-10-13 전수 점검 — 최초 조회와 변경 요청의 상태를 같은 골격으로 설명하던 부분을 구체화했다.
306
+ - 단계 전환·필터 시트 초안·0건 복구 등을 SearchScreen이 소유: 2026-10-06 전에는 이 모두가 Storybook 미리보기 코드에만 있었다. 같은 날 같은 예제를 그리던 `기본 흐름/검색과 필터`(복구 흐름)와 `구성/검색/여러 조건 적용과 초기화`(초안·적용 필터 시트)를 이 항목으로 합쳤다([옛 링크](../../../../../docs/STORYBOOK_NAVIGATION.md#실험-정리와-합친-항목-2026-10-06)).
307
+ - 제목 숨김 시 `leading`·검색창·`actions` 한 줄 배치: 2026-10-16 선수 검색 화면 검토 — 뒤로가기만 별도 줄에 남아 입력창과 분리됐다.
308
+ - 최근 검색은 `onSubmit`에서만 저장: 2026-10-06 개편 전 예제가 `onSearch`에서 저장해 debounce된 부분 입력이 따로 쌓였다.
309
+ - 시트 안 검색의 `size`·`contentInset` 조합: 2026-10-06 utilverse에서 1.13.0 이하 Native 화면이 0pt가 되고 칩 줄이 잘렸다.