@hjmds/design-contracts 1.27.1 → 1.28.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 (155) hide show
  1. package/dist/activity-chip.d.ts +1 -1
  2. package/dist/activity-chip.js +1 -1
  3. package/dist/activity-chip.js.map +1 -1
  4. package/dist/auth-screen.d.ts +71 -5
  5. package/dist/auth-screen.d.ts.map +1 -1
  6. package/dist/auth-screen.js +60 -2
  7. package/dist/auth-screen.js.map +1 -1
  8. package/dist/base-recipes.d.ts +9 -1
  9. package/dist/base-recipes.d.ts.map +1 -1
  10. package/dist/base-recipes.js +7 -1
  11. package/dist/base-recipes.js.map +1 -1
  12. package/dist/behaviors.d.ts +1 -1
  13. package/dist/breadcrumb.d.ts +1 -1
  14. package/dist/breadcrumb.js +1 -1
  15. package/dist/breadcrumb.js.map +1 -1
  16. package/dist/calendar.d.ts +1 -1
  17. package/dist/calendar.js +2 -2
  18. package/dist/calendar.js.map +1 -1
  19. package/dist/catalog.d.ts +148 -89
  20. package/dist/catalog.d.ts.map +1 -1
  21. package/dist/component-contracts.d.ts +1 -1
  22. package/dist/component-contracts.js +1 -1
  23. package/dist/component-contracts.js.map +1 -1
  24. package/dist/component-recipes.d.ts +105 -60
  25. package/dist/component-recipes.d.ts.map +1 -1
  26. package/dist/component-recipes.js +138 -69
  27. package/dist/component-recipes.js.map +1 -1
  28. package/dist/conversation-row.d.ts +2 -2
  29. package/dist/conversation-row.d.ts.map +1 -1
  30. package/dist/conversation-row.js +6 -4
  31. package/dist/conversation-row.js.map +1 -1
  32. package/dist/counter-badge-recipe.d.ts +1 -1
  33. package/dist/counter-badge-recipe.js +1 -1
  34. package/dist/counter-badge-recipe.js.map +1 -1
  35. package/dist/data-table.d.ts +4 -4
  36. package/dist/data-table.js +4 -4
  37. package/dist/data-table.js.map +1 -1
  38. package/dist/date-picker.d.ts +1 -1
  39. package/dist/design-profile.d.ts.map +1 -1
  40. package/dist/design-profile.js +19 -16
  41. package/dist/design-profile.js.map +1 -1
  42. package/dist/file-picker.d.ts +1 -1
  43. package/dist/file-picker.js +1 -1
  44. package/dist/file-picker.js.map +1 -1
  45. package/dist/foundations.d.ts +18 -14
  46. package/dist/foundations.d.ts.map +1 -1
  47. package/dist/foundations.js +18 -13
  48. package/dist/foundations.js.map +1 -1
  49. package/dist/heading.d.ts +13 -13
  50. package/dist/heading.js.map +1 -1
  51. package/dist/icon-button-recipe.d.ts +23 -0
  52. package/dist/icon-button-recipe.d.ts.map +1 -1
  53. package/dist/icon-button-recipe.js +20 -1
  54. package/dist/icon-button-recipe.js.map +1 -1
  55. package/dist/number-field.d.ts +1 -1
  56. package/dist/otp-field.d.ts +1 -1
  57. package/dist/password-field.d.ts +1 -1
  58. package/dist/result.d.ts +1 -1
  59. package/dist/result.js +2 -2
  60. package/dist/result.js.map +1 -1
  61. package/dist/screen-patterns.d.ts +12 -1
  62. package/dist/screen-patterns.d.ts.map +1 -1
  63. package/dist/screen-patterns.js +12 -1
  64. package/dist/screen-patterns.js.map +1 -1
  65. package/dist/semantic-colors.d.ts +10 -0
  66. package/dist/semantic-colors.d.ts.map +1 -1
  67. package/dist/semantic-colors.js +6 -0
  68. package/dist/semantic-colors.js.map +1 -1
  69. package/dist/sheet.d.ts +7 -4
  70. package/dist/sheet.d.ts.map +1 -1
  71. package/dist/sheet.js +8 -5
  72. package/dist/sheet.js.map +1 -1
  73. package/dist/side-panel.d.ts +1 -1
  74. package/dist/side-panel.js +2 -2
  75. package/dist/side-panel.js.map +1 -1
  76. package/dist/steps.d.ts +1 -1
  77. package/dist/steps.js +1 -1
  78. package/dist/steps.js.map +1 -1
  79. package/dist/tag.d.ts +1 -1
  80. package/dist/tag.js +1 -1
  81. package/dist/tag.js.map +1 -1
  82. package/dist/timeline.d.ts +1 -1
  83. package/dist/timeline.js +1 -1
  84. package/dist/timeline.js.map +1 -1
  85. package/dist/top.d.ts +4 -4
  86. package/dist/top.d.ts.map +1 -1
  87. package/dist/top.js +3 -1
  88. package/dist/top.js.map +1 -1
  89. package/dist/version.d.ts +1 -1
  90. package/dist/version.js +1 -1
  91. package/dist/version.js.map +1 -1
  92. package/docs/activity-chip.md +5 -1
  93. package/docs/auth-screen.md +2 -0
  94. package/docs/design-profile.md +14 -10
  95. package/docs/generated/component-maturity.md +1 -1
  96. package/docs/generated/renderer-evidence.json +9 -5
  97. package/docs/generated/renderer-evidence.md +1 -1
  98. package/docs/generated/showcase-manifest.json +1 -1
  99. package/docs/heading.md +14 -9
  100. package/docs/sheet.md +2 -0
  101. package/docs/top.md +4 -3
  102. package/docs/usage/README.md +1 -1
  103. package/docs/usage/components/auth-screen-layout.md +26 -9
  104. package/docs/usage/components/bottom-cta.md +6 -2
  105. package/docs/usage/components/bottom-navigation.md +5 -1
  106. package/docs/usage/components/breadcrumb.md +5 -1
  107. package/docs/usage/components/button.md +2 -0
  108. package/docs/usage/components/chat-message.md +6 -2
  109. package/docs/usage/components/checkbox-group.md +5 -1
  110. package/docs/usage/components/collapsible.md +1 -0
  111. package/docs/usage/components/comment-thread-row.md +1 -1
  112. package/docs/usage/components/effect-surface.md +2 -2
  113. package/docs/usage/components/heading.md +14 -12
  114. package/docs/usage/components/icon-button.md +5 -0
  115. package/docs/usage/components/inline-edit.md +1 -1
  116. package/docs/usage/components/list-detail-screen.md +1 -1
  117. package/docs/usage/components/list-row.md +23 -11
  118. package/docs/usage/components/list.md +10 -5
  119. package/docs/usage/components/notice.md +5 -1
  120. package/docs/usage/components/notification-item.md +5 -1
  121. package/docs/usage/components/photo-source-sheet.md +1 -1
  122. package/docs/usage/components/repeatable-field-group.md +1 -1
  123. package/docs/usage/components/saved-items-screen.md +1 -1
  124. package/docs/usage/components/screen-layout.md +10 -6
  125. package/docs/usage/components/search-screen.md +2 -2
  126. package/docs/usage/components/section.md +5 -1
  127. package/docs/usage/components/segmented-control.md +1 -1
  128. package/docs/usage/components/sheet.md +17 -13
  129. package/docs/usage/components/skeleton.md +41 -15
  130. package/docs/usage/components/statistic.md +5 -1
  131. package/docs/usage/components/switch.md +3 -2
  132. package/docs/usage/components/table-column-settings.md +1 -1
  133. package/docs/usage/components/tag.md +5 -1
  134. package/docs/usage/components/text.md +6 -4
  135. package/docs/usage/components/timeline.md +5 -1
  136. package/docs/usage/components/toast.md +10 -1
  137. package/docs/usage/components/top-bar.md +5 -1
  138. package/docs/usage/components/top.md +1 -1
  139. package/docs/usage/components/upload-item.md +1 -1
  140. package/docs/usage/compositions/adaptive-content.md +5 -1
  141. package/docs/usage/compositions/compound-controls.md +2 -1
  142. package/docs/usage/compositions/design-profile-comparison.md +1 -1
  143. package/docs/usage/compositions/family-drawer.md +5 -3
  144. package/docs/usage/compositions/input-sheet.md +1 -1
  145. package/docs/usage/compositions/interaction-flow-apply.md +3 -3
  146. package/docs/usage/compositions/photo-source.md +3 -3
  147. package/docs/usage/compositions/selection-motion.md +5 -1
  148. package/docs/usage/compositions/toast-layout.md +4 -4
  149. package/docs/usage/compositions/visual-foundations.md +5 -4
  150. package/docs/usage/screens/common-login.md +9 -7
  151. package/docs/usage/screens/common-search.md +1 -1
  152. package/docs/usage/screens/landing.md +15 -14
  153. package/docs/usage/screens/notification-settings.md +4 -2
  154. package/docs/usage/tokens/typography.md +21 -13
  155. package/package.json +1 -1
@@ -66,6 +66,7 @@ import { Button } from "@hjmds/react-native/actions";
66
66
  | `size` | `small` · `medium` · `large` | `medium` | 높이 36 · 44 · 52 |
67
67
  | `shape` | `rounded` · `pill` | `rounded` | `radius.md` · `full` |
68
68
  | `align` | `center` · `leading` | `center` | `leading`은 꽉 찬 폭의 행 행동 |
69
+ | `edge` | `start` · `end` · `both` | — | 버튼이 놓인 컨테이너 가장자리(논리 방향). `ghost`면 자기 좌우 여백만큼 그 쪽으로 내밀어 라벨·trailing 아이콘이 여백 선에 온다(`small` 12, `medium` 16, `large` 24, `resolveButtonEdgeOverhang`; Web은 투명 테두리 1을 더한다). 틀이 보이는 tone과 `link`는 움직이지 않는다. 규칙과 근거는 [IconButton](icon-button.md#근거) |
69
70
  | `selected` | `boolean` | — | 눌림 상태를 갖는 행동 토글(저장·북마크, 켜 둔 도구). Web `aria-pressed`, Native 접근성 state. 선택지 고르기·설정 켜고 끄기에는 쓰지 않는다 |
70
71
  | `loading` | `boolean` | `false` | 기존 문구·아이콘을 시각적으로 숨기고 중앙 스피너 하나만 표시한다. 같은 children을 유지해 크기를 보존하고 누름을 막는다. 접근성 이름·포커스는 유지한다(Web `aria-disabled`, Native는 `disableWhileLoading`으로만 옛 disabled 동작) |
71
72
  | Web `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void` | — | `disabled`·`loading`·`aria-disabled`이면 부르지 않는다 |
@@ -86,6 +87,7 @@ import { Button } from "@hjmds/react-native/actions";
86
87
  | 크기 | 높이 `small` 36 · `medium` 44 · `large` 52. `small`만 hitSlop 4로 터치 영역 44 | `control.buttonHeight`, `control.buttonHitSlop` |
87
88
  | 간격 | 한 묶음의 버튼 사이: **세로로 쌓으면 `spacing.xs` 8, 가로로 나란하면 `spacing.sm` 12**. 예제·제품의 `Stack`도 같은 값을 쓴다. Dialog·Sheet 행동 영역 좌우·아래 여백 `spacing.lg` 20 | `actionGroupRecipe`, `.hjm-dialog__footer`, Native Sheet footer |
88
89
  | 순서·정렬 | 가로 행동 줄은 **보조 → 주**, 끝 정렬. 세로 순서는 배치 소유자의 계약을 따른다: Dialog는 보조 → 주, Native Sheet는 주 → 보조, 좁은 AlertDialog는 주 → 보조. 본문의 짧은 선택·확정 구성은 해당 구성의 순서다. 목록 행 끝은 `small` + `ghost`/`secondary`, `primary` 금지 | `renderAction` 순서(Native overlays), `.hjm-dialog__footer` |
90
+ | 가장자리 정렬 | 행·섹션 머리 끝의 ghost 글자 버튼(정렬 트리거, 전체 삭제)은 `edge="end"`로 라벨·화살표를 옆 글자와 같은 여백 선(16)에 맞춘다. 2026-10-17 실측: SearchScreen 정렬 화살표 29 → 16 | `base-recipes.ts` `resolveButtonEdgeOverhang`, `styles.css` `[data-edge]` |
89
91
  | 고정·스크롤 | 화면 맨 아래 고정 주 행동은 Button 대신 [BottomCTA](bottom-cta.md)(안전 영역·키보드 처리) | — |
90
92
  | 좁은 폭·큰 글자 | 폭 < 600(`breakpoint.medium`)이면 AlertDialog 행동을 세로로 쌓고 주 행동이 위(DOM은 [취소][확인] 유지). 라벨은 두 줄까지, 큰 글자에서는 상한 해제 | `.hjm-alert-dialog__actions`, [버튼 라벨](../../button-label.md) |
91
93
 
@@ -80,7 +80,7 @@ import { Text } from "@hjmds/react-native/primitives";
80
80
  | `timestampPresentation` | `"always"` · `"swipe"` | `"always"` | `swipe`는 수신은 오른쪽·발신은 왼쪽으로 끄는 동안 반대편 여백에 시각을 보여 주고 놓으면 숨긴다. 이 모드에서는 같은 제스처로 답장을 실행하지 않는다. 시각은 보조기술에서도 읽을 수 있다. |
81
81
  | `bubbleTail` | `boolean` | `true`(미게시 변경, 이전 `false`) | 묶음 첫 말풍선의 위쪽 모서리(수신 시작 쪽·발신 끝 쪽)에 윗변이 이어지는 삼각 꼬리(`tailWidth` 7 × `tailHeight` 10)를 같은 표면으로 그리고, 그 모서리 radius를 `tailCornerRadius` 4로 줄인다. 행 양쪽 `spacing.xs` 8 여백으로 잘림을 막는다 |
82
82
  | `deliveryLabel` | `string` | 없음 | 시각 뒤에 ` · `로 붙는 전송 상태 |
83
- | `avatar` | `ReactNode` | 없음 | 버블 옆 아바타. `chatMessageRecipe.avatarSize` 36 크기로 넘긴다. 묶음 뒤 말풍선에서는 같은 폭을 비워 정렬을 유지한다 |
83
+ | `avatar` | `ReactNode` | 없음 | 버블 옆 아바타. `chatMessageRecipe.avatarSize` 32(`avatarRecipe.sizes.small`) 크기로 넘긴다(Web `size="small"`, Native `size={chatMessageRecipe.avatarSize}`). 묶음 뒤 말풍선에서는 같은 폭을 비워 정렬을 유지한다 |
84
84
  | `reply` | `ReactNode` | 없음 | 인용 노드. `replyLink`가 없으면 버블 맨 위에 그리고 아래 구분선(1)으로 답장 본문과 나눈다 |
85
85
  | `replyQuote` | `{ title, excerpt }` | 없음 | 미게시. 말풍선 맨 위에 HJM이 인용을 그린다: 제목(예: "서연에게 답장", 본문 글자) → 원문 한 줄(흐린 글자, 넘치면 말줄임) → 구분선 1 → 답장 본문. 자유 노드 `reply`보다 우선 사용한다 |
86
86
  | `replyLink` | `{ label, onPress() }` | 없음 | 인용과 같이 주면 말풍선 안 인용을 눌러 원문으로 이동한다(Native는 말풍선의 "원문으로 이동" 접근성 동작도 제공). 이전에는 버블 밖 ghost 버튼이었다 |
@@ -96,7 +96,7 @@ import { Text } from "@hjmds/react-native/primitives";
96
96
 
97
97
  | 항목 | 값 | 근거 |
98
98
  | --- | --- | --- |
99
- | 크기 | 말풍선 열 최대 폭 `screenPatternRecipe.messageMaxWidth` 84%; 말풍선 radius `chatMessageRecipe.bubbleRadius` 14, 안쪽 padding 세로 6·가로 10, 테두리 없음(수신 `surface`, 발신 `surfaceAccent`); 아바타 36; 꼬리 10 | `chatMessageRecipe`, Web `.hjm-chat-message*`, Native `ChatMessage` |
99
+ | 크기 | 말풍선 열 최대 폭 `screenPatternRecipe.messageMaxWidth` 84%; 말풍선 radius `chatMessageRecipe.bubbleRadius` 14, 안쪽 padding 세로 6·가로 10, 테두리 없음(수신 `surface`, 발신 `surfaceAccent`); 아바타 32(`avatarRecipe.sizes.small`); 꼬리 10 | `chatMessageRecipe`, Web `.hjm-chat-message*`, Native `ChatMessage` |
100
100
  | 간격 | 묶음 사이 `groupGap` 12, 같은 묶음 말풍선 사이 `runGap` 6(메시지 행이 위쪽 여백으로 소유하므로 타임라인 목록에는 gap을 주지 않는다); 아바타–버블 `avatarGap` 8; 작성자–버블 `authorGap` 4; 버블–시각 `metaGap` 4; 인용 아래 `spacing.xs` 8 | `chatMessageRecipe`, `resolveChatMessageGroupDisplay` |
101
101
  | 순서·정렬 | incoming 시작 쪽, outgoing 끝 쪽; (첫 말풍선) 작성자 → (replyLink 인용) → 버블 + 아래 끝 옆 시각·전송 상태·actions | 렌더 순서 |
102
102
  | 고정·스크롤 | 자체 고정 없음, 타임라인(ChatScreen 본문)이 스크롤; 반응 메뉴는 Web Popover · Native 전체 화면 `Modal`; 답장 스와이프 중 가로 이동은 ±72로 제한 | Web·Native `ChatMessage` |
@@ -144,3 +144,7 @@ import { Text } from "@hjmds/react-native/primitives";
144
144
  ## 대화 스와이프 복귀 (2026-10-12)
145
145
 
146
146
  번뚝의 좌우 넘김 개선 요청으로 Native의 이동값은 Animated.Value에 전달하고 놓으면 공통 motion.normal(200ms)로 복귀한다. 매 이동마다 메시지 트리를 다시 렌더링하지 않는다. Web도 드래그 중에는 손가락을 따라가고 놓을 때만 200ms 복귀한다. 동작 줄이기는 즉시 복귀한다. 사진 캐러셀처럼 가로 제스처가 있는 메시지는 제품에서 timestampPresentation="always"를 선택해 사진 넘김과 시간 표시가 경쟁하지 않게 한다.
147
+
148
+ ## 근거
149
+
150
+ - 아바타 32: 2026-10-17 정렬 실측. recipe·문서는 36이었지만 Web Avatar 단계(32/40/48/64)에 36이 없어 Web은 36 칸에 32 아바타를, Native는 36 아바타를 그렸다. 카카오톡 참고값 약 34에 가장 가까운 Avatar 단계 `small` 32를 recipe가 직접 읽게 해 두 플랫폼이 같은 값을 쓴다. 작성자 첫 줄과의 위쪽 맞춤(카카오톡 방식)은 유지한다.
@@ -79,7 +79,7 @@ import { CheckboxGroup } from "@hjmds/react-native/inputs";
79
79
  | --- | --- | --- |
80
80
  | 크기 | 폭을 채우는 묶음. 각 행은 [Checkbox](checkbox.md) 규격(최소 높이 `medium` 56 · `small` 44, 표시 24 · 20) | `selectionControlRecipe.sizes`, `.hjm-choice` |
81
81
  | 간격 | 그룹 이름(legend)↔항목 `spacing.xs` 8. 항목 사이 세로 `card` `spacing.xs` 8 · `plain` `spacing.xxs` 4 · `grouped` 0, 가로 `card` `spacing.md` 16 · `plain` `spacing.sm` 12 · `grouped` 0. 설명·오류는 `formSupportContract.gap` `spacing.xs` 8 | `selectionGroupRecipe.orientations`, `.hjm-checkbox-group*` |
82
- | 순서·정렬 | 위→아래 [그룹 이름(semibold)] → [설명] → [항목들] → [오류]. 항목은 시작 쪽 정렬. `grouped`는 항목들을 테두리 1px·`radius.lg` 16 카드 하나에 붙여 담는다 | `selectionGroupRecipe.slots`, `.hjm-checkbox-group[data-presentation="grouped"]` |
82
+ | 순서·정렬 | 위→아래 [그룹 이름(medium 500)] → [설명] → [항목들] → [오류]. 항목은 시작 쪽 정렬. `grouped`는 항목들을 테두리 1px·`radius.lg` 16 카드 하나에 붙여 담는다 | `selectionGroupRecipe.slots`, `.hjm-checkbox-group[data-presentation="grouped"]` |
83
83
  | 고정·스크롤 | 고정 영역이 없다. 목록이 길어도 그룹 안에서 스크롤 영역을 만들지 않는다 | `.hjm-checkbox-group__items` |
84
84
  | 좁은 폭·큰 글자 | `horizontal`은 줄바꿈된다(`flex-wrap: wrap`). 좁은 폭·큰 글자에서는 `vertical`을 쓴다 | `.hjm-checkbox-group[data-orientation="horizontal"]` |
85
85
 
@@ -102,3 +102,7 @@ import { CheckboxGroup } from "@hjmds/react-native/inputs";
102
102
  | 그룹 전체 비활성 | `disabled`(fieldset) | `disabled` |
103
103
  | 필수·읽기 전용 안내 | `aria-required`·`aria-readonly` | `requiredLabel`·`readOnlyLabel`을 행 hint로 읽음 |
104
104
  | 선택 표시 교체 | 없음 | `indicator="none"`, `renderIndicator(item, props)` |
105
+
106
+ ## 근거
107
+
108
+ 2026-10-17 사용자 결정("굵기 좀더 줄여"): 모든 글자 굵기를 한 단계 낮췄다. 큰 숫자·선택 상태 600(큰 제목 단계는 같은 날 폐지), 제목·강조 500, label 500, 본문 400이며 정본은 [타이포그래피](../tokens/typography.md)다.
@@ -124,4 +124,5 @@ Web에서 외부 상태 변경으로 닫힐 때 내부에 있던 초점은 trigg
124
124
 
125
125
  ## 근거
126
126
 
127
+ - `edge` 축을 두지 않는다: 2026-10-17 1.27.2 채택 점검. 포트폴리오 사이트 헤더 끝 메뉴 트리거(402)의 펼침 표시 칸 끝은 화면 끝에서 36이었는데, 트리거 자체는 좌우 padding 0이고 표시 칸 끝이 트리거 끝과 같다(Storybook 측정: 표시 칸 끝 = 트리거 끝 = 루트 끝). 36은 제품이 TopBar(자기 여백 16)를 `Container gutter="regular"`(20) 안에 둔 결과다. IconButton/Button `edge`는 frameless 컨트롤의 안쪽 padding만큼 바깥으로 내미는 규칙인데 이 트리거는 내밀 padding이 없어 `edge`는 아무것도 바꾸지 못한다. 위치는 제품의 겹친 여백에서 고친다.
127
128
  - 2026-10-17 구성 리뷰: 텍스트 표시 ▸가 약 8px로 그려져 트리거가 정적 문구처럼 보였다. 그래서 두 플랫폼 모두 glyph.sm 20 chevron 아이콘으로 바꿨다.
@@ -77,7 +77,7 @@ import { CommentThreadRow } from "@hjmds/react-native/screen-flows";
77
77
  | --- | --- | --- |
78
78
  | 크기 | 아바타 칸 최상위 32·답글 24, 좋아요 수 칸 폭 44, `media` 최대 폭 160·radius `radius.md` | `commentThreadRecipe` |
79
79
  | 간격 | 아바타·본문·좋아요 열 사이 `spacing.xs` 8, 본문 줄 사이 2, 행동 줄 항목 사이 `spacing.sm` 12, 작성자–시각 `spacing.xxs` 4, `media` 위 4 | `commentThreadRecipe.lineGap`, `Stack gap` |
80
- | 순서·정렬 | 아바타 → (작성자·시각 → 본문 → media → 시각/반응 요약/답글 행동) → 좋아요 버튼·`actions` 아래 좋아요 수. 위 정렬 | 양 renderer |
80
+ | 순서·정렬 | 아바타 → (작성자·시각 → 본문 → media → 시각/반응 요약/답글 행동) → 좋아요 버튼·`actions` 아래 좋아요 수. 위 정렬 단, 오른쪽 좋아요·`actions`·더보기 열은 44 터치 영역을 첫 줄(작성자 caption, `bodyText`면 본문 줄)의 가운데에 맞춘다: 열을 (첫 줄 높이 − 44) / 2만큼 올린다(`commentThreadRecipe.actionAnchor`). HJM의 좋아요·더보기가 마지막이면 열 전체가 IconButton 가장자리 오버행(10)만큼 끝으로 나가 아이콘 칸이 행 끝 여백 선(아바타 시작과 같은 선)에 온다. 제품 `actions`가 마지막이면 그 버튼에 `edge="end"`를 준다. 측정 전후(Web 402): 아바타 168 / 작성자 줄 162 / 아이콘 174 → 아이콘 160 = 작성자 줄, 더보기 칸 끝 28 → 16(2026-10-17 실측 감사, [IconButton `edge`](icon-button.md#배치)) | 양 renderer |
81
81
  | 고정·스크롤 | 행은 스크롤하지 않는다. 목록 스크롤·행 간격(`rowGap` 12)·답글 들여쓰기(`replyIndent` 40)는 host가 준다 | `CommentThreadScreen` |
82
82
  | 좁은 폭·큰 글자 | 본문 열이 `min-width: 0`으로 줄며 줄바꿈되고 작성자·행동 줄은 `flexWrap`. 좋아요 열은 줄지 않는다 | `.hjm-comment-row__text`, `flexShrink: 0` |
83
83
 
@@ -85,14 +85,14 @@ import { EffectSurface } from "@hjmds/react-native/effect-surface";
85
85
  | --- | --- | --- |
86
86
  | 크기 | 기본 여백은 없다(`padding="none"`). 크기는 children과 `padding`이 정하고 장식 레이어가 그 영역 전체(`absolute`, inset 0)를 덮는다. 넘치는 장식은 `radius` 모서리 안에서 잘린다(`overflow: hidden`). 안에 둔 Surface와 같은 기본 radius라 둥근 카드 모서리 밖으로 장식이 비치지 않는다. 배경은 테마 `bg`다. 높이를 늘리려면 children 높이, Web `layoutStyle`·Native `style`의 높이로 정한다. 장식 레이어는 배치에 참여하지 않는다 | `react/src/effect-surface.tsx`, `react-native/src/effect-surface.tsx` |
87
87
  | 간격 | 안쪽 여백은 `padding`(예: `lg` 20), 내용 사이 간격은 children 쪽 [Stack](stack.md)으로 준다. raw 요소에 padding을 직접 주지 않는다 | `react/src/effect-surface.tsx` |
88
- | 순서·정렬 | children은 일반 레이아웃이다. [Stack](stack.md)으로 eyebrow → [Heading](heading.md) → 소개 → [Button](button.md)을 쌓는다(Showcase 랜딩 예는 `gap="lg"` 20, Heading `level1`) | `showcase/web/src/patterns/Landing.stories.tsx` |
88
+ | 순서·정렬 | children은 일반 레이아웃이다. [Stack](stack.md)으로 eyebrow → [Heading](heading.md) → 소개 → [Button](button.md)을 쌓는다(Showcase 랜딩 예는 `gap="lg"` 20, Heading `level3` 24·h1) | `showcase/web/src/patterns/Landing.stories.tsx` |
89
89
  | 고정·스크롤 | 화면 위쪽 히어로 하나에만 둔다. 같은 화면에 여러 개를 겹치거나 목록 행마다 두지 않는다 | — |
90
90
  | 좁은 폭·큰 글자 | — | — |
91
91
 
92
92
  ```text
93
93
  ┌────────────────────────────┐ ← EffectSurface(배경 bg + mesh/glow/grain, 터치 없음)
94
94
  │ eyebrow │
95
- │ 큰 제목(Heading level1) │ ← children: 일반 레이아웃(Stack gap lg 20)
95
+ │ 제목(Heading level3, 24) │ ← children: 일반 레이아웃(Stack gap lg 20)
96
96
  │ 소개 문장 │
97
97
  │ [시작하기] primary │
98
98
  └────────────────────────────┘
@@ -10,9 +10,9 @@
10
10
 
11
11
  ## 언제 쓰나
12
12
 
13
- 자리를 모르는 큰 제목 글자 하나가 필요할 때 쓴다. 랜딩 히어로, 결과 화면의 큰 제목,
14
- 카드·표·빈 상태 안의 제목처럼 `Text`의 최대 크기(24px)로는 모자라거나 문서 제목 단계를
15
- 실제로 내야 하는 자리다. 주변 여백은 갖지 않는다. 담는 블록이 정한다.
13
+ 자리를 모르는 제목 글자 하나가 필요할 때 쓴다. 랜딩 대표 제목, 결과 화면의 제목,
14
+ 카드·표·빈 상태 안의 제목처럼 문서 제목 단계를 실제로 내야 하는 자리다. 크기는 `Text`와 같은
15
+ 단계(최대 24px)를 쓴다. 주변 여백은 갖지 않는다. 담는 블록이 정한다.
16
16
 
17
17
  ## 쓰지 않을 때
18
18
 
@@ -34,7 +34,7 @@
34
34
  // Web
35
35
  import { Heading } from "@hjmds/react/heading";
36
36
 
37
- <Heading level="level2">{t("result.title")}</Heading>
37
+ <Heading level="level3" semanticLevel={2}>{t("result.title")}</Heading>
38
38
  ```
39
39
 
40
40
  ```tsx
@@ -48,23 +48,23 @@ import { Heading } from "@hjmds/react-native/heading";
48
48
 
49
49
  | prop | 값 | 기본값 | 설명 |
50
50
  | --- | --- | --- | --- |
51
- | `level`(필수) | `level1` 40px/700 · `level2` 32px/700 · `level3` 24px/600 · `level4` 20px/600 · `level5` 18px/600 | — | recipe 기본은 `level3`이지만 두 renderer 모두 prop을 필수로 받는다 |
51
+ | `level`(필수) | `level3` 24px/500 · `level4` 20px/500 · `level5` 18px/500. `level1`·`level2`는 사용 중단된 이름으로 `level3`과 같은 24px/500이다 | — | recipe 기본은 `level3`이지만 두 renderer 모두 prop을 필수로 받는다. 가장 큰 단계는 24px다 |
52
52
  | `semanticLevel` | 1~6 | `level`의 숫자 | 크게 보이지만 구조상 h4인 카드 제목처럼 시각 크기와 문서 단계가 다르면 둘을 따로 적는다 |
53
53
  | `layoutStyle` | 배치 key(margin·폭·정렬 등) | — | 바깥 배치만. 크기·줄 높이·굵기·색은 받지 않는다 |
54
54
 
55
55
  - 범위 밖 값은 `TypeError`로 거부된다.
56
56
  - 위 크기는 프로필 없는 기본값이다. [디자인 프로필](../../design-profile.md)의
57
- `tokens.heading.level1`~`level5`로 다섯 시각 단계를 지정하면 양 renderer가 따르며,
58
- `semanticLevel`은 그대로다. 크기를 맞추려고 문서 단계를 바꾸지 않는다.
59
- - `defineHjmDesignProfile`에서 `tokens.typography.heading/titleLarge/title`을 바꾸면 기존
60
- level3/4/5 연결을 유지한다. 같은 단계에 `tokens.heading`도 지정하면 명시한 heading
61
- 값이 우선한다. 큰 제목 level1/2는 본문 크기에서 자동 추정하지 않는다.
57
+ `tokens.heading`으로 시각 단계를 지정하면 양 renderer가 따르며, `semanticLevel`은 그대로다.
58
+ 어느 단계도 `typography.heading` 24px를 넘으면 `RangeError`로 거부된다. 크기를 맞추려고 문서 단계를 바꾸지 않는다.
59
+ - `defineHjmDesignProfile`에서 `tokens.typography.heading/titleLarge/title`을 바꾸면
60
+ level1~3/4/5가 그 값을 따른다. 같은 단계에 `tokens.heading`도 지정하면 명시한 heading
61
+ 값이 우선한다.
62
62
 
63
63
  ## 배치
64
64
 
65
65
  | 항목 | 값 | 근거 |
66
66
  | --- | --- | --- |
67
- | 크기 | 줄 높이: `level1` 48 · `level2` 40 · `level3` 32 · `level4` 28 · `level5` 26 | `foundations.ts` `heading`, `typography` |
67
+ | 크기 | 줄 높이: `level3` 32 · `level4` 28 · `level5` 26 (`level1`·`level2`는 32) | `foundations.ts` `heading`, `typography` |
68
68
  | 간격 | Heading은 margin이 0이다(Web `.hjm-heading`). 위아래 간격은 감싸는 Stack·Section의 `gap`이 정한다 | `styles.css` `.hjm-heading` |
69
69
  | 순서·정렬 | 한 블록에 Heading은 하나를 맨 위에 둔다 | — |
70
70
  | 고정·스크롤 | — | — |
@@ -88,7 +88,9 @@ import { Heading } from "@hjmds/react-native/heading";
88
88
 
89
89
  ## 근거
90
90
 
91
- 2026-10-17 사용자 결정: `level1`·`level2`는 Pretendard 800이 큰 크기에서 무겁고 거칠어 700(bold)으로 낮췄다. 같은 날 600으로 낮춘 `level3`~`level5`보다 한 단계 굵게 유지한다. Native는 `fonts="pretendard"`일 때 `Pretendard-Bold` face로 그린다. `Top size="large"` 제목도 `heading.level2`라 함께 700이 된다. 브루탈리즘 프리셋은 level1·2를 800으로 명시한다.
91
+ 2026-10-17 사용자 결정("글자 너무 커", "큰 글자 정책은 없애"): 큰 글자 단계를 폐지했다. Native Storybook 서비스 소개 화면의 히어로 제목(level1 40px)과 구획 제목(level2 32px)이 지나치게 컸던 것이 계기다. `level1`·`level2`는 기존 소비 코드가 깨지지 않도록 이름만 남기고 `level3`과 같은 24/32/500으로 고정했으며 새 코드에서 쓰지 않는다. 위계는 `level3`(24) → `level4`(20) → `level5`(18)로 만들고, 문서 구조는 `semanticLevel`로 적는다. `Top size="large"` 제목도 `heading.level3` 24px다. 브루탈리즘 프리셋은 굵기만 800으로 명시하고 크기는 공통 단계를 따른다.
92
+
93
+ 같은 날 앞선 굵기 결정: 제목 단계(`level3`~`level5`)는 500(medium)이며 Native는 `fonts="pretendard"`일 때 `Pretendard-Medium` face로 그린다. 이전의 큰 제목 600(semibold) 단계는 큰 글자 단계와 함께 없어졌다.
92
94
 
93
95
  ## 함정
94
96
 
@@ -82,6 +82,7 @@ import { NotificationBell } from "@hjmds/react-native/notification-bell";
82
82
  | `size` | `small` 36 · `medium` 44 · `large` 52 | `medium` | `small`은 터치 여백 4 추가. 아이콘 크기는 각각 `sm`·`md`·`lg` |
83
83
  | `shape` | `rounded` · `circle` | `rounded` | — |
84
84
  | `selected` | `true` · `false` | — | 주면 토글이 된다(Web `aria-pressed`, Native 접근성 state) |
85
+ | `edge` | `start` · `end` · `both` | — | 버튼이 놓인 컨테이너 가장자리(논리 방향, RTL에서 뒤집힘). `ghost`면 터치 영역은 그대로 두고 그 쪽으로 (보이는 지름 − 아이콘 칸) / 2만큼 내민다(`medium` 10, `small` 8, `large` 12, `resolveIconButtonEdgeOverhang`). 테두리·배경이 있는 tone은 틀이 정렬 기준이라 움직이지 않는다 |
85
86
  | `loading` | `true` · `false` | — | 누름을 막고 스피너로 바꾼다. Native는 기본으로 포커스를 유지한다(`disableWhileLoading`으로만 옛 동작) |
86
87
  | `loading` 중 `renderLoadingIndicator`(Native) | `(props: { color: string; size: "small" }) => ReactNode` | 기본 스피너 | 스피너를 바꿀 때만 |
87
88
  | Web `onClick` · Native `onPress` | Web `(event: MouseEvent<HTMLButtonElement>) => void` · Native `(event: GestureResponderEvent) => void` | — | Native는 `onLongPress`도 받는다 |
@@ -97,6 +98,7 @@ import { NotificationBell } from "@hjmds/react-native/notification-bell";
97
98
  | --- | --- | --- |
98
99
  | 크기 | 지름: `small` 36 · `medium` 44 · `large` 52. `small`은 hitSlop 4를 더해 터치 영역을 44(`control.minTouchTarget`)에 맞춘다. 아이콘 크기는 `glyph.sm` 20 · `glyph.md` 24 · `glyph.lg` 28이다 | `icon-button-recipe.ts` `iconButtonRecipe` |
99
100
  | 간격 | [TopBar](top-bar.md)가 슬롯 최소 폭 44, 슬롯 안 간격 `spacing.xs`(8), 좌우 여백 `spacing.md`(16)를 준다. NotificationBell 배지가 버튼 밖으로 넘칠 수 있으니 옆 요소와 간격을 둔다 | `component-recipes.ts` `topBarRecipe`, `styles.css` `.hjm-top-bar` |
101
+ | 가장자리 정렬 | 행·헤더·카드의 시작/끝 가장자리에 놓인 ghost IconButton은 `edge`를 준다. 아이콘 칸(`medium` 24)이 컨테이너 안쪽 여백 선(보통 16)에 오고 44 터치 영역은 여백 쪽으로 10 넘어간다. 같은 줄에 여러 버튼이 있으면 가장자리에 닿은 마지막(또는 첫) 버튼에만 준다. HJM이 그리는 화면 나가기(`exit`)와 SavedItemsScreen 새 컬렉션, ListDetailScreen 새로고침, MediaSelectionScreen 추가, UploadItem 취소·다시 시도, InlineEdit·RepeatableFieldGroup·TableColumnSettings 행 끝 도구, SearchScreen 정렬·전체 삭제는 이미 적용돼 있다. 제품이 헤더 `actions`·ListRow `trailing`·Native `trailingAction`에 넣는 끝 버튼은 제품이 `edge="end"`를 준다. Web 포커스 링은 잘리지 않게 버튼 안쪽에 그린다 | `icon-button-recipe.ts` `iconButtonRecipe.edge`·`resolveIconButtonEdgeOverhang`, `react/src/theme.ts`(`--hjm-icon-button-edge-*`), `styles.css` `[data-edge]`, `react-native/src/actions.tsx` |
100
102
  | 순서·정렬 | 화면 맨 위에서는 TopBar의 `leading`(뒤로·닫기)과 `actions`/`trailing`(더보기·공유·알림)에 둔다. 행 끝(목록·카드)에는 `size="small"` 또는 `medium`, `tone="ghost"`를 쓴다. NotificationBell은 IconButton 오른쪽 위 모서리(`end: 0`, `top: 0`)에 [CounterBadge](counter-badge.md) `floating`을 겹친다 | `react-native/src/notification-bell.tsx` |
101
103
  | 고정·스크롤 | — | — |
102
104
  | 좁은 폭·큰 글자 | — | — |
@@ -141,5 +143,8 @@ TopBar
141
143
 
142
144
  ## 근거
143
145
 
146
+ - 가장자리 정렬(`edge`): 2026-10-17 화면 정렬 실측 감사(Web Storybook 402·1280, Native iPhone 17 Pro 3x 캡처). 행·헤더 끝의 44 버튼이
147
+ 아이콘을 여백 선보다 10~15 안쪽에 두었다(댓글 더보기 28 vs 아바타 16, 화면 머리 뒤로 31 vs 16, 업로드 취소 26 vs 16, 정렬 화살표 29 vs 16).
148
+ 컴포넌트마다 오프셋을 따로 두지 않고 이 한 규칙과 계약 값으로 맞췄다. Toast 닫기(같은 날 `toastRecipe.close.inlineOverhang`)와 같은 식이다.
144
149
  - 도구성 행동은 IconButton: 2026-10-17 사용자 지시 — '나의 기록' 헤더의 새로고침·새 기록 글자 버튼.
145
150
  - 선택 표현: 2026-10-10 사용자 요청 — Button과 같은 선택 표현을 쓴다.
@@ -73,7 +73,7 @@ import { InlineEdit } from "@hjmds/react-native/collection-editing";
73
73
  | --- | --- | --- |
74
74
  | 크기 | 기존 TextField·Button 규격 그대로 | 양 renderer |
75
75
  | 간격 | 보기 행·버튼 행 sm, 편집 묶음 `actionGap` sm | collectionEditingRecipe |
76
- | 순서·정렬 | 보기: 값 → 편집(ghost), 세로 중앙. 편집: 입력 → [취소(ghost) · 저장(primary)] → 오류 | 양 renderer |
76
+ | 순서·정렬 | 보기: 값 → 편집(ghost), 세로 중앙. 편집: 입력 → [취소(ghost) · 저장(primary)] → 오류 보기 행 끝 편집 버튼은 `edge="end"`로 아이콘 칸이 묶음 끝에 온다(전에는 10 안쪽, 2026-10-17 실측 감사, [IconButton `edge`](icon-button.md#배치)) | 양 renderer |
77
77
  | 고정·스크롤 | 바깥 목록·화면 host 소유 | 별도 scroller 없음 |
78
78
  | 좁은 폭·큰 글자 | 보기 행과 버튼 행은 `wrap`으로 줄바꿈 | 양 renderer |
79
79
 
@@ -77,7 +77,7 @@ import { ListDetailScreen } from "@hjmds/react-native/screen-flows";
77
77
  | --- | --- | --- |
78
78
  | 크기 | 목록 또는 상세 pane 하나만 표시하고 각 pane이 host 높이 100%(Native `flex: 1`)를 채운다; 각 pane은 ScreenLayout 폭(최대 720) | Web `height: 100%`, Native `Pane` |
79
79
  | 간격 | 각 pane의 ScreenLayout padding `spacing.md` 16; 추가 간격 없음, 목록 행 간격은 `list`(제품·List) 소유 | `ScreenLayout`, `screen-flows.tsx` |
80
- | 순서·정렬 | 목록 pane: 헤더(제목 → 새로고침) → 목록 → footer(더 보기); 상세 pane: 헤더(뒤로 → 제목) → `detail.content` | `ListDetailScreen` 렌더 순서 |
80
+ | 순서·정렬 | 목록 pane: 헤더(제목 → 새로고침) → 목록 → footer(더 보기); 상세 pane: 헤더(뒤로 → 제목) → `detail.content` 아이콘이 있는 새로고침은 헤더 끝에서 `edge="end"`로 그린다. 제품 `actions`는 그 앞에 오므로 `edge`를 주지 않는다(2026-10-17 실측 감사, [IconButton `edge`](icon-button.md#배치)) | `ListDetailScreen` 렌더 순서 |
81
81
  | 고정·스크롤 | 두 pane 모두 헤더 고정·본문 스크롤; 숨긴 목록 pane은 mount를 유지해 스크롤·입력이 남는다(Web `hidden`, Native `display: "none"`) | Web·Native `ListDetailScreen` |
82
82
  | 좁은 폭·큰 글자 | 제목 열 최소 폭 120 × 글자 배율, 모자라면 새로고침 버튼이 다음 줄로 내려간다; 넓은 폭에서도 두 pane을 나란히 두지 않는다 | `screenPatternRecipe.headerMinWidth` |
83
83
 
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-17
8
8
  - 근거: [ListRow](../../list-row.md), recipe `listRowRecipe`(`src/component-recipes.ts`)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/목록 행`
10
10
 
@@ -71,7 +71,7 @@ import { Avatar, ListRow } from "@hjmds/react-native/data-display";
71
71
  | `onClick`(Web) | `(event: MouseEvent<HTMLElement>) => void` | — | 있으면 `<button>` 행 |
72
72
  | `onPress`(Native) | `(event: GestureResponderEvent) => void` | — | 있으면 누를 수 있는 행 |
73
73
  | `loading`·`loadingLabel` | `boolean` · 현지화 문자열 | `false` · — | 같은 슬롯 모양의 자리표시 행. `loadingLabel`이 상태로 읽힌다 |
74
- | `trailingAction`(Native) | ReactNode | — | 행 명령 옆에 따로 그리는 별도 target(IconButton 등). 자기 `onPress`를 가진다 |
74
+ | `trailingAction` | ReactNode | — | 행 명령 옆에 따로 그리는 별도 target(IconButton 등, 끝에 둘 때 `edge="end"`). 자기 `onPress`/`onClick`을 가진다 |
75
75
  | `layoutStyle` | 배치 key만 | — | 행 루트 배치. Native는 슬롯 배치용 `leadingStyle`·`contentStyle`·`titleRowStyle`·`trailingStyle`·`trailingActionStyle`(모두 배치 key만)도 받는다 |
76
76
  | `titleStyle`·`descriptionStyle`(Native) | — | — | 제거된 속성 — 배치는 `layoutStyle`, 제목의 위험색은 `tone="danger"`, 글자는 listRowRecipe가 정한다 |
77
77
 
@@ -84,9 +84,9 @@ Native 원형 leading의 `full`은 Provider token에서 읽지만 고정 원형
84
84
 
85
85
  | 항목 | 값 | 근거 |
86
86
  | --- | --- | --- |
87
- | 크기 | 폭은 부모를 가득 채운다(Web `inline-size: 100%`). leading 프레임은 40×40(`leadingSize`), `circle`이면 `radius.full`. trailing 아이콘은 `glyph.sm` 20. 최소 높이는 아래 density 표 | `design-contracts/src/component-recipes.ts`(`listRowRecipe`), `design-contracts/src/foundations.ts`(`layout.rowHeight`) |
88
- | 간격 | leading·content·trailing 사이 `spacing.sm` 12(`listRowRecipe.gap`). 좌우 여백은 모두 `spacing.xs` 8, 위아래 여백은 아래 density 표. 행에 margin을 주지 않는다 | `design-contracts/src/component-recipes.ts`(`listRowRecipe`), `react/src/styles.css`(`.hjm-list-row`) |
89
- | 순서·정렬 | leading → content(제목·설명) → trailing. Native `trailingAction`은 행 명령 바깥 오른쪽(끝)에 따로 놓이고 끝 여백 `spacing.xs` 8을 갖는다. 행 사이 구분선·바깥 둥근 배경은 [List](list.md)가 그린다 | `react-native/src/data-display.tsx`(ListRow) |
87
+ | 크기 | 행 표면(누름·hover·선택 배경, 포커스 링, 누를 수 있는 영역)은 부모 콘텐츠 폭보다 양쪽으로 `listRowRecipe.surfaceBleed`(= density `paddingHorizontal`, `spacing.xs` 8)씩 넓다(Web `inline-size: calc(100% + 16)` + `margin-inline: -8`, Native `marginHorizontal: -8`). 글자·leading·trailing은 부모 콘텐츠 가장자리에 놓인다. leading 프레임은 40×40(`leadingSize`), `circle`이면 `radius.full`. trailing 아이콘은 `glyph.sm` 20. 최소 높이는 아래 density 표 | `design-contracts/src/component-recipes.ts`(`listRowRecipe`), `design-contracts/src/foundations.ts`(`layout.rowHeight`) |
88
+ | 간격 | leading·content·trailing 사이 `spacing.sm` 12(`listRowRecipe.gap`). 행 안 좌우 여백 8은 표면이 바깥으로 넓어진 만큼이라 글자는 부모 콘텐츠 가장자리(기본 화면이면 여백 선 16)에서 시작한다. 위아래 여백은 아래 density 표. Native의 투명 선택 테두리 1은 이 여백 안에 포함된다(테두리 + padding = 8). 제품은 행에 margin을 주지 않는다 | `design-contracts/src/component-recipes.ts`(`listRowRecipe`), `react/src/styles.css`(`.hjm-list-row`) |
89
+ | 순서·정렬 | leading → content(제목·설명) → trailing. trailing 내용(아이콘·Switch·문구)은 행의 세로 가운데에 둔다(Web `.hjm-list-row__trailing` flex 가운데, Native 행 가운데 정렬 View). `trailingAction`(두 플랫폼)은 행 명령 바깥 끝에 `listRowRecipe.gap` 12 떨어져 놓이고, 명령과 함께 하나의 표면(선택 배경·테두리)을 쓴다. 행동 상자의 끝은 부모 콘텐츠 가장자리에 맞으므로 ghost IconButton에 `edge="end"`를 주면 glyph 칸 끝이 여백 선 16에 온다. 행 사이 구분선·바깥 둥근 배경은 [List](list.md)가 그린다. 행은 leading 여부를 List에 알려(Web 루트 `data-leading`, Native List 항목 context) 자기 아래 `indented` 구분선이 글자 열(leading 있으면 52, 없으면 0)에서 시작하게 한다 | `react-native/src/data-display.tsx`(ListRow), `react/src/styles.css`(`.hjm-list-row__trailing`) |
90
90
  | 고정·스크롤 | — | — |
91
91
  | 좁은 폭·큰 글자 | 긴 제목·설명은 줄바꿈되고(`overflow-wrap: anywhere`) 행 높이가 늘어난다. 큰 글자에서도 자르지 않는다 | `react/src/styles.css`(`.hjm-list-row`) |
92
92
 
@@ -98,10 +98,12 @@ Native 원형 leading의 `full`은 Provider token에서 읽지만 고정 원형
98
98
  | `spacious` | 72 | 84 | `spacing.md` 16 |
99
99
 
100
100
  ```text
101
- ┌───────────────────────────────────────────────┐
102
- │8│[40×40]│12│ 제목(bodyLarge bold) │12│ 값 ›│8│
103
- │ │leading│ │ 설명(body, secondary) │ │trail│ │
104
- └───────────────────────────────────────────────┘
101
+ 화면 끝 ↓ 여백 선 16 ↓ ↓ 여백 선 16
102
+ ┌─8─┬──────────────────────────────────────────┬─8─┐ ← 행 표면(누름·선택 배경), 8씩 바깥으로
103
+ │ │[40×40]│12│ 제목(bodyLarge 500) │12│ 값 ›│ │
104
+ │ │leading│ │ 설명(body, secondary) │ │trail│ │
105
+ └───┴──────────────────────────────────────────┴───┘
106
+ 섹션 제목·결과 개수(16)와 행 글자·leading(16)이 같은 선에서 시작한다
105
107
  최소 높이: comfortable 한 줄 56 · 두 줄 68
106
108
  ```
107
109
 
@@ -110,7 +112,8 @@ Native 원형 leading의 `full`은 Provider token에서 읽지만 고정 원형
110
112
  - Web `href`는 탐색 링크이며 `download` prop은 없다. 다운로드 속성이 필요한 파일은 [Link](link.md)를 사용한다. 큰 미리보기와 다운로드·메뉴 등 독립 행동은 Card로 구성하며 클릭 가능한 행 안에 링크를 중첩하지 않는다. 2026-10-07 파일 사례 대조에서 파일 행의 외형만으로 다운로드 지원을 추론한 대응표를 바로잡았다.
111
113
 
112
114
  - 제목·설명은 i18n 키로 넣는다. leading의 사진·아이콘은 장식으로 두고 의미는 제목이 말한다.
113
- - 행 안에 다른 버튼을 넣지 않는다. Native는 별도 target을 `trailingAction`에 둔다(행 명령 옆에 따로 그린다).
115
+ - 행 안에 다른 버튼을 넣지 않는다. 별도 target은 `trailingAction`에 둔다(행 명령 옆에 따로 그린다).
116
+ - 행은 부모가 좌우로 8 이상의 여백을 가진 곳에 둔다(화면 본문 16, Card·Sheet·SidePanel 본문, `grouped` List). 여백이 0인 상자(예: 여백 없는 분할 패널, 제품의 꽉 찬 div)에 바로 두면 표면이 잘리고 Web은 가로 스크롤이 생긴다. 그 상자에 콘텐츠 여백을 준다. HJM의 스크롤 상자는 이 여유를 스스로 만든다: VirtualList는 바깥으로 8 넓히고 안쪽 8을 두며, 스크롤 Sheet 본문은 시트 여백 끝까지 넓고 여백을 안쪽에 둔다(내부 `listRowScrollRoom`). 콘텐츠 위치는 그대로다. 화면을 바깥 Container 여백 + `contentInset="none"`으로 감싸는 구성은 지원하지 않는다([ScreenLayout](screen-layout.md#꼭-지킬-것)). `ScreenLayout contentInset="body-edge"` 본문은 자기 여백 16을 가진 전체 폭 행(ConversationRow·NotificationRow) 전용이다. ListRow·NotificationItem은 `default` 본문에 둔다(그래야 글자가 여백 선 16에 온다).
114
117
  - 배치는 `layoutStyle`로 한다. 높이·여백·배경을 덮지 않는다. 밀도는 `density`로 바꾼다.
115
118
  - 아바타 프레임을 제품이 다시 그리지 않는다. `leadingShape`를 쓴다.
116
119
 
@@ -121,7 +124,7 @@ Native 원형 leading의 `full`은 Provider token에서 읽지만 고정 원형
121
124
  | 제목·설명 타입 | `ReactNode` | `string` |
122
125
  | 자리표시 행 | `loading` + `loadingLabel` | `loading` + `loadingLabel` |
123
126
  | 링크 행 | `href` | 없음(`onPress`에서 router 호출) |
124
- | 제목 옆 메타·별도 뒤 행동·뒤 문구 | — | `titleMetadata`, `trailingAction`, `trailingText` |
127
+ | 제목 옆 메타·뒤 문구 | — | `titleMetadata`, `trailingText` |
125
128
  | 접근성 이름 조합 | 요소 내용 | `accessibilityLabel` 없으면 제목·`metadataLabel`·설명·`trailingLabel`을 이어 붙임 |
126
129
  | 기본 `density` | provider density가 compact면 `compact` | 항상 `comfortable` |
127
130
 
@@ -142,3 +145,12 @@ Web도 Native와 같이 `trailingAction`을 지원한다. 이 슬롯은 행의 l
142
145
  팔로우·삭제 같은 행동을 링크에 중첩하지 않는다. `renderLink`는 HJM이 만든 anchor props를
143
146
  프레임워크 Link에 전달하는 연결점이다. 배치·제목·앞/뒤 슬롯은 HJM이 계속 만든다.
144
147
  예: `renderLink={props => <Link {...props} href={props.href!} />}`.
148
+
149
+ ## 근거
150
+
151
+ 2026-10-17 사용자 결정("굵기 좀더 줄여"): 모든 글자 굵기를 한 단계 낮췄다. 큰 숫자·선택 상태 600(큰 제목 단계는 같은 날 폐지), 제목·강조 500, label 500, 본문 400이며 정본은 [타이포그래피](../tokens/typography.md)다.
152
+
153
+ - 스크롤 상자의 표면 여유: 2026-10-17 1.27.2 채택 점검. Native 스크롤 Sheet는 여백 20이 ScrollView 바깥에 있어 행 표면이 시트 콘텐츠 끝(21pt)에서 잘렸다(3x 측정: 선택 테두리 위 선 21–381, 옆 선 없음 → 수정 후 옆 선 13·끝 13, 제목 위치 그대로). VirtualList와 같은 규칙이라 내부 helper 하나로 묶었다.
154
+ - trailing 세로 가운데: 2026-10-17 정렬 실측(Web 402). trailing span이 block이라 안의 inline svg가 글자 기준선에 놓여 행 가운데보다 2~2.5px 위였다(설정 334/331.5, 컬렉션 184/181.5). Native는 행 가운데(388/388)였다. trailing을 flex 가운데 상자로 바꾼 뒤 Web도 334/334, 184/184로 측정됐다.
155
+ - 글자를 여백 선에, 표면은 바깥으로: 2026-10-17 사용자 결정. 행이 누름 배경용 좌우 여백 8을 콘텐츠 안에 두어 기본 화면에서 행 글자가 16 + 8 = 24에서 시작했고, 같은 화면의 섹션 제목·검색 결과 머리("결과 N개")·설정 묶음 제목은 16에서 시작했다(Web 402 실측: 검색 결과 행 24 / 머리 16, 설정 leading 24 / 묶음 제목 16, 알림 leading 24 / "오늘" 16. Native 3x 화면 잉크: 결과 행 제목 25 / 개수 16.7 — Native는 투명 선택 테두리 1까지 더해졌다). 표면을 바깥으로 8 넓히는 방식으로 바꾼 뒤 Web 402·1280, LTR·RTL 모두 행 글자·leading 시작과 trailing 끝이 16(1280은 화면 열 기준 296)으로, Native는 결과 행 제목 잉크 16, 설정 leading 아이콘 잉크 36 → 27, 알림 42.3 → 33(프레임 16 + 아이콘 안쪽 여백)으로 측정됐다. 구분선 시작도 글자 위치(52)로 옮겼다.
156
+ - 구분선이 행의 글자 열을 따름: 2026-10-17 사용자 보고(leading 없는 행에서 구분선이 중간에서 잘림). 위 52는 leading 있는 행의 글자 위치일 뿐이라 leading 없는 행(글자 16)에서는 구분선이 68에서 시작했다. 행이 leading 여부를 List에 알리도록 바꿔 구분선 시작이 각 행의 글자 열과 같아졌다([List 근거](list.md#근거)).
@@ -73,7 +73,7 @@ import { Text } from "@hjmds/react-native/primitives";
73
73
  | prop | 값 | 기본값 | 설명 |
74
74
  | --- | --- | --- | --- |
75
75
  | `label` | 현지화 문자열(필수) | — | 목록의 접근성 이름. 비면 `TypeError` |
76
- | `separator` | `indented` · `full` · `none` | `indented` | `indented`는 leading 슬롯 폭만큼 들여씀 |
76
+ | `separator` | `indented` · `full` · `none` | `indented` | `indented`는 위 행의 글자 열에서 시작(leading이 있으면 52, 없으면 0) |
77
77
  | `appearance` | `plain` · `grouped` | `plain` | `grouped`는 배경과 둥근 모서리로 한 덩어리 |
78
78
  | `layoutStyle` | 배치 key만 | — | 목록 루트 배치. Native `style`은 제거된 속성 — `layoutStyle`, 모양은 `appearance`·`separator` |
79
79
  | TaskList `items` | `readonly TaskItem[]` — `{ id, label, completed, description?, disabled? }` | — | `id`·`label`이 비거나 `id`가 중복되거나 `completed`가 boolean이 아니면 `TypeError` |
@@ -92,8 +92,8 @@ Native `grouped` 프레임은 Provider의 `tokens.radius.lg`를 읽는다. plain
92
92
 
93
93
  | 항목 | 값 | 근거 |
94
94
  | --- | --- | --- |
95
- | 크기 | 화면 본문 폭을 채우는 세로 묶음이다. `grouped`는 배경 `--hjm-color-bg`와 모서리 `radius.lg` 16으로 한 덩어리가 된다 | `design-contracts/src/foundations.ts`(`radius`), `react/src/styles.css`(`.hjm-list`) |
96
- | 간격 | 행 사이 간격은 없고 구분선 1px(`--hjm-color-border`)만 들어간다. `indented` 구분선의 시작 들여쓰기: 60(행 좌우 여백 `spacing.xs` 8 + leading 40 + `spacing.sm` 12, 글자 시작과 같은 위치, `listRecipe.separators.indented`, 두 플랫폼. 2026-10-17 전에는 행 여백을 빼서 52였다). 끝 쪽은 둘 다 0. 덩어리 바깥 여백·섹션 사이 간격은 감싸는 Stack이 준다(페이지 좌우 여백은 `layout.pagePadding`, 섹션 간격은 `layout.sectionGap` = `spacing.xl` 24) | `design-contracts/src/component-recipes.ts`(`listRecipe`), `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-list`), `react-native/src/data-display.tsx`(List) |
95
+ | 크기 | 화면 본문 폭을 채우는 세로 묶음이다. `plain`은 자르지 않으므로 행 표면이 부모 여백 쪽으로 8씩 넓어진다(행 글자는 목록 콘텐츠 가장자리). `grouped`는 배경 `--hjm-color-bg`와 모서리 `radius.lg` 16으로 한 덩어리가 되고 그 모양으로 자르므로, 좌우 안쪽 여백 `listRecipe.grouped.paddingInline`(= `listRowRecipe.surfaceBleed` 8)을 두어 행 표면이 잘리지 않고 행 글자는 이전처럼 덩어리 끝에서 8 안쪽에 온다 | `design-contracts/src/component-recipes.ts`(`listRecipe`), `react/src/styles.css`(`.hjm-list`), `react-native/src/data-display.tsx`(List) |
96
+ | 간격 | 행 사이 간격은 없고 구분선 1px(`--hjm-color-border`)만 들어간다. 구분선 위치는 목록 콘텐츠 가장자리(= leading 없는 행의 글자 시작) 기준이고, 각 구분선은 바로 위 행에 속한다(iOS 셀 구분선과 같다). `indented`는 위 행의 글자 열에서 시작한다: leading이 있으면 `leadingInsetStart` 52(leading 40 + `spacing.sm` 12), 없으면 `insetStart` 0. 끝은 행의 trailing 끝과 같은 콘텐츠 가장자리 0이다. `full`은 위 행과 무관하게 시작·끝 모두 −8로 행 표면 폭과 같다(`listRecipe.separators`, 두 플랫폼, Web `--hjm-list-separator-*`). 위 행의 leading 여부는 ListRow가 알린다: Web은 행 루트의 `data-leading`, Native는 List 항목 context. 덩어리 바깥 여백·섹션 사이 간격은 감싸는 Stack이 준다(페이지 좌우 여백은 `layout.pagePadding`, 섹션 간격은 `layout.sectionGap` = `spacing.xl` 24) | `design-contracts/src/component-recipes.ts`(`listRecipe`), `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-list`), `react-native/src/data-display.tsx`(List) |
97
97
  | 순서·정렬 | 다음 페이지 요청은 목록 바로 아래에 [LoadMore](load-more.md)를 둔다 | — |
98
98
  | 고정·스크롤 | List 자체는 스크롤하지 않는다. 화면 스크롤 안에 둔다 | `react-native/src/data-display.tsx`(List) |
99
99
  | 좁은 폭·큰 글자 | — | — |
@@ -101,7 +101,7 @@ Native `grouped` 프레임은 Provider의 `tokens.radius.lg`를 읽는다. plain
101
101
  ```text
102
102
  ┌─ List grouped (radius.lg 16, --hjm-color-bg) ─────────┐
103
103
  │ ListRow │
104
- │ ────────────────────────────────────────── │ ← indented 구분선(시작 들여씀)
104
+ │ ────────────────────────────────────────── │ ← indented 구분선(위 행에 leading이 있으면 글자 열 52, 없으면 0)
105
105
  │ ListRow │
106
106
  │ ────────────────────────────────────────── │
107
107
  │ ListRow │
@@ -189,6 +189,7 @@ import { CollectionRail } from "@hjmds/react/collection-rail";
189
189
 
190
190
  - 자식 하나가 행 하나다. 자식마다 안정된 `key`를 준다(없으면 순서 index로 key를 만든다).
191
191
  - 구분선을 행에 직접 그리지 않는다. `separator`로 정한다.
192
+ - `plain` 목록은 좌우 여백이 8 이상인 부모(화면 본문, Card·Sheet 본문 등) 안에 둔다. 여백 0인 상자에 두면 행 표면이 잘리거나 Web에 가로 스크롤이 생긴다.
192
193
  - TaskList는 체크해도 순서를 바꾸거나 지우지 않는다. 저장·실패·되돌리기는 제품이 `onCompletedChange`에서 처리한다.
193
194
 
194
195
  ## 플랫폼 차이
@@ -196,9 +197,13 @@ import { CollectionRail } from "@hjmds/react/collection-rail";
196
197
  | 항목 | Web | Native |
197
198
  | --- | --- | --- |
198
199
  | 구조 | `role="list"` + 자식마다 `role="listitem"` | `accessibilityRole="list"` |
199
- | 구분선 | CSS(`data-separator`) | 행 사이 1px `View` |
200
+ | 구분선 | CSS(`data-separator`, 다음 항목 `::before`, 위 행 `data-leading`은 `:has()`로 읽음) | 행 사이 1px `View`(위 행 ListRow가 context로 leading 여부를 알림) |
200
201
  | 배치 | `layoutStyle`(+ `className`) | `layoutStyle`(`style`은 제거된 속성) |
201
202
 
202
203
  2026-10-07 Utilverse 항목 삭제 채택을 위해 `renderItemAction`을 추가했다. 체크와 삭제의 초점·누름을 분리하고 큰 글자 라벨 폭을 보존하도록 행동을 다음 줄에 둔다. `renderCollection`의 `renderItem`에도 포함된다. 삭제 저장·실패·되돌리기는 제품이 처리한다.
203
204
 
204
205
  항목 삭제 뒤에는 제품이 다음 항목의 독립 행동(없으면 이전 항목, 목록이 비면 후속 행동)으로 초점을 복구한다. Web Showcase는 ref와 커밋 후 focus 예시를 제공한다. Native 접근성 초점 검증은 아직 남아 있다.
206
+
207
+ ## 근거
208
+ - 구분선 시작은 위 행의 글자 열: 2026-10-17 사용자 보고(leading 없는 행에서 구분선이 중간에서 잘린 것처럼 보임). 고정 시작 52가 leading 없는 행에도 적용돼 Web 402 대시보드 기록 목록에서 행 글자 16 / 구분선 68~394(콘텐츠 끝 386보다 8 더 나감)였다. 위 행의 leading 여부로 시작을 정하고 끝을 콘텐츠 가장자리로 맞춘 뒤 글자 16 / 구분선 16~386, leading 있는 목록은 글자 85 / 구분선 85~369(콘텐츠 끝 369), RTL도 대칭으로 측정됐다.
209
+ - 구분선·grouped 여백: 2026-10-17 사용자 결정(행 글자는 여백 선 16, 표면만 바깥으로 8). 행 콘텐츠 상자가 목록 콘텐츠 가장자리로 옮겨져 `indented` 시작이 60 → 52가 되어도 화면에서 보이는 위치(글자 시작)는 같다. 기본 화면에서 행 글자가 24, 섹션 제목이 16이던 실측은 [ListRow 근거](list-row.md#근거)에 있다. 이전 plain 목록의 `overflow: hidden`은 grouped 모서리만을 위한 것이라 grouped로 옮겼다.
@@ -88,7 +88,7 @@ import { Notice } from "@hjmds/react-native/feedback";
88
88
  | --- | --- | --- |
89
89
  | 크기 | 본문 폭을 꽉 채운다. 아이콘 20(`glyph.sm`), 테두리 1(`stroke.default`), radius `radius.md` 12 | `noticeRecipe` |
90
90
  | 간격 | 안쪽 `spacing.md` 16, 아이콘·문구·행동 사이 `spacing.sm` 12, 제목·설명 사이 `spacing.xxs` 4. 이웃 블록과는 `layout.contentGap` 16 | `noticeRecipe`, `.hjm-notice*` |
91
- | 순서·정렬 | 아이콘 — 제목·설명 — 행동. 행동은 하나, `Button` `size="small"` `tone="secondary"`(또는 `ghost`), `primary` 금지. Web은 행동이 같은 줄 끝, Native는 행동이 문구 줄 **아래** | `.hjm-notice__action`, `react-native/src/feedback.tsx` |
91
+ | 순서·정렬 | 아이콘 — 제목·설명 — 행동. 행동은 하나, `Button` `size="small"` `tone="secondary"`(또는 `ghost`), `primary` 금지. Web은 행동이 같은 줄 끝, Native는 행동이 문구 줄 **아래**. 아이콘·제목 첫 줄·(Web) 행동은 한 첫 줄 기준에 가운데를 맞춘다. 기준 높이는 행동이 없으면 제목 줄 높이(`1lh`), Web 행동이 있으면 small 버튼 높이 36(`control.buttonHeight.small`)이고 추가 줄은 아래로 늘어난다. Native 아이콘 칸은 제목 첫 줄 높이(토큰×글자 배율) | `.hjm-notice`·`.hjm-notice__icon`·`.hjm-notice__action`, `react-native/src/feedback.tsx` |
92
92
  | 고정·스크롤 | 본문 흐름 안. 먼저 읽어야 하는 경고·입력 오류는 영향을 받는 내용 바로 위(화면 상태는 제목 아래, 폼 검증은 제출 버튼 위). 제출 후 성공·실패 결과는 구성 지침이 지정한 행동 인접 위치에 둔다. 떠 있거나 고정되지 않고 함께 스크롤되며 여러 개 쌓지 않는다 | — |
93
93
  | 좁은 폭·큰 글자 | Web은 문구 칸이 60% 아래로 줄면 행동이 다음 줄로 감긴다. 제목·설명은 줄바꿈된다 | `.hjm-notice__content`(`flex: 1 1 60%`) |
94
94
 
@@ -113,3 +113,7 @@ import { Notice } from "@hjmds/react-native/feedback";
113
113
  - Web은 반대로 항상 발표되므로, 화면 진입 때부터 늘 있는 안내를 Notice로 두면 매번 읽힌다. 그런 조건 안내는 BottomInfo다.
114
114
 
115
115
  - 2026-10-06 독립 재구현에서 입력 전 경고 위치와 시간 선택의 확정 후 결과 위치가 충돌했다. 시간 선택의 결과는 행동 아래, 입력 시트의 저장 실패는 본문 입력 위로 각 구성에 명시한다. Notice 자체가 결과 위치를 강제하지 않는다.
116
+
117
+ ## 근거
118
+
119
+ - 첫 줄 기준: 2026-10-17 정렬 실측(Web 402·1280, 검색 오류 Notice). 행 끝 행동 버튼 가운데 195, 제목 첫 줄 가운데 187로 8px 어긋났고 Web 아이콘 칸에는 규칙이 없었다. Toast의 첫 줄 기준(c053d956)과 같은 방식으로 맞춘 뒤 아이콘·제목 첫 줄·행동 모두 195로 측정됐다.
@@ -95,4 +95,8 @@ import { NotificationItem } from "@hjmds/react-native/screens";
95
95
  | 상태·시각 줄 | 설명 아래 caption `Text` | `description`과 줄바꿈(`\n`)으로 합친 한 문자열 |
96
96
  | 이동 | `href`, `onClick` | `onPress` |
97
97
  | 추가 슬롯 | `className`, `layoutStyle` | `titleMetadata`, `trailingAction`, `trailingText`, `layoutStyle` |
98
- | 제목 굵기 조정 | `Text emphasis` | 렌더러 비공개 입력(`hjmTitleEmphasis`)으로 안 읽음 700(`fontWeight.bold`)·읽음 400(`fontWeight.regular`). 제거된 `ListRow.titleStyle`을 사용하지 않는다. `NotificationItemProps`에는 `titleStyle`이 없다(1.13.0 변경) |
98
+ | 제목 굵기 조정 | `Text emphasis` | 렌더러 비공개 입력(`hjmTitleEmphasis`)으로 안 읽음 `strong` 500(제목 굵기)·읽음 `regular` 400. 제거된 `ListRow.titleStyle`을 사용하지 않는다. `NotificationItemProps`에는 `titleStyle`이 없다(1.13.0 변경) |
99
+
100
+ ## 근거
101
+
102
+ 2026-10-17 사용자 결정("굵기 좀더 줄여"): 모든 글자 굵기를 한 단계 낮췄다. 큰 숫자·선택 상태 600(큰 제목 단계는 같은 날 폐지), 제목·강조 500, label 500, 본문 400이며 정본은 [타이포그래피](../tokens/typography.md)다.
@@ -95,7 +95,7 @@ import { PhotoSourceSheet } from "@hjmds/react-native/screen-flows";
95
95
  | --- | --- | --- |
96
96
  | 크기 | Sheet 기본 `placement="bottom"`·`size="auto"`(내용 높이, 화면 높이 0.9 상한), Web 최대 폭 640; 선택 버튼은 `tone="secondary"` `Button` 두 개 | `sheetRecipe`, `screen-flows.tsx` |
97
97
  | 간격 | 시트 안 여백은 Sheet 소유(두 플랫폼 좌우 `spacing.lg` 20 · 위아래 `spacing.sm` 12 + 하단 safe area); 두 버튼 사이 `spacing.xs` 8(세로로 쌓은 버튼, `actionGroupRecipe.stackedGap`); 바깥에 padding을 더하지 않는다 | Web `.hjm-sheet__body`, Native `sheetRecipe.content`, `Stack gap="xs"` |
98
- | 순서·정렬 | Sheet 제목·닫기 → 앨범 → 촬영(`cameraAvailable`일 때만) | Web `screen-flows.tsx` 59–60행, Native 50–51행 |
98
+ | 순서·정렬 | Sheet 손잡이 → 앨범 → 촬영(`cameraAvailable`일 때만). `labels.title`은 보이지 않는 시트 이름(2026-10-17 사용자 결정: 시트는 보이는 제목 없이 `title`을 접근성 이름으로만 쓴다) | Web `screen-flows.tsx` 59–60행, Native 50–51행 |
99
99
  | 고정·스크롤 | 화면 하단에 뜨는 오버레이(배경 막) — 선택 뒤 picker 호출 시점은 플랫폼 차이 참고 | `Sheet` |
100
100
  | 좁은 폭·큰 글자 | 버튼 문구는 줄바꿈되고 Sheet 높이가 늘어난다(0.9 상한을 넘으면 본문 스크롤); 카메라가 없으면 버튼 하나만 남는다 | `sheetRecipe.content.maxHeightRatio` |
101
101
 
@@ -76,7 +76,7 @@ import { TextField } from "@hjmds/react-native/inputs";
76
76
  | --- | --- | --- |
77
77
  | 크기 | 입력은 제품이 넣은 컨트롤, 버튼은 Button 기본 | 양 renderer |
78
78
  | 간격 | 묶음과 추가 버튼 사이 `collectionEditingRecipe.gap`=`md`(16), 행 안 입력–삭제 `sm`(12) | `collectionEditingRecipe`, 양 renderer |
79
- | 순서·정렬 | FieldGroup 제목 → 행마다 입력 → [삭제](ghost) → … → 마지막 [추가](secondary) | 렌더 순서 |
79
+ | 순서·정렬 | FieldGroup 제목 → 행마다 입력 → 삭제 IconButton(ghost, 행 이름을 담은 `label`) → … → 마지막 추가 Button(secondary). 행 끝 삭제는 `edge="end"`로 아이콘 칸이 묶음 끝에 온다(전에는 10 안쪽, 2026-10-17 실측 감사, [IconButton `edge`](icon-button.md#배치)) | 렌더 순서 |
80
80
  | 고정·스크롤 | 자체 고정·스크롤 없음, 바깥 폼·화면 host 소유 | 별도 scroller 없음 |
81
81
  | 좁은 폭·큰 글자 | 행은 세로로 쌓여 폭이 좁아도 입력과 삭제가 겹치지 않는다 | 세로 Stack |
82
82
 
@@ -100,7 +100,7 @@ Native collection cover의 모서리는 Provider의 `tokens.radius.md`다. 저
100
100
  | --- | --- | --- |
101
101
  | 크기 | host의 남은 높이 100%, ScreenLayout 폭(최대 720); 컬렉션 표지·게시물 썸네일 1:1, 표지 radius `radius.md` | `ListDetailScreen`, Web `.hjm-saved-*`, Native `saved-items.tsx` |
102
102
  | 간격 | 화면 padding `spacing.md` 16; 개인 저장 안내–격자 `spacing.md` 16; 컬렉션 2열 간격 `spacing.md` 16; 표지 4장 사이·게시물 3열 간격 `spacing.xxs` 4; 표지–컬렉션 제목 `spacing.sm` 12 | `Stack gap="md"`, `Grid gap`, Web `.hjm-saved-collection`, Native `gap: spacing.sm` |
103
- | 순서·정렬 | 홈: 헤더(제목 → 새 컬렉션) → 개인 저장 안내 → 모든 게시물 → 사용자 컬렉션; 컬렉션: 헤더(뒤로 → 컬렉션 제목) → 사진 격자 또는 빈 안내; 상세: 헤더(뒤로 → 항목 제목) → `renderDetail` | `SavedItemsScreen` 렌더 순서 |
103
+ | 순서·정렬 | 홈: 헤더(제목 → 새 컬렉션) → 개인 저장 안내 → 모든 게시물 → 사용자 컬렉션; 컬렉션: 헤더(뒤로 → 컬렉션 제목) → 사진 격자 또는 빈 안내; 상세: 헤더(뒤로 → 항목 제목) → `renderDetail` 새 컬렉션 버튼은 `edge="end"`로 아이콘 칸이 헤더 끝 여백 16에 온다(26 → 16, 2026-10-17 실측 감사, [IconButton `edge`](icon-button.md#배치)) | `SavedItemsScreen` 렌더 순서 |
104
104
  | 고정·스크롤 | 헤더 고정, 본문 스크롤; 상세 동안 격자 mount 유지 | ListDetailScreen·ScreenLayout |
105
105
  | 좁은 폭·큰 글자 | 좁은 폭(320 창, 본문 288)에서도 컬렉션 2열·사진 3열을 유지한다. 열 수는 사용 가능 폭이 컬렉션 176 미만(최소 열 폭 80), 사진 140 미만(최소 열 폭 44)일 때만 줄어든다; 컬렉션 제목은 줄바꿈; 사진 속 글자를 유일한 이름으로 쓰지 않는다(버튼 접근성 이름은 `title`) | `Grid minColumnWidth`, `grid.ts` 열 계산 |
106
106
 
@@ -82,9 +82,10 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
82
82
  | `scroll` | `"screen"` · `"content"` | `"screen"` | `content`는 본문 자식(가상화 목록)이 스크롤을 소유한다. 상태 교체 중에는 `screen`으로 돌아간다 |
83
83
  | `contentAlignment` | `"start"` · `"center"` | `"start"` | ready 본문을 사용 영역 중앙에 배치. 긴 본문은 위가 잘리지 않게 스크롤한다. 로그인/제품 안내를 가짜 query state로 바꾸지 않는다(1.24.0) |
84
84
  | `surface` | `"default"` · `"transparent"` | `"default"` | 투명은 제품 host의 배경·wallpaper를 보존. 배경색 style 우회 대신 사용(1.24.0) |
85
- | `contentInset` | `"default"` · `"none"` · `"body-edge"` | `"default"` | `none`은 헤더·notice·본문·footer padding을 0으로(Native는 footer 위 테두리도 뺀다). host가 모든 여백을 이미 줄 때 쓴다. `body-edge`는 헤더·notice·footer 여백(16)과 footer 테두리를 유지하고 **본문 좌우 padding만 0**으로 한다. 자기 좌우 여백을 가진 전체 폭 목록 행(ConversationRow·NotificationRow 등 행 padding 16)을 본문에 바로 둘 때 쓴다(미게시). 값 목록은 계약 `ScreenContentInset`·`screenContentInsets` |
85
+ | `contentInset` | `"default"` · `"none"` · `"body-edge"` | `"default"` | `none`은 헤더·notice·본문·footer padding을 0으로(Native는 footer 위 테두리도 뺀다). host가 모든 여백을 이미 줄 때 쓴다. `body-edge`는 헤더·notice·footer 여백(16)과 footer 테두리를 유지하고 **본문 좌우 padding만 0**으로 한다. 자기 좌우 여백을 가진 전체 폭 목록 행(ConversationRow·NotificationRow 등 행 padding 16)을 본문에 바로 둘 때 쓴다(미게시). ListRow·NotificationItem은 `default`에 둔다. 값 목록은 계약 `ScreenContentInset`·`screenContentInsets` |
86
86
  | `presentation` | `"dashboard"` · `"editorial"` · `"landscape"` | 가장 가까운 profile.screens.overview / 없으면 기존 골격 | [디자인 프로필](../../design-profile.md)의 제공 범위를 따른다. 명시값이 프로필을 이긴다. 헤더 배치·읽기 폭만 바꾸고 상태·입력·행동 슬롯은 유지한다 |
87
87
  | `as` (Web) | `"main"` · `"section"` | `"main"` | 제품 셸에 이미 `<main>`이 있으면 `section` |
88
+ | `safeAreaBottom` (Native) | 0 이상의 유한수 | `0`(미게시) | 화면이 닿는 창 아래 가장자리의 safe area(`useSafeAreaInsets().bottom`, 탭바 위·이미 아래를 들인 host 안이면 0). 마지막 영역(footer, 없으면 본문)이 아래 여백을 `max(16, inset)`으로 한 번만 준다. 넘길 때는 host의 아래 SafeAreaView edge를 빼고, 둘을 같이 쓰지 않는다. 음수·무한대는 `RangeError` |
88
89
  | `layoutStyle` | `HjmCompositionStyleProp` | 없음 | 화면 루트 배치 전용(예: 분할 화면의 `flex`·`width`). ScreenLayout 위에 만든 화면은 모두 이 prop을 루트까지 넘긴다 |
89
90
  | `className` (Web) | `string` | 없음 | 식별·배치 보조. 색·여백을 덮지 않는다 |
90
91
  | `testID` · `scrollRef` · `scrollProps` (Native) | `string` · `Ref<ScrollView>` · `refreshControl`·`keyboardDismissMode`·스크롤 막대 표시·`onScroll`·`onScrollBeginDrag`·`onScrollEndDrag`·`scrollEventThrottle`·`alwaysBounceVertical`·`automaticallyAdjustKeyboardInsets` | 없음 | ScrollView가 있을 때(`scroll="screen"` 또는 상태 교체 중)만 `scrollRef`·`scrollProps`가 연결된다 |
@@ -96,9 +97,9 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
96
97
  | 크기 | 폭 100%, 최대 `screenPatternRecipe.maxWidth`(`layout.readingMaxWidth` 720), 가운데 정렬; 높이는 host가 준 남은 높이(Web `block-size: 100%`, Native `flex: 1`) | Web `.hjm-screen`, Native `ScreenLayout` |
97
98
  | 간격 | 바깥 padding `screenPatternRecipe.padding`(`spacing.md` 16)을 헤더 사방·notice 좌우·본문 사방·footer 사방에 준다(`contentInset="none"`이면 0, `"body-edge"`면 본문 좌우만 0); 헤더 안 `exit`·제목·`actions` 간격 `itemGap`(`spacing.sm`) 12(두 플랫폼, `contentInset="none"`이어도 유지); 상태 안내는 위아래 `sectionGap`(`spacing.xl`) 24, 안내–`stateAction` `stateGap`(`spacing.md`) 16 | Web `src/screens.tsx`(`--hjm-screen-item-gap`·`--hjm-screen-state-gap`)·`.hjm-screen__*`, Native `ScreenLayout` |
98
99
  | 제목 | 기본 가운데 `Heading` `screenHeaderRecipe.centeredTitleLevel`(level5), `headerAlignment="start"`면 `titleLevel`(level3). 추가 기능 버튼(`actions`)은 끝(오른쪽) 위, 나가기(`exit`)는 시작(왼쪽) 위. 헤더 높이는 두 플랫폼 모두 `screenHeaderRecipe`의 고정 띠 | `screenHeaderRecipe` |
99
- | 순서·정렬 | 헤더(`exit` → 제목·설명 → `actions`) → notice → 본문 → footer; 상태 안내는 본문 세로 가운데 | 렌더 순서 |
100
- | 고정·스크롤 | 헤더·notice·footer 고정, `scroll="screen"`은 본문이 스크롤, `"content"`는 자식이 스크롤; footer 위 테두리 1, Web은 하단 safe area만큼 padding을 늘린다 | Web `data-scroll`·`.hjm-screen__footer`, Native `ScrollView` |
101
- | 좁은 폭·큰 글자 | 제목 열 최소 폭 `headerMinWidth` 120 × 글자 배율, 모자라면 `actions`가 다음 줄로 내려간다; 제목은 줄바꿈(`overflow-wrap: anywhere`)되고 자르지 않는다; Native safe area·탭바 inset은 host | `screenPatternRecipe.headerMinWidth`, Web `.hjm-screen__heading` |
100
+ | 순서·정렬 | 헤더(`exit` → 제목·설명 → `actions`) → notice → 본문 → footer; 상태 안내는 본문 세로 가운데 `exit`는 HJM이 `edge="start"`로 그려 아이콘 칸이 헤더 여백 선 16에 온다(Web·Native 31 → 16, 가운데 헤더의 같은 폭 양쪽 열·제목 중앙은 그대로). 끝 `actions`의 마지막 ghost IconButton은 제품이 `edge="end"`를 준다(2026-10-17 실측 감사, [IconButton `edge`](icon-button.md#배치)) | 렌더 순서 |
101
+ | 고정·스크롤 | 헤더·notice·footer 고정, `scroll="screen"`은 본문이 스크롤, `"content"`는 자식이 스크롤; footer 위 테두리 1. 마지막 영역(footer, 없으면 본문)의 아래 여백은 `max(16, 하단 inset)`이다 — Web은 `env(safe-area-inset-bottom)`, Native는 `safeAreaBottom`. 16 + inset으로 더하지 않으며, 키보드가 열려 있으면 키보드가 홈 인디케이터를 덮으므로 16만 둔다 | Web `data-scroll`·`.hjm-screen__footer`, Native `ScrollView` |
102
+ | 좁은 폭·큰 글자 | 제목 열 최소 폭 `headerMinWidth` 120 × 글자 배율, 모자라면 `actions`가 다음 줄로 내려간다; 제목은 줄바꿈(`overflow-wrap: anywhere`)되고 자르지 않는다; Native 위·옆 safe area·탭바 inset은 host, 아래 inset은 `safeAreaBottom` | `screenPatternRecipe.headerMinWidth`, Web `.hjm-screen__heading` |
102
103
 
103
104
  ## 꼭 지킬 것
104
105
 
@@ -109,11 +110,12 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
109
110
 
110
111
  - Web의 화면 스크롤 본문은 Tab으로 진입할 수 있다. `scroll="content"`의 정상 본문은 자식이 키보드 스크롤을 소유하지만, 오류·빈 상태 등 대체 안내는 화면 스크롤로 바뀌며 Tab 진입도 함께 복원된다.
111
112
  - Web에서 본문에 초점을 가진 입력·재시도 버튼이 상태 전환으로 제거되면 같은 본문으로 초점을 복구한다. `scroll="content"`의 본문은 이 복구만을 위한 `tabIndex=-1`을 가지며 일반 Tab 순서에는 추가되지 않는다. 사용자가 다른 컨트롤이나 바깥 영역으로 초점을 옮긴 경우에는 이동시키지 않는다. 2026-10-13 재시도·권한 제한 전환에서 초점이 body로 유실된 경로를 수정했다.
113
+ - **화면 여백은 ScreenLayout이 소유한다(기본 `contentInset`).** 화면을 바깥 `Container gutter`로 감싸고 `contentInset="none"`을 주는 구성은 지원하지 않는다. 행의 표면 bleed, footer·BottomCTA 바, 끝 컨트롤의 `edge`는 모두 ScreenLayout이 여백을 가진다는 전제로 맞춰져 있어, 그 구성에서는 행 표면이 본문 끝에서 잘리고 footer가 여백 안에 떠 있는 상자가 된다. `none`은 host가 모든 inset을 소유하는 기존 의미 그대로다(새 동작 없음).
112
114
  - 가상화 목록(FlatList, VirtualList)을 넣으면 `scroll="content"`로 둔다. 스크롤을 이중으로 중첩하지 않는다.
113
115
  - 새로고침 실패·저장 오류는 `state`를 바꾸지 말고 `ready` + `notice`로 알린다. `loading`으로 바꾸면 본문이
114
116
  unmount되어 입력 초안이 사라진다.
115
117
  - `state.title`·문구는 i18n 키로 넣는다. ready 외 상태의 빈 `title`은 `TypeError`다.
116
- - Web host는 실제 남은 높이(`height:100%` 등)를, Native host는 safe area·탭바 inset을 먼저 처리한다.
118
+ - Web host는 실제 남은 높이(`height:100%` 등)를, Native host는 위·옆 safe area와 탭바 inset을 먼저 처리한다. 아래 홈 인디케이터는 host가 SafeAreaView로 들이지 말고 `safeAreaBottom`으로 넘긴다(들인 뒤 footer 16을 더하면 버튼 아래가 50이 된다).
117
119
  HJM은 기종별 높이를 추측하지 않는다.
118
120
  - 배치는 Web·Native 모두 `layoutStyle`로 한다(Web은 `className`도 있다). 색·여백을 덮지 않는다.
119
121
 
@@ -130,7 +132,7 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
130
132
 
131
133
  - 이미 `<main>`이 있는 제품 셸 안에서는 `as="section"`을 준다. 중첩 main이 생긴다.
132
134
 
133
- - 본문 좌우 여백을 직접 빼려고 목록에 음수 margin이나 제품 CSS를 쓰지 않는다. 행이 자기 여백을 가지면 `contentInset="body-edge"`, host가 모든 여백을 가지면 `"none"`이다. 본문 안에 행 여백이 없는 제목·문단을 함께 두는 화면(설정 묶음 제목, 알림 그룹 제목 + ListRow)은 `default`를 유지한다. ListRow 좌우 여백은 8이라 본문 여백을 빼면 제목이 화면 끝에 붙는다.
135
+ - 본문 좌우 여백을 직접 빼려고 목록에 음수 margin이나 제품 CSS를 쓰지 않는다. 행이 자기 여백을 가지면 `contentInset="body-edge"`, host가 모든 여백을 가지면 `"none"`이다. ListRow·NotificationItem은 글자를 부모 콘텐츠 가장자리에 두고 표면만 8 바깥으로 넓히므로(2026-10-17) `default` 본문에서 글자가 여백 선 16에 오고 설정 묶음 제목·알림 그룹 제목과 같은 선에 선다. `body-edge` 본문에 두면 글자가 화면 끝에 붙으므로 두지 않는다.
134
136
 
135
137
  ### 디자인 프로필의 화면 표현
136
138
 
@@ -141,4 +143,6 @@ dashboard는 가로 헤더/1200 목록 폭, editorial은 세로 헤더/720 읽
141
143
 
142
144
  ## 근거
143
145
 
146
+ - 하단 여백 `max(16, inset)`(Native `safeAreaBottom`): 2026-10-17 사용자 보고 "전체적으로 버튼 아래 여백이 많아". Native Storybook의 사진 선택과 업로드 화면에서 버튼 아래가 약 33pt였다. 원인은 둘이었다. 화면 스토리의 route host가 고정 높이 720이라 iPhone 17 Pro 스토리 영역 737보다 17 짧았고(footer 16 + 빈 칸 17), 컴포넌트는 Native footer가 safe area를 다루지 않아 host의 아래 inset 위에 16을, footer 안 BottomCTA의 `safeAreaBottom`도 16 위에 더했다(16 + 34 = 50). Web footer는 이미 `max(16, env())`였지만 footer 안 BottomCTA의 `safeAreaBottom`은 그 위에 더했다. 표준을 "마지막 컨트롤 아래 = footer 위 여백 16, 홈 인디케이터 기기에서는 max(16, inset)"으로 정하고 footer가 한 번만 소유하게 했다.
147
+ - 화면 여백은 ScreenLayout 소유, 바깥 여백 + `none` 미지원: 2026-10-17 바깥 여백 호스트 예제 사용자 검토. utilverse식(`Container gutter="regular"` 20 + `contentInset="none"`)을 HJM에서 지원하려고 본문 확장·footer 바 소유를 붙인 예제가 어색하다는 판단으로 지원을 접었다(2026-10-15 공통 구조는 HJM 소유 원칙). 측정상 그 구성에서는 Web 402·1280에서 행 표면이 양쪽 8 잘리고 본문이 옆으로 8 스크롤되며, Native 3x에서 선택 테두리 옆 선이 그려지지 않았다. 제품은 바깥 여백을 걷고 기본 inset을 쓴다.
144
148
  - `contentInset="body-edge"`: 2026-10-17 사용자 결정. 화면 검토 S5#9에서 대화 목록 아바타가 화면 끝에서 약 32pt 안쪽에서 시작했다. 본문 padding 16과 ConversationRow 자체 padding 16이 겹쳤다(인스타그램 DM은 약 16). 앱 약 45곳이 "host가 모든 여백을 소유한다"는 뜻으로 `none`을 쓰고 있어 `none`의 의미를 바꿀 수 없었으므로 값을 새로 추가했다. ChatScreen의 10 타임라인 여백은 `default`일 때만 공통 여백을 대신한다.
@@ -157,7 +157,7 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
157
157
  | `resultSummary.countVisibility` | `visible` · `hidden` | `visible` | `hidden`은 시각적 개수·개수 Skeleton만 숨기며 정렬·로딩 행·빈 상태·접근성 개수 알림은 유지한다. 2026-10-14 야잘알 검색 결과에서 개수 없는 목록을 요청해 추가한 선택형 축(미게시)이다 |
158
158
  | `resultSummary.sort` | `{ label, triggerLabel, value, options: { id, label }[], onChange(id), icon? }` (Native는 `dismissLabel` 추가) | 없음 | 결과 머리 끝 Menu(single). 바꾸면 본문을 맨 위로 |
159
159
  | `appliedFilters` | `{ items: { key, label }[], removeLabel(label), onRemove(key), clearAllLabel, onClearAll(), removeIcon? }` | 없음 | 결과 머리 아래 × 칩 + `모두 해제`. 개수는 trigger 이름과 0건 원인 판정에도 쓴다 |
160
- | `filterSheet` | `{ open, onOpenChange(open), title, value, onApply(next), count(draft): number \| null, reset(draft), isDefault(draft), renderContent(draft, setDraft), labels: { close, reset, apply(count) }, size?, trigger? }` | 없음 | 열 때 `value`를 초안으로 복사, 닫히면 초안을 버림, 주 행동에서만 `onApply`. `count = 0`이면 주 행동 비활성, `isDefault`면 초기화 비활성. `trigger`를 주면 칩 줄 맨 앞에 `label(적용 개수)` Chip |
160
+ | `filterSheet` | `{ open, onOpenChange(open), title, value, onApply(next), count(draft): number \| null, reset(draft), isDefault(draft), renderContent(draft, setDraft), labels: { close, reset, apply(count) }, size?, trigger? }` | 없음 | 열 때 `value`를 초안으로 복사, 닫히면 초안을 버림, 주 행동에서만 `onApply`. `count = 0`이면 주 행동 비활성, `isDefault`면 초기화 비활성. `trigger`를 주면 칩 줄 맨 앞에 `label(적용 개수)` Chip. `title`은 필터 시트의 보이지 않는 접근성 이름이다(2026-10-17 사용자 결정: 시트 제목 줄 없음). 가림막을 누르면 닫히고 초안을 버린다 |
161
161
  | `filtersOverflow` | `wrap` · `scroll` | `wrap` | `scroll`이면 `filters`를 한 줄 가로 스크롤 영역에 넣고 화면 좌우 여백(`spacing.md` 16, `contentInset="none"`이면 0)에 `hostGutter`를 더한 만큼 가장자리까지 넓힌다. 첫 칩은 검색 입력과 같은 시작선에 놓인다 |
162
162
  | `hostGutter` | `none` · `compact` · `regular` · `spacious` | `none` | 1.13.1 변경. 이 화면을 감싼 host가 이미 준 좌우 여백 이름(`containerRecipe.gutters`: 0 · `spacing.md` 16 · `spacing.lg` 20 · `spacing.xl` 24). Container 안이면 그 `gutter`, Sheet 본문 안이면 `regular`(`sheetRecipe.content.paddingHorizontal`과 같은 `spacing.lg`). `scroll` 줄만 이 값을 쓴다 |
163
163
  | `recentSearches` | `ReactNode` | 없음 | `query`가 공백일 때만 본문 맨 위에 보인다 |
@@ -170,7 +170,7 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
170
170
  | --- | --- | --- |
171
171
  | 크기 | ScreenLayout 폭(최대 720); 검색 입력은 padding 안 전체 폭 | `ScreenLayout`, `SearchField` |
172
172
  | 간격 | 화면 padding `spacing.md` 16(notice 영역은 좌우만); 검색 입력·`filters`·`notice` 사이 `spacing.sm` 12; 본문 최근 검색–결과 `spacing.lg` 20 | Web·Native `SearchScreen` `Stack gap="sm"`·`gap="lg"` |
173
- | 순서·정렬 | 헤더(제목) → 검색 입력 → [`filterSheet.trigger`, `filters`] → `notice` → 본문. 본문은 검색 전: `recentSearches` → `recentQueries` → `suggestedQueries`(사이 `spacing.xl` 24) / 입력 중: 확정 행 → 제안 / 결과: `resultSummary.notice` → 개수·정렬 → 적용 필터(사이 `spacing.sm` 12) → `children`(위 `spacing.lg` 20) | 렌더 순서, `searchScreenRecipe` |
173
+ | 순서·정렬 | 헤더(제목) → 검색 입력 → [`filterSheet.trigger`, `filters`] → `notice` → 본문. 본문은 검색 전: `recentSearches` → `recentQueries` → `suggestedQueries`(사이 `spacing.xl` 24) / 입력 중: 확정 행 → 제안 / 결과: `resultSummary.notice` → 개수·정렬 → 적용 필터(사이 `spacing.sm` 12) → `children`(위 `spacing.lg` 20) 결과 머리 끝 정렬 트리거와 최근 검색 머리 끝 `전체 삭제`는 ghost Button `edge="end"`로 화살표·라벨 끝이 개수·제목 시작과 같은 여백 선 16에 온다(Web 29 → 16, Native 31.3 → 19.3: 화살표 잉크의 자체 여백 포함, 2026-10-17 실측 감사, [IconButton `edge`](icon-button.md#배치)) | 렌더 순서, `searchScreenRecipe` |
174
174
  | 고정·스크롤 | 검색 입력·필터는 notice 자리에 고정, 본문만 스크롤; `state`가 바뀌어도 입력·필터는 남는다 | `ScreenLayout` `notice` |
175
175
  | 좁은 폭·큰 글자 | 입력은 폭을 줄여 맞춘다. `filtersOverflow="wrap"`은 필터가 줄바꿈되어 고정 영역이 줄 수만큼 커진다. `scroll`은 큰 글자에서도 한 줄(Web 2배 글자 실측: 칩 줄 52, 1배 44)로 고정 영역을 묶는다 | `SearchScreen`, `.hjm-search-screen__filters` |
176
176
 
@@ -78,7 +78,7 @@ import { Section } from "@hjmds/react-native/primitives";
78
78
  | 항목 | 값 | 근거 |
79
79
  | --- | --- | --- |
80
80
  | 크기 | 자체 배경·테두리·여백이 없다. 폭은 부모를 따른다 | `.hjm-section` |
81
- | 글자 | 제목 `title` 18/26 bold, 설명 `caption` 11/16(두 플랫폼). 2026-10-06까지 Web은 둘 다 본문 14를 물려받았다(1.13.0 변경) | `sectionRecipe.title`·`description`, `.hjm-section__title`·`__description` |
81
+ | 글자 | 제목 `title` 18/26 500(제목 굵기), 설명 `caption` 11/16(두 플랫폼). 2026-10-06까지 Web은 둘 다 본문 14를 물려받았다(1.13.0 변경) | `sectionRecipe.title`·`description`, `.hjm-section__title`·`__description` |
82
82
  | 간격 | 머리와 본문 사이 `spacing.xs` 8, 글자 묶음과 행동 사이 `spacing.sm` 12, 제목과 설명 사이 `spacing.xxs` 4. Section끼리의 간격은 Section이 주지 않는다. 감싸는 [Stack](stack.md)이나 `layoutStyle`로 `layout.sectionGap`(`spacing.xl` 24)을 둔다 | `sectionRecipe.gap`·`headerGap`·`copyGap`, `layout.sectionGap` |
83
83
  | 순서·정렬 | 머리 행동은 끝(LTR 오른쪽)에 하나, 글자 묶음과 세로 가운데 정렬. 보통 [Link](link.md) 또는 `tone="link"` [Button](button.md) | `.hjm-section__header`, Native `Section` |
84
84
  | 고정·스크롤 | 고정되지 않는다. 화면 본문 스크롤 안에 둔다 | — |
@@ -109,3 +109,7 @@ import { Section } from "@hjmds/react-native/primitives";
109
109
  | heading 수준 | `headingLevel` | 없음(header role 고정) |
110
110
  | 루트 | `<section>` | `View` |
111
111
  | 배치 | `className`, `layoutStyle`(HTML `style`도 받음) | `layoutStyle`, slot `*Style`(`style`은 제거된 속성) |
112
+
113
+ ## 근거
114
+
115
+ 2026-10-17 사용자 결정("굵기 좀더 줄여"): 모든 글자 굵기를 한 단계 낮췄다. 큰 숫자·선택 상태 600(큰 제목 단계는 같은 날 폐지), 제목·강조 500, label 500, 본문 400이며 정본은 [타이포그래피](../tokens/typography.md)다.
@@ -80,7 +80,7 @@ import { SegmentedControl } from "@hjmds/react-native/inputs";
80
80
 
81
81
  | 항목 | 값 | 근거 |
82
82
  | --- | --- | --- |
83
- | 크기 | 항목 최소 높이 `medium` 44 · `small` 36 + hitSlop 4. 항목 좌우 여백 `medium` `spacing.md` 16 · `small` `spacing.sm` 12(두 플랫폼. Web `small`은 `::after`로 위아래 4를 넓힌다) | `segmentedControlRecipe.sizes`, `.hjm-segmented__item` |
83
+ | 크기 | 바깥 높이(트랙 테두리·안쪽 여백 포함) `height`는 같은 크기의 Button·IconButton·Chip과 같다: `medium` 44 · `small` 36. 항목 최소 높이는 그보다 트랙 안쪽 여백 4와 테두리 1을 위아래로 뺀 `medium` 34 · `small` 26이고, 위아래로만 넓힌 터치 영역(`hitSlop` 5 · 9, Web `::after`)으로 44를 채운다. 작은 트랙은 바깥으로 4 더 넓어지되 레이아웃 높이는 늘지 않는다. 큰 글자에서 세로로 쌓이면 항목마다 44. 항목 좌우 여백 `medium` `spacing.md` 16 · `small` `spacing.sm` 12. 2026-10-17 사용자 요청("높이 맞춰")으로 같은 줄의 칩과 높이를 맞췄다(이전에는 항목 높이 기준이라 트랙이 46·54였다) | `segmentedControlRecipe.sizes`, `.hjm-segmented__scroller`, `.hjm-segmented__item` |
84
84
  | 간격 | 트랙 안쪽 여백·항목 간격 `container.padding`·`gap` `spacing.xxs` 4. 트랙 모서리 `radius.lg` 16(테두리 1). 선택 항목 모서리 `radius.md` 12, 선택 테두리 2(`stroke.strong`). 아래 내용과 `layout.contentGap` 16 | `segmentedControlRecipe.container`·`item`, `.hjm-segmented__items` |
85
85
  | 순서·정렬 | 항목은 같은 폭으로 나뉘고(Native `flex: 1`, Web `flex: 1 1 0`) 트랙은 부모 폭을 채운다. 전환할 내용 바로 위, 화면 좌우 여백(`layout.pagePadding`) 안 | Native `SegmentedControl`, `.hjm-segmented__item` |
86
86
  | 고정·스크롤 | 고정되지 않는다. Web 항목은 글자보다 좁아지지 않아(`min-inline-size: max-content`) 넘치면 트랙이 가로 스크롤된다. Native는 스크롤하지 않는다 | `.hjm-segmented__items`(overflow-x) |