@hjmds/design-contracts 1.30.0 → 1.32.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 (102) hide show
  1. package/dist/behaviors.d.ts +3 -0
  2. package/dist/behaviors.d.ts.map +1 -1
  3. package/dist/bottom-navigation.d.ts +43 -0
  4. package/dist/bottom-navigation.d.ts.map +1 -1
  5. package/dist/bottom-navigation.js +55 -0
  6. package/dist/bottom-navigation.js.map +1 -1
  7. package/dist/carousel.d.ts +30 -0
  8. package/dist/carousel.d.ts.map +1 -1
  9. package/dist/carousel.js +32 -0
  10. package/dist/carousel.js.map +1 -1
  11. package/dist/catalog.d.ts +178 -1
  12. package/dist/catalog.d.ts.map +1 -1
  13. package/dist/catalog.js +16 -6
  14. package/dist/catalog.js.map +1 -1
  15. package/dist/component-recipes.d.ts +126 -0
  16. package/dist/component-recipes.d.ts.map +1 -1
  17. package/dist/component-recipes.js +71 -2
  18. package/dist/component-recipes.js.map +1 -1
  19. package/dist/icon.d.ts +58 -1
  20. package/dist/icon.d.ts.map +1 -1
  21. package/dist/icon.js +69 -0
  22. package/dist/icon.js.map +1 -1
  23. package/dist/image-choice.d.ts +47 -0
  24. package/dist/image-choice.d.ts.map +1 -1
  25. package/dist/image-choice.js +56 -0
  26. package/dist/image-choice.js.map +1 -1
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/pagination.d.ts +53 -0
  32. package/dist/pagination.d.ts.map +1 -1
  33. package/dist/pagination.js +48 -0
  34. package/dist/pagination.js.map +1 -1
  35. package/dist/recipes.d.ts +1 -1
  36. package/dist/recipes.d.ts.map +1 -1
  37. package/dist/recipes.js.map +1 -1
  38. package/dist/screen-patterns.d.ts +152 -2
  39. package/dist/screen-patterns.d.ts.map +1 -1
  40. package/dist/screen-patterns.js +132 -4
  41. package/dist/screen-patterns.js.map +1 -1
  42. package/dist/showcase.d.ts.map +1 -1
  43. package/dist/showcase.js +3 -0
  44. package/dist/showcase.js.map +1 -1
  45. package/dist/steps.d.ts +16 -7
  46. package/dist/steps.d.ts.map +1 -1
  47. package/dist/steps.js +6 -1
  48. package/dist/steps.js.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/dist/version.js.map +1 -1
  52. package/docs/authoring-tools.md +1 -0
  53. package/docs/bottom-navigation.md +28 -0
  54. package/docs/carousel.md +8 -0
  55. package/docs/data-workflows.md +1 -0
  56. package/docs/dialog.md +23 -0
  57. package/docs/generated/component-maturity.md +7 -4
  58. package/docs/generated/renderer-evidence.json +317 -19
  59. package/docs/generated/renderer-evidence.md +9 -3
  60. package/docs/generated/showcase-manifest.json +164 -6
  61. package/docs/icon.md +9 -0
  62. package/docs/pagination.md +17 -0
  63. package/docs/screen-patterns.md +55 -1
  64. package/docs/steps.md +4 -3
  65. package/docs/toast.md +10 -0
  66. package/docs/usage/README.md +5 -2
  67. package/docs/usage/SELECTION.md +1 -1
  68. package/docs/usage/components/auth-screen-layout.md +15 -5
  69. package/docs/usage/components/bottom-cta.md +11 -0
  70. package/docs/usage/components/bottom-navigation.md +99 -2
  71. package/docs/usage/components/card.md +36 -5
  72. package/docs/usage/components/carousel.md +1 -0
  73. package/docs/usage/components/chat-date-separator.md +86 -0
  74. package/docs/usage/components/chat-message.md +13 -1
  75. package/docs/usage/components/chat-screen.md +3 -2
  76. package/docs/usage/components/comment-thread-row.md +11 -3
  77. package/docs/usage/components/comment-thread-screen.md +1 -1
  78. package/docs/usage/components/dialog.md +69 -0
  79. package/docs/usage/components/editor-screen.md +6 -2
  80. package/docs/usage/components/field.md +13 -0
  81. package/docs/usage/components/icon-tile.md +19 -3
  82. package/docs/usage/components/icon.md +45 -6
  83. package/docs/usage/components/image-choice-group.md +35 -4
  84. package/docs/usage/components/media-selection-screen.md +20 -1
  85. package/docs/usage/components/message-composer.md +4 -1
  86. package/docs/usage/components/moderation-screen.md +107 -2
  87. package/docs/usage/components/page-indicator.md +102 -0
  88. package/docs/usage/components/pagination.md +28 -4
  89. package/docs/usage/components/profile-screen.md +9 -3
  90. package/docs/usage/components/profile-summary.md +3 -3
  91. package/docs/usage/components/screen-layout.md +15 -3
  92. package/docs/usage/components/search-screen.md +5 -3
  93. package/docs/usage/components/steps.md +14 -7
  94. package/docs/usage/components/tabs.md +21 -1
  95. package/docs/usage/components/text.md +30 -1
  96. package/docs/usage/components/toast.md +5 -0
  97. package/docs/usage/components/typing-indicator.md +89 -0
  98. package/docs/usage/screens/common-login.md +3 -1
  99. package/docs/usage/screens/common-profile.md +3 -3
  100. package/docs/usage/screens/flow-editor.md +13 -2
  101. package/docs/usage/screens/flow-moderation.md +31 -3
  102. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "designSystemVersion": "1.30.0",
3
+ "designSystemVersion": "1.32.0",
4
4
  "packageName": "@hjmds/design-contracts",
5
5
  "components": [
6
6
  {
@@ -2742,7 +2742,9 @@
2742
2742
  "storyId": "navigation/bottom-navigation",
2743
2743
  "name": "BottomNavigation",
2744
2744
  "label": "하단 탐색",
2745
- "companions": [],
2745
+ "companions": [
2746
+ "BottomNavigationInsetProvider"
2747
+ ],
2746
2748
  "extensions": [],
2747
2749
  "category": "navigation",
2748
2750
  "platform": "adaptive",
@@ -2830,6 +2832,55 @@
2830
2832
  "authored": "docs/breadcrumb.md"
2831
2833
  }
2832
2834
  },
2835
+ {
2836
+ "id": "page-indicator",
2837
+ "storyId": "navigation/page-indicator",
2838
+ "name": "PageIndicator",
2839
+ "label": "페이지 위치 표시",
2840
+ "companions": [],
2841
+ "extensions": [],
2842
+ "category": "navigation",
2843
+ "platform": "shared",
2844
+ "contractStatus": "beta",
2845
+ "surfaces": {
2846
+ "web": "beta",
2847
+ "native": "beta"
2848
+ },
2849
+ "requirements": [
2850
+ {
2851
+ "surface": "contract",
2852
+ "scenarios": [
2853
+ "contract"
2854
+ ]
2855
+ },
2856
+ {
2857
+ "surface": "web",
2858
+ "scenarios": [
2859
+ "default",
2860
+ "dark",
2861
+ "large-text",
2862
+ "rtl",
2863
+ "reduced-motion",
2864
+ "accessibility"
2865
+ ]
2866
+ },
2867
+ {
2868
+ "surface": "native",
2869
+ "scenarios": [
2870
+ "default",
2871
+ "dark",
2872
+ "large-text",
2873
+ "rtl",
2874
+ "reduced-motion",
2875
+ "accessibility"
2876
+ ]
2877
+ }
2878
+ ],
2879
+ "documentation": {
2880
+ "generated": "docs/generated/component-maturity.md",
2881
+ "authored": null
2882
+ }
2883
+ },
2833
2884
  {
2834
2885
  "id": "pagination",
2835
2886
  "storyId": "navigation/pagination",
@@ -4809,7 +4860,8 @@
4809
4860
  "name": "Dialog",
4810
4861
  "label": "대화상자",
4811
4862
  "companions": [
4812
- "OverlayStackProvider"
4863
+ "OverlayStackProvider",
4864
+ "ImmersiveOverlay"
4813
4865
  ],
4814
4866
  "extensions": [],
4815
4867
  "category": "overlay",
@@ -5657,7 +5709,9 @@
5657
5709
  "storyId": "layout/moderation-screen",
5658
5710
  "name": "ModerationScreen",
5659
5711
  "label": "신고 화면",
5660
- "companions": [],
5712
+ "companions": [
5713
+ "ModerationForm"
5714
+ ],
5661
5715
  "extensions": [],
5662
5716
  "category": "layout",
5663
5717
  "platform": "adaptive",
@@ -5912,7 +5966,9 @@
5912
5966
  "storyId": "layout/media-selection-screen",
5913
5967
  "name": "MediaSelectionScreen",
5914
5968
  "label": "미디어 선택 화면",
5915
- "companions": [],
5969
+ "companions": [
5970
+ "MediaSelectionCell"
5971
+ ],
5916
5972
  "extensions": [],
5917
5973
  "category": "layout",
5918
5974
  "platform": "adaptive",
@@ -6014,7 +6070,9 @@
6014
6070
  "storyId": "data-display/chat-message",
6015
6071
  "name": "ChatMessage",
6016
6072
  "label": "채팅 말풍선",
6017
- "companions": [],
6073
+ "companions": [
6074
+ "MediaAlbumPageStatus"
6075
+ ],
6018
6076
  "extensions": [],
6019
6077
  "category": "data-display",
6020
6078
  "platform": "adaptive",
@@ -6060,6 +6118,106 @@
6060
6118
  "authored": null
6061
6119
  }
6062
6120
  },
6121
+ {
6122
+ "id": "typing-indicator",
6123
+ "storyId": "feedback/typing-indicator",
6124
+ "name": "TypingIndicator",
6125
+ "label": "입력 중 표시",
6126
+ "companions": [],
6127
+ "extensions": [],
6128
+ "category": "feedback",
6129
+ "platform": "adaptive",
6130
+ "contractStatus": "beta",
6131
+ "surfaces": {
6132
+ "web": "beta",
6133
+ "native": "beta"
6134
+ },
6135
+ "requirements": [
6136
+ {
6137
+ "surface": "contract",
6138
+ "scenarios": [
6139
+ "contract"
6140
+ ]
6141
+ },
6142
+ {
6143
+ "surface": "web",
6144
+ "scenarios": [
6145
+ "default",
6146
+ "dark",
6147
+ "large-text",
6148
+ "rtl",
6149
+ "reduced-motion",
6150
+ "accessibility"
6151
+ ]
6152
+ },
6153
+ {
6154
+ "surface": "native",
6155
+ "scenarios": [
6156
+ "default",
6157
+ "dark",
6158
+ "large-text",
6159
+ "rtl",
6160
+ "reduced-motion",
6161
+ "accessibility"
6162
+ ]
6163
+ }
6164
+ ],
6165
+ "documentation": {
6166
+ "generated": "docs/generated/component-maturity.md",
6167
+ "authored": null
6168
+ }
6169
+ },
6170
+ {
6171
+ "id": "chat-date-separator",
6172
+ "storyId": "data-display/chat-date-separator",
6173
+ "name": "ChatDateSeparator",
6174
+ "label": "대화 날짜 구분",
6175
+ "companions": [],
6176
+ "extensions": [],
6177
+ "category": "data-display",
6178
+ "platform": "adaptive",
6179
+ "contractStatus": "beta",
6180
+ "surfaces": {
6181
+ "web": "beta",
6182
+ "native": "beta"
6183
+ },
6184
+ "requirements": [
6185
+ {
6186
+ "surface": "contract",
6187
+ "scenarios": [
6188
+ "contract"
6189
+ ]
6190
+ },
6191
+ {
6192
+ "surface": "web",
6193
+ "scenarios": [
6194
+ "default",
6195
+ "dark",
6196
+ "long-copy",
6197
+ "large-text",
6198
+ "rtl",
6199
+ "reduced-motion",
6200
+ "accessibility"
6201
+ ]
6202
+ },
6203
+ {
6204
+ "surface": "native",
6205
+ "scenarios": [
6206
+ "default",
6207
+ "dark",
6208
+ "long-copy",
6209
+ "large-text",
6210
+ "rtl",
6211
+ "reduced-motion",
6212
+ "accessibility"
6213
+ ]
6214
+ }
6215
+ ],
6216
+ "documentation": {
6217
+ "generated": "docs/generated/component-maturity.md",
6218
+ "authored": null
6219
+ }
6220
+ },
6063
6221
  {
6064
6222
  "id": "notification-row",
6065
6223
  "storyId": "data-display/notification-row",
package/docs/icon.md CHANGED
@@ -46,3 +46,12 @@ Lucide 1.49.0 is the verified development/showcase version and optional peer.
46
46
  The Native factory uses a scalar-only component interface instead of exporting
47
47
  one host version's RN StyleProp types, keeping consumers on supported newer RN
48
48
  versions structurally compatible. This does not replace provider/brand logos.
49
+
50
+ ## 공용 경로 — 2026-10-18 (1.31.0)
51
+
52
+ semantic 이름별 24-unit stroke 경로는 `semanticIconPaths`(`components/icon`)가 소유한다. 이전에는 Web renderer
53
+ 내부 레지스트리라 Native 제품이 같은 그림을 adapter에 다시 그렸다(utilverse `arrowUp`·`arrowDown`·`rotate`).
54
+ Web Icon은 이 데이터를 내장 그림으로 쓰고, Native는 optional peer `react-native-svg`를 쓰는 `/icon-svg`의
55
+ `renderSemanticGlyph`로 같은 경로를 그린다. Native core는 계속 SVG 없이 동작하며 `renderGlyph` 계약은 바뀌지 않는다.
56
+ 이번에 순서 이동(`arrowUp`·`arrowDown`), 회전(`rotate`, 시계 방향), 차단 해제(`unblock`)를 추가했다. 세 방향 기호는
57
+ 물리 방향이라 RTL에서 반전하지 않는다. 눈/가린 눈은 기존 `visibility`·`visibilityOff`다.
@@ -31,6 +31,22 @@ validator가 `TypeError`로 거부한다. `currentPage`가 유도된 `totalPages
31
31
  범위를 벗어나면 `RangeError`로 거부한다 — 조용히 clamp하지 않는다(브리프:
32
32
  "validator는 던진다").
33
33
 
34
+ ### 전체 수를 모르는 결과 (2026-10-18, 1.32.0)
35
+
36
+ choose-window 장소 검색은 Kakao Local이 `meta.is_end`(다음 페이지가 있는지)만 주고 총량은 주지 않아 Pagination을
37
+ 쓰지 못하고 이전/다음 Button 줄을 직접 조립했다. LoadMore는 결과를 이어 붙이므로 "이전 결과"로 돌아가는 이 화면의
38
+ 페이지 교체 모델과 다르다. 그래서 같은 컴포넌트에 별도 descriptor 형태를 더했다.
39
+
40
+ - `PaginationOpenEndedDescriptor = { currentPage, hasNext }`. 총량 필드·window 옵션은 함께 줄 수 없고
41
+ (`hasNext`는 총량 형태에서 금지 키다) `isPaginationOpenEndedDescriptor`가 `hasNext` 유무로 형태를 가른다.
42
+ - `resolvePaginationOpenEndedDescriptor(descriptor, { labels, composeCurrentPageLabel })`가
43
+ `{ currentPage, hasPrevious, hasNext, labels, currentPageLabel }`을 만든다. 번호 목록은 없다. 다음 페이지 너머는 주소를
44
+ 알 수 없기 때문이다. 현재 페이지 표시는 버튼이 아닌 글자이며 제품이 조립한 `currentPageLabel`이 보이는 글자이자
45
+ 접근성 이름이다. 기존 `composeAccessibleName`의 `totalPages: number`를 nullable로 넓히면 기존 제품 composer의 타입이
46
+ 깨지므로 별도 composer로 분리했다.
47
+ - 기존 총량 형태와 `ResolvedPaginationDescriptor`는 바뀌지 않는다. renderer props는 descriptor 형태와 composer를
48
+ 짝지은 union이며, 짝이 맞지 않으면 resolver가 composer 부재로 던진다.
49
+
34
50
  ### 배제한 것 — size changer와 jump-to-page
35
51
 
36
52
  **페이지 크기 변경(size changer)과 "몇 페이지로 이동" 입력은 넣지 않는다.**
@@ -110,6 +126,7 @@ Steps와 같은 이유다: 순서를 나타내는 문장의 어순과 조사는
110
126
  | 축 | 상태 |
111
127
  | --- | --- |
112
128
  | `currentPage`, `totalPages`(또는 `totalCount`+`pageSize`) | 공개(필수) |
129
+ | 총량 없는 `hasNext` + `composeCurrentPageLabel` | 공개(1.32.0, 별도 descriptor 형태) |
113
130
  | 페이지 window 크기(`siblingCount`/`boundaryCount`) | 공개(선택, 기본값 있음) |
114
131
  | 생략 표시 | 공개하되 장식으로 한정 — 낭독 제외 |
115
132
  | page size changer | **배제** — 측정된 수요 없음, 필요 시 제품이 같은 상태를 다시 넘기는 방식으로 조합 가능 |
@@ -156,7 +156,7 @@ UploadItem, RadioGroup, AlertDialog, TextField와 ScreenLayout의 계약을 그
156
156
  | EditorScreen | 수정 중 닫기 확인, 저장 busy, 초안 안내 | 검증, 영속 초안, 저장, 라우터/OS 뒤로가기 guard |
157
157
  | ProfileScreen | 요약, 수정 진입, 계정 action 슬롯 | 계정 정보, 인증, 탈퇴/로그아웃 |
158
158
  | ModerationScreen | 사유 선택, 신고 활성 조건, 차단 확인 | 서버 신고/권한, 차단 mutation |
159
- | MediaSelectionScreen | 썸네일 격자, 업로드 상태, 순서/삭제/재시도 | 실제 picker, 권한, 이미지 URI 수명, 업로드 |
159
+ | MediaSelectionScreen | 썸네일 격자, 업로드 상태, 순서/삭제/재시도, 제품 작업 중 순서·회전·삭제 잠금(`disabled`, 1.32.0) | 실제 picker, 권한, 이미지 URI 수명, 업로드, 잠글 작업의 시작·끝 |
160
160
  | SearchScreen | 300ms debounce, 이전 요청 AbortSignal, 검색/필터 슬롯, 기본 입력의 확정 신호(`onSubmit`)·검색 중 표시(`searching`)·label 숨김, 한 줄 가로 스크롤 필터 줄, 두 단계 검색 단계(검색 전·입력 중·결과)와 확정 경로, 최근·추천 검색어·제안 배치, 결과 개수·정렬, 적용 필터 칩, 초안/적용 필터 시트, 0건 원인별 복구, 개수 낭독 | API, 제안·결과·개수 조회, 오류 문구, 필터 정의와 계산, 정렬 값, 최근 검색 저장소, 늦은 응답 무시 |
161
161
  | PermissionScreen | prompt/denied/granted/unavailable에 맞는 action | OS 요청과 설정 이동, 앱 복귀 후 실제 권한 조회 |
162
162
  | OnboardingScreen | 단계 범위 검증, 이전/다음/건너뛰기 | 선택 데이터와 완료 여부 저장 |
@@ -316,6 +316,7 @@ SearchScreen accepts a queryField slot for an existing accessible SearchField wi
316
316
  | `committedQuery`를 주면 두 단계 검색. 단계는 `resolveSearchScreenPhase(query, committedQuery)`가 정한다(공백 = 검색 전, 확정값과 다름 = 입력 중, 같음 = 결과, 앞뒤 공백 무시) | 참고 서비스 공통의 "입력 중 제안 → 확정 → 결과" 구분. 기존 한 단계 검색(`committedQuery` 없음)은 그대로 둔다 | 입력 즉시 결과만 지원(제안 단계가 없어 최근 검색 저장 시점이 없음) |
317
317
  | 모든 확정(Enter·검색 키, "‘q’ 검색" 행, 제안, 최근 검색, 추천 검색어)은 `onSubmit` 하나로 간다. 값은 `resolveSearchCommit`으로 앞뒤 공백을 지우고 공백만이면 부르지 않는다. 고른 값이 입력과 다르면 먼저 `onQueryChange`를 부른다 | "확정한 검색만 최근 검색에 넣는다"를 API가 지킨다. debounce된 `onSearch`는 `onSubmit`에 닿지 않는다 | 행마다 다른 콜백(제품이 저장 규칙을 경로마다 다시 맞춤) |
318
318
  | 입력 중에는 `filters`·필터 시트 trigger를 숨기고, 검색어 때문에 0건이면(`resolveSearchEmptyCause` = `query`) 레일을 숨긴다. 필터 때문에 0건이면 레일을 남기고 "모두 해제"를 EmptyState 행동으로 둔다 | 입력 중 고정 영역을 줄인다. 필터가 원인인데 레일을 숨기면 원인을 지울 길이 사라진다 | 참고 서비스처럼 0건이면 항상 숨김 |
319
+ | 1.32.0: `filtersRole`(`narrow` 기본 · `scope`). `scope`면 검색어 때문인 0건에도 레일을 남긴다. 판정은 `resolveSearchFilterRailVisible(phase, twoStep, cause, role)` 하나를 두 renderer가 부른다 | 2026-10-18 choose-window: 장소명/주소 전환 칩이 0건의 복구 수단인데 레일이 숨겨져 `resultSummary`를 버리고 로딩·0건을 직접 그렸다. "검색어 탓이면 어떤 필터도 못 고친다"는 좁히는 필터에만 맞는다. 필터 의미는 제품만 알므로 제품이 선언하고, 보이기 규칙은 계약이 소유해 플랫폼 간에 갈리지 않는다 | 0건이면 항상 레일 유지(좁히는 필터만 있는 화면에서 소용없는 칩이 고정 영역을 차지), `appliedFilters`에 기본 범위를 가짜 항목으로 넣기(× 칩·"모두 해제"가 범위를 지우는 동작이 되어 의미가 틀림) |
319
320
  | 필터 시트는 열 때 `value`를 초안으로 복사하고, 어떤 방식으로 닫혀도 초안을 버리며, 주 행동에서만 `onApply(draft)`. 주 행동 문구는 `count(draft)`로 실시간, 0이면 비활성. 초기화는 항상 같은 자리, 초안이 기본값이면 비활성 | 기존 `여러 조건 적용과 초기화` 규칙과 eBay·Baymard 권고("N개 결과 보기"). 위치 흔들림 방지 | 시트 안에서 즉시 적용(조합 필터는 결과가 계속 바뀜) |
320
321
  | 시트 열림은 제품이 제어(`open`/`onOpenChange`). 섹션만 여는 패싯 칩은 제품이 자기 범위를 정하고 `open`을 켠다 | 필터 정의·범위는 제품 소유. HJM에 범위 개념을 넣으면 제품 필터 모델을 강제한다 | `scope` 문자열을 HJM이 소유 |
321
322
  | 레일 첫 칩(`filterSheet.trigger`)은 HJM이 그리고 이름에 적용 개수를 넣는다(Web `aria-haspopup="dialog"`) | 적용 칩·모두 해제 뒤 포커스가 돌아갈 고정 대상이 필요하다 | 제품이 trigger를 그림(포커스 복귀 대상을 HJM이 모름) |
@@ -426,3 +427,56 @@ API별 제공 기준과 개발 중 추가분은 [사용 지침](usage/README.md)
426
427
 
427
428
  검증: 각 renderer의 `screen-recovery` 테스트. Web은 Chromium 동작 검사, Native는 host 렌더 검사이며
428
429
  실기기 성능·운영 서버·npm 게시·소비 앱 반영을 뜻하지 않는다.
430
+
431
+ ## 2026-10-18 헤더 식별 표시·채팅 타임라인 조각
432
+
433
+ HJM 1.30.0 채택 감사에서 제품이 공통 구조를 다시 만든 지점(야잘알 팀 상세 헤더, 유틸버스 도구 헤더·런처·채팅,
434
+ 번뚝 DM, spint 홈 로딩)을 공개 API로 옮겼다. 모두 추가 API이며 기존 기본값은 그대로다.
435
+
436
+ - `ScreenLayout.titleLeading`: 제목 앞 장식 표시. `titleVisibility="hidden"`이면 표시가 `title`을 이름으로 하는
437
+ heading이 된다. ChatScreen의 비공개 상대 사진 자리를 공개했다(`avatar`는 유지). Native 가운데 헤더의 양쪽 열
438
+ 측정 방식은 바꾸지 않았다.
439
+ - `ScreenLayout.loadingIndicator`: CompositionLayout과 같은 브랜드 로딩 계약.
440
+ - `IconTile` `frame`·`shape`·`size`(`iconTileRecipe.shapes`·`fillMinSize`·`fillMaxSize`·`defaults`).
441
+ - `ChatMessage.reactions.summary`(`chatMessageRecipe.reactionSummary`, `resolveChatReactionSummary`)와
442
+ 메뉴 행 `tone`(Menu의 `MenuItemTone`).
443
+ - `MessageComposer.error`는 나타날 때 assertive로 알린다(버그 수정).
444
+ - 새 조각 `TypingIndicator`(`typingIndicatorRecipe`)·`ChatDateSeparator`(`chatDateSeparatorRecipe`).
445
+
446
+ 검증: Web `screens-additions.browser.test.tsx`(Chromium), Native `screens-additions.test.tsx`(host 렌더),
447
+ contracts `screen-patterns.test.ts`. 실기기·npm 게시·소비 앱 반영을 뜻하지 않는다.
448
+
449
+ ### 2026-10-18 읽기 전용 반응 집계·앨범 장 상태 (1.32.0)
450
+
451
+ HJM 1.31.0 유틸버스 채택에서 남은 두 격차를 공개 API로 옮겼다. 둘 다 추가 API이며 기본값은 그대로다.
452
+
453
+ - `ChatReactionSummary.readOnly`(기본 `false`): 반응할 수 없는 독자(게스트)에게 집계를 시각 옆 같은 자리에
454
+ 누름 대상이 아닌 글자로 그린다. 알약 표면 없이 caption, 접근성 이름은 `label` 하나다. 유틸버스는 게스트 집계를
455
+ `actions`에 제품 caption으로 따로 그렸다.
456
+ - `MediaAlbumPageStatus`(`mediaAlbumRecipe.pageStatus`, `validateMediaAlbumPageStatus`): `mediaAlbum.renderPage`가
457
+ 사진 대신 그리는 불러오는 중·실패(+다시 시도) 장. 표시되는 동안 사진 칸은 하나의 접근성 이미지(Native는 자식을
458
+ 합치는 단일 요소)·열기 버튼이 아니게 되어 스피너 이름·실패 문구·다시 시도 버튼이 화면 낭독기와 포인터에 닿는다.
459
+ 사진 칸과 장 wrapper는 같은 요소로 유지되어 제품 장(조회 소유)이 다시 마운트되지 않는다. 칸 가운데 정렬(VQ-09).
460
+ 유틸버스는 장 안에 CompositionLayout 상태를 그렸지만 칸의 `role="img"`(Native `accessible`)가 그 내용을 삼켰다.
461
+
462
+ 검증: Web `media-album.browser.test.tsx`·`screens-additions.browser.test.tsx`(Chromium), Native
463
+ `media-album.test.tsx`·`screens-additions.test.tsx`(host 렌더), contracts `media-album.test.ts`. 실기기 낭독기 확인은 하지 않았다.
464
+
465
+ ## 2026-10-18 지도 본문·아이콘 크기 전달·하단 탐색 여백 소유 (1.32.0)
466
+
467
+ HJM 1.31.0 채택 뒤에도 제품이 공통 계산을 다시 하던 세 지점을 정리했다. 기본값은 그대로다.
468
+
469
+ - `ScreenContentInset`에 `body-bleed`를 더했다. 헤더·notice·footer는 여백 16을 유지하고 본문은 네 방향 padding이
470
+ 0이다. footer가 없으면 본문이 마지막 영역이므로 여백 없이 `safeAreaBottom`과 겹친 하단 탐색 높이만 비운다.
471
+ 선택의창 지도 화면(`body-edge`)은 지도 위아래에 16 띠가 남았다. `none`은 host가 모든 여백을 가진다는 기존 의미라
472
+ 쓸 수 없었다. 함께 `body-edge`·`body-bleed`의 상태 안내 묶음(로딩·빈·오류)이 좌우 여백 16을 유지하도록 고쳤다.
473
+ 본문 여백을 넘겨받는 것은 제품 내용(행·지도)이고 공통 상태 묶음은 자기 여백이 없어 글이 화면 끝에 붙었다.
474
+ - `IconTile.icon`은 함수도 받는다. 함수는 `IconTileArtwork`(`{ edge }`: `fixed`는 64, `fill`은 측정한 칸 크기)를 받아
475
+ 고정 크기 이미지가 스스로 크기를 맞춘다. 측정 전 추정이 필요한 host는 `resolveIconTileFillEdge(cellWidth)`로
476
+ 같은 48~68 제한을 쓴다. 유틸버스 런처가 이 제한을 격자 폭에서 다시 계산해 아트워크 크기로 넘겼다.
477
+ - `BottomNavigationInsetProvider`의 덮인 높이는 ScreenLayout만 적용한다(patch). 사용 지침이 `scroll="content"` 화면에
478
+ `useBottomNavigationInset()`을 다시 더하라고 안내해, 따르면 마지막 영역 여백과 목록 여백이 두 번 쌓였다. 코드는
479
+ 이미 한 곳(마지막 영역)만 적용했으므로 지침을 고치고 두 renderer에 한 번만 적용되는지 검사를 더했다.
480
+
481
+ 검증: Web `hjm-1320-screen-insets.browser.test.tsx`(Chromium), Native `hjm-1320-screen-insets.test.tsx`(host 렌더),
482
+ contracts `screen-patterns.test.ts`. 실기기·npm 게시·소비 앱 반영을 뜻하지 않는다.
package/docs/steps.md CHANGED
@@ -33,7 +33,7 @@ index > cursor → pending
33
33
  두 단계 이상 "current"이거나, cursor보다 앞선 단계가 아직 "pending"으로 남거나, cursor
34
34
  너머의 단계가 "complete"인 상태는 애초에 표현할 수 없다 — 이 저장소의 다른 계약들
35
35
  (`SheetOpenState`, `ComboboxCollectionState`, `LoadMoreState`)과 같은 이유다: 유효하지 않은
36
- 조합을 타입과 유도 규칙으로 만들 수 없게 한다. clickable을 공개하지 않기로 했으므로(아래
36
+ 조합을 타입과 유도 규칙으로 만들 수 없게 한다. 누르기는 커서 앞 단계로 돌아가는 것만 열었으므로(아래
37
37
  참고) 흐름은 항상 cursor 기준 선형이라 이 유도가 항상 맞다.
38
38
 
39
39
  `"complete"`는 2026-10-02에 추가했다. 그 전에는 마지막 단계에 커서가 있으면 항상 "진행 중"으로
@@ -100,11 +100,12 @@ index > cursor → pending
100
100
  | --- | --- |
101
101
  | `pending / current / complete / error`(유도) | 공개 |
102
102
  | 순서(`position`/`total`) | 공개 |
103
- | clickable(스텝 탭으로 이동) | **배제** — 브리프 지침이자 실제 사용처 둘 다 뒤로 가기는 별도 버튼(Yajalal `OnboardingScreen`의 "이전" `AppButton`)이다. 필요해지면 그때 추가한다. |
103
+ | 지난 단계 누르기(`revisitable` + renderer `onStepPress`) | **공개(1.31.0)** — 2026-10-18 spint 작성 화면이 지난 단계를 눌러 돌아가려고 단계 레일을 직접 그렸다. resolver가 커서보다 앞선 단계에 `revisitable: true`를 붙이고, renderer는 제품이 `onStepPress`를 줄 때 그 단계만 버튼으로 만든다(칸 전체, 최소 44 `stepsRecipe.press.minTouchTarget`). 이전 판단("뒤로 가기는 별도 버튼")은 바로 앞 단계 행동에는 그대로 유효하다. |
104
+ | 앞으로 건너뛰기(현재·다음 단계 누르기) | **배제** — 검증 전 단계를 지나치고 단일 커서 유도를 흐린다. 커서 단계는 이미 있는 자리라 누를 이유가 없다. |
104
105
  | vertical 방향 | **배제** — 두 사용처 모두 가로 진행바 하나만 필요해 antd 표면을 미리 복제하지 않는다. |
105
106
  | dot type(압축 점 표현) | **배제** — 측정된 요구가 없다. numbered + check/error 마크 하나만 공개한다. |
106
107
  | custom icon(스텝별 임의 아이콘 교체) | **배제** — 4개 상태 마크로 충분하고, 임의 아이콘 허용은 브랜드 표현의 일관성을 깨뜨린다. |
107
- | navigation type(`navigation`/`inline` 등 antd variant) | **배제** — 위 clickable/vertical 배제와 같은 이유로 단일 표현만 남긴다. |
108
+ | navigation type(`navigation`/`inline` 등 antd variant) | **배제** — 위 앞으로 건너뛰기·vertical 배제와 같은 이유로 단일 표현만 남긴다. |
108
109
 
109
110
  ## 검증 화면
110
111
 
package/docs/toast.md CHANGED
@@ -165,6 +165,16 @@ Toast의 `●` 글리프가 뜻 없는 회색 점으로 보였고, 글자 글리
165
165
 
166
166
  - tone은 32-unit 원형 tinted badge 안의 stroke SVG 아이콘이 전달합니다(neutral = 종). 2px 가장자리
167
167
  띠(`toneMark`)는 badge와 역할이 겹쳐 너비 0으로 은퇴했습니다 — slot은 가산 계약을 위해 남깁니다.
168
+
169
+ ### 띠 계약 — 2026-10-18 (1.32.0)
170
+
171
+ Toast에는 가장자리 상태색 띠가 **없습니다**. tone은 badge(또는 제품 `media`)와 문구가 전달하며, 띠를 켜거나 끄는
172
+ 옵션을 두지 않습니다. 번뚝이 1.10.0 이후에도 띠를 숨기려고 Web `.hjm-toast__tone-mark`를 `display: none`으로 덮고
173
+ Native 토스트 영역의 `statusAccents`·`danger`를 바탕색으로 바꿔 두었는데, 덮을 띠가 없었고 Native 덮어쓰기는 tone
174
+ 아이콘 색 입력만 지웠습니다. 원인은 은퇴한 slot이 Web DOM에 늘 숨겨진 빈 요소로, Native에 도달하지 않는 분기로 남아
175
+ "띠가 있다"고 읽힌 것입니다. 1.32.0은 두 renderer에서 그 요소·분기를 지우고, `toastRecipe.toneMark`(너비 0)는 게시된
176
+ recipe 모양 호환을 위해서만 `@deprecated`로 남깁니다(다음 major에서 제거). 띠를 다시 그리려면 badge와의 역할 중복을 새로
177
+ 검토하는 별도 계약 변경이 필요합니다.
168
178
  - action은 끝 정렬 tinted pill(`action.background` = `surface.brand`, 44-unit target)입니다. 라벨과
169
179
  fill 대비 4.5:1을 contract test가 지킵니다.
170
180
  - 카드 radius md → lg, description은 secondary 색으로 제목과 위계를 나눕니다. 테두리는
@@ -6,7 +6,7 @@
6
6
  버튼 위치에서 시작해 구성·컴포넌트 지침으로 내려가고, 값은 토큰 지침에서 고른다.
7
7
  [선택 기준](SELECTION.md)으로 그대로 사용·공개 슬롯 확장·공통 API 보완·제품 고유 기능 연결을 고른다. 설치 버전의 지침을 본다: `node_modules/@hjmds/design-contracts/docs/usage/`.
8
8
  제공 기준은 확인한 기본 API 버전이며 최초 출시 버전이 아니다. 구성·화면은 각 API 지침의 제공 버전을 확인한다.
9
- API 안정성은 1.29.0의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
9
+ API 안정성은 1.31.0의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
10
10
  Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품 반영을 의미하지 않는다.
11
11
  `분류`는 Storybook 제목 `<배포|실험>/<단계>/<분류>/<항목>`의 셋째 마디이고, 토큰·구성·화면은 Storybook 메뉴와 같은 순서다.
12
12
 
@@ -57,6 +57,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
57
57
  | [Carousel](components/carousel.md) | 데이터 표시 | 한 번에 카드 하나만 보이고 사용자가 순서대로 넘겨 보는 유한한 묶음에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
58
58
  | [Celebration](components/celebration.md) | 구성/직접 조작과 모션 | 목표 달성, 첫 완료처럼 드물게 일어나는 성공 순간에 한 번 터지는 색종이 효과에 쓴다. | 1.12.1 | Web · Native | 개별 API 계약 참조 | 배포 |
59
59
  | [Chart](components/chart.md) | 데이터 표시 | 수치의 흐름·비교·몫·모양·관계를 그림으로 읽게 할 때 쓴다. | 1.21.0 | Web · Native | Web beta / Native beta | 배포 |
60
+ | [ChatDateSeparator](components/chat-date-separator.md) | 데이터 표시 | 대화 타임라인에서 날짜(또는 오래 쉰 뒤의 시각)가 바뀌는 자리에 가운데 정렬 구분 줄을 둘 때 쓴다. | 미게시(1.30.0 이후) | Web · Native | Web beta / Native beta | 실험 |
60
61
  | [ChatMessage](components/chat-message.md) | 구성/정보 표시 | DM·대화 타임라인의 메시지 한 개에 쓴다. | 1.17.1 | Web · Native | Web stable / Native stable | 배포 |
61
62
  | [ChatScreen](components/chat-screen.md) | 화면/소통 | DM·대화방처럼 헤더, 메시지 타임라인, 하단 작성창으로 이루어진 화면 한 장에 쓴다. | 1.17.1 | Web · Native | Web stable / Native stable | 배포 |
62
63
  | [Checkbox](components/checkbox.md) | 입력 | 독립된 예/아니오 하나를 고르는 항목에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
@@ -99,7 +100,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
99
100
  | [Grid](components/grid.md) | 레이아웃 | 카드·타일처럼 같은 모양의 자식을 창 크기에 따라 열 수를 바꿔 배치할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
100
101
  | [Heading](components/heading.md) | 글자와 아이콘 | 자리를 모르는 제목 글자 하나가 필요할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
101
102
  | [HistogramExplorer](components/histogram-explorer.md) | 구성/정보 표시 | 실제 관측값 배열의 분포를 동폭 구간 막대로 보여 주고, 사용자가 구간 수를 바꿔 볼 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
102
- | [Icon](components/icon.md) | 글자와 아이콘 | HJM semantic 이름(`search`, `back`, `chevronEnd`, `notifications` 등 43개)으로 고르는 그림 기호에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
103
+ | [Icon](components/icon.md) | 글자와 아이콘 | HJM semantic 이름(`search`, `back`, `chevronEnd`, `notifications` 등 47개)으로 고르는 그림 기호에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
103
104
  | [IconButton](components/icon-button.md) | 동작 | 보이는 글자 없이 아이콘만으로 표시하는 행동에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
104
105
  | [IconTile](components/icon-tile.md) | — | 프로필·런처 격자의 아이콘 위/제목 아래 항목에 쓴다. | 1.26.0 | Web · Native | Web stable / Native stable | 실험 |
105
106
  | [Image](components/image.md) | 데이터 표시 | 원본 크기를 아는 사진·차트 이미지를 로드 전에 자리를 잡아 두고, 실패해도 의미를 잃지 않게 보여 줄 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
@@ -134,6 +135,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
134
135
  | [OnboardingScreen](components/onboarding-screen.md) | 화면/소개 | 첫 실행 소개·초기 설정처럼 **몇 단계를 차례로 넘기는 화면**에 쓴다. | 1.14.0 | Web · Native | Web stable / Native stable | 배포 |
135
136
  | [OtpField](components/otp-field.md) | 입력 | 문자·메일로 받은 **숫자 인증번호**를 칸 모양으로 입력받을 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
136
137
  | [OverviewScreen](components/overview-screen.md) | 레이아웃 | 같은 데이터와 기능을 유지하면서 테마별 행·카드·격자와 도구 배치를 선택하는 목록 화면에 쓴다. | 1.15.0 | Web · Native | 개별 API 계약 참조 | 배포 |
138
+ | [PageIndicator](components/page-indicator.md) | 탐색 | 제품이 직접 만든 가로 페이저(Native 페이징 `FlatList`, Web scroll-snap 줄) 아래에 지금 몇 번째 페이지인지 점으로 보여 주고, 점을 눌러 그 페이지로 옮길 때 쓴다. | 미게시(1.30.0 이후) | Web · Native | Web beta / Native beta | 실험 |
137
139
  | [Pagination](components/pagination.md) | 탐색 | 총 개수(또는 총 페이지 수)가 정해진 결과 집합에서 사용자가 **임의의 페이지로 바로 이동**해야 할 때 Web·Native에서 쓴다. | 1.12.1 | Web · Native | Web stable / Native beta | 배포 |
138
140
  | [PasswordField](components/password-field.md) | 입력 | 비밀번호를 입력받고, 필요할 때만 값을 눈으로 확인하게 할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
139
141
  | [PasswordRulesField](components/password-rules-field.md) | 구성/입력과 작성 | 새 비밀번호 입력 아래에 제품이 정한 규칙의 충족 개수와 규칙별 상태를 함께 보여 줄 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
@@ -203,6 +205,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
203
205
  | [TransferList](components/transfer-list.md) | 입력 | 한 항목 집합을 두 목록으로 나누고 사용자가 항목을 오가게 할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
204
206
  | [TransferTask](components/transfer-task.md) | 구성/피드백과 복구 | 업로드·다운로드·동기화처럼 실제 전송 계층이 진행량을 알려 주는 작업에 시작·취소·진행률·결과를 한 묶음으로 붙인다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
205
207
  | [Tree](components/tree.md) | 데이터 표시 | 깊이가 정해지지 않은 계층 데이터를 펼치고 접으며 탐색하고, 그 안에서 하나 또는 여럿을 고를 때 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
208
+ | [TypingIndicator](components/typing-indicator.md) | 상태와 알림 | 대화 타임라인에서 상대가 지금 입력 중임을 보여 줄 때 쓴다. | 미게시(1.30.0 이후) | Web · Native | Web beta / Native beta | 실험 |
206
209
  | [UploadItem](components/upload-item.md) | 데이터 표시 | 사용자가 고른 파일 **한 개**의 업로드 상태(대기·전송 중·완료·실패)를 한 행으로 보여 줄 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
207
210
  | [VirtualList](components/virtual-list.md) | 데이터 표시 | 행 높이가 **모두 같은** 긴 목록(수백~수천 행)을 정해진 높이 안에서 스크롤할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
208
211
  | [VisuallyHidden](components/visually-hidden.md) | 기반 기능 | Web에서 화면에는 보이지 않지만 스크린 리더는 읽어야 하는 **문맥 문구**를 덧붙일 때 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
@@ -43,7 +43,7 @@
43
43
  | 예제 전용 도구(실패 예약 등) | "미리보기 도구" 캡션 아래 `Button size="small" tone="ghost"`로 분리 | 제품 행동과 같은 모양·폭 |
44
44
  | 헤더 제목·추가 기능 | 제목은 기본 가운데(`ScreenLayout`·`TopBar`, 왼쪽 정렬은 선택), 추가 기능은 `actions`에 아이콘 버튼 1~2개로 오른쪽 위(`ScreenFlowAction.icon`, 넘치면 Menu) | 왼쪽 정렬 제목, 헤더의 글자 버튼(가운데 제목을 밀어냄), 본문에 흩어진 도구 버튼 |
45
45
  | 도구성 행동(새로고침·추가·편집·삭제·공유·검색·필터·더보기) | `IconButton` + 아이콘, 글자는 `label`(보조기술 이름). HJM 화면은 아이콘 슬롯(`ScreenFlowAction.icon` 등) | 헤더·행·도구 막대의 글자 버튼 |
46
- | 화면 나가기(뒤로·닫기·설정 나가기) | `ScreenLayout`(과 이를 쓰는 화면)의 `exit` — 헤더 시작 쪽 아이콘 버튼, 글자 없음 | 본문·하단의 "나가기" 글자 버튼, 화면마다 다른 화살표 |
46
+ | 화면 나가기(뒤로·닫기·설정 나가기) | `ScreenLayout`(과 이를 쓰는 화면)·`AuthScreenLayout`의 `exit` — 헤더 시작 쪽 아이콘 버튼, 글자 없음. 단계형 작성의 앞 단계는 `EditorScreen` `stepBack`(1.31.0) | 본문·하단의 "나가기" 글자 버튼, 화면마다 다른 화살표, 로그인·작성 화면 위에 따로 얹은 `TopBar` |
47
47
  | 버튼 묶음 간격 | 세로로 쌓으면 `gap="xs"`(8), 가로로 나란하면 `gap="sm"`(12) — `actionGroupRecipe` | 세로 버튼 사이 12 이상 |
48
48
  | 간격 | `Stack`·`ScreenLayout`·recipe 간격 | 임의 `margin`·빈 `View` |
49
49
  | 화면 진입 시 입력 포커스 | `ScreenLayout`(과 이를 쓰는 화면)·`AuthScreenLayout`·`Sheet`·`Dialog`의 `initialFocus`, 주 입력 지정은 칸의 `initialFocusTarget`([진입 포커스](components/screen-layout.md#진입-포커스)) | 앱별 `autoFocus`·지연 `ref.focus()`·단계 전환·검색 열기 뒤 `ref.focus()`, 대화·댓글 화면의 작성창 자동 포커스 |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1 (`safeAreaBottom`·링크 `target`/`rel`/`accessibilityHint` 1.28.1)
7
- - 검토일: 2026-10-17
7
+ - 검토일: 2026-10-18
8
8
  - 근거: [로그인 화면 골격](../../auth-screen.md), [1.4 제품 채택 가이드](../../product-adoption-1.4.md), recipe `authScreenRecipe`(`src/auth-screen.ts`), 포트폴리오 상위 기준 루트 `docs/LOGIN_SCREEN_STANDARD.md`(LS)
9
9
  - 스토리북: `배포/컴포넌트/레이아웃/로그인 화면`
10
10
 
@@ -21,6 +21,7 @@
21
21
  | --- | --- |
22
22
  | 제공자 버튼 하나하나 | [AuthProviderButton](auth-provider-button.md) (`main` 슬롯에 넣는다) |
23
23
  | 동의 체크 항목 | [Agreement](agreement.md) (`footer`나 가입 단계에 넣는다) |
24
+ | 로그인 위에 "로그인" 제목·뒤로/닫기만 두려고 바깥에 얹는 [TopBar](top-bar.md) | 이 레이아웃의 `title`·`exit`(1.31.0) |
24
25
 
25
26
  ## 공개 이름과 import
26
27
 
@@ -54,6 +55,8 @@ import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react
54
55
  import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react-native/auth-screen";
55
56
 
56
57
  <AuthScreenLayout
58
+ title={t("auth.title")} // 머리 띠의 route 이름(선택)
59
+ exit={{ kind: "close", label: t("common.close"), disabled: pending, onPress: close }} // 헤더 시작 쪽 닫기(선택)
57
60
  mainCard
58
61
  {...(pending ? { pendingLabel: t("auth.pending") } : {})}
59
62
  hero={<AuthScreenHero mark={<ProductMark />} title={t("auth.tagline")} description={t("auth.description")} />}
@@ -71,6 +74,8 @@ import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react
71
74
 
72
75
  | prop | 값 | 기본값 | 설명 |
73
76
  | --- | --- | --- | --- |
77
+ | `title` | 문자열 | — | 1.31.0. route 이름("로그인")을 [ScreenLayout](screen-layout.md) 가운데 정렬 머리와 같은 띠(최소 높이 52, `Heading` `level5`, 문서 수준 1)에 그린다. 공백만 있으면 `TypeError`. Web은 이때 `AuthScreenHero`의 기본 `headingLevel`이 2가 된다(route 이름이 h1) |
78
+ | `exit` | `ScreenExit`(`{ kind: "back" \| "close", label, onClick/onPress, disabled? }`) | — | 1.31.0. ScreenLayout·TopBar `exit`와 같은 아이콘 뒤로/닫기, 머리 띠 시작 끝. `label`은 보조기술 이름. `title` 없이 `exit`만 줘도 띠를 그린다 |
74
79
  | `hero` · `main` | `ReactNode` | 필수 | 제품 마크·제목·설명 / 제공자 버튼 목록(진행 중에도 같은 목록) |
75
80
  | `footer` | `ReactNode` | — | 동의 고지·정책 링크 |
76
81
  | `density` | `regular` · `compact` | `regular` | 제품이 화면 높이를 보고 고른다. 간격·padding만 바뀐다 |
@@ -80,7 +85,7 @@ import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react
80
85
  | Web `as` | `main` · `section` | `main` | 최대 폭은 416이다 |
81
86
  | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 전용(Web·Native 모두) |
82
87
  | `initialFocus` | `first-input` · `none` | `none` | 1.28.2. [ScreenLayout 진입 포커스](screen-layout.md#진입-포커스)와 같은 규칙. 로그인은 제공자 버튼이 주 행동이라 기본 `none`, 가입 닉네임 단계처럼 입력이 주 행동인 `main`만 `first-input`. 닉네임 단계가 같은 화면에서 나중에 나타나면 그 칸에 `initialFocusTarget`을 준다 |
83
- | Native `testID` | `string` | — | 뿌리 `ScrollView` 식별 |
88
+ | Native `testID` | `string` | — | 본문 `ScrollView` 식별(머리 띠가 있어도 같은 `ScrollView`) |
84
89
  | Native `safeAreaBottom` | 0 이상의 유한수 | `0` | 화면이 닿는 창 아래 가장자리의 safe area(`useSafeAreaInsets().bottom`, 이미 아래를 들인 host 안이면 0). 아래 여백이 `max(paddingBlock, inset)` — `regular` max(40, inset), `compact` max(20, inset) — 으로 한 번만 들어간다(ScreenLayout과 같은 규칙, 합산 없음). 키보드가 열리면 paddingBlock만. 음수·무한대는 `RangeError` |
85
90
  | `AuthScreenHero` `title` · `eyebrow` · `description` · `mark` · `headingLevel` | 문자열 · `ReactNode` · `1`~`3` | `headingLevel` 1 | 제목은 필수이고 빈 문자열이면 `TypeError`. 밀도는 바깥 `AuthScreenLayout`을 따른다 |
86
91
  | `AuthScreenFooter` `links` · `notice` · `children` | 링크 배열(1개 이상) · `ReactNode` | — | 빈 링크 목록·빈 라벨은 `TypeError`(LS-04: 약관 링크는 로그인 전 같은 화면에 있어야 한다). `children`은 링크 아래 제품 내용 |
@@ -95,8 +100,9 @@ import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react
95
100
  | 간격 | `regular` / `compact`: 바깥 좌우 `layout.pagePadding.regular` 20 / `.compact` 16, 바깥 위아래 `spacing.xxxl` 40 / `spacing.lg` 20, hero 안 `spacing.md` 16 / `spacing.xs` 8, hero↔main `spacing.xl` 24 / `spacing.md` 16, 위 블록↔footer 최소 `spacing.xl` 24 / `spacing.md` 16. mainCard 안쪽 `spacing.md` 16 | `authScreenRecipe.densities`·`mainCard` |
96
101
  | 히어로 문구 | 마크 ↔ 문구 묶음은 hero 간격. eyebrow `label` 12/18·500·브랜드색, 아래 `spacing.xxs` 4 → 태그라인 `regular` `typography.heading` 24/32 · `compact` `titleLarge` 20/28, 굵기는 제목 단계 500, 기본 글자색, 실제 heading 요소(Native `header`) → 설명 `body` 14/20·400·보조색(`muted`), 위 `spacing.xs` 8 | `authScreenRecipe.hero`, `densities.*.titleVariant` |
97
102
  | 동의 고지·정책 링크 | 고지 `caption` 11/16 흐린 색, 링크 줄과 `spacing.xxs` 4. 링크 `caption`·브랜드색·밑줄, 링크 사이 `linkGap` 8 + `·` + 8(보이는 글자 간격 약 19). 각 링크 높이 44(배경 없음), 가로 터치 폭은 구분점 앞까지 보이지 않게 넓힘(Web 가상 요소, Native `hitSlop`) | `authScreenRecipe.footer`, `.hjm-auth-screen__link` |
98
- | 순서·정렬 | 위→아래 hero(가운데 정렬 문구) → main(폭 꽉 채움) → footer(동의 고지·정책 링크). 위 블록은 남는 세로 공간에서 가운데, footer는 바닥에 붙는다. 진행 중 로딩은 main 카드 가운데에 겹친다 | `.hjm-auth-screen__block`(`justify-content: center`), `.hjm-auth-screen__pending` |
99
- | 고정·스크롤 | 고정 영역이 없다. 넘치면 화면 전체가 스크롤한다. 위 safe area는 host(TopBar·SafeAreaView top)가, 아래는 레이아웃이 `max(paddingBlock, 하단 inset)`으로 맡는다 — Web `env(safe-area-inset-bottom)`, Native `safeAreaBottom`. Native 키보드 inset은 자동 조정이 처리한다 | `react-native/src/auth-screen.tsx`, `.hjm-auth-screen` |
103
+ | 머리 띠(`title`·`exit`) | ScreenLayout 가운데 머리와 같다: 최소 높이 52(`screenHeaderRecipe.minHeight`), 위아래 `spacing.xxs` 4, 좌우 화면 gutter `spacing.md` 16(`screenPatternRecipe.padding`), 시작 끝 `exit`, 가운데 제목 `level5`(양옆 같은 폭 칸). 화면 가장자리까지 닿고(바깥 좌우 여백 밖), 원래 바깥 위 여백(`regular` 40 / `compact` 20)은 띠 아래로 옮겨진다. 스크롤하지 않는다(Native는 `ScrollView` 밖) | `screenHeaderRecipe`, `.hjm-auth-screen__header`, Native `AuthScreenHeader` |
104
+ | 순서·정렬 | 위→아래 (머리 띠) → hero(가운데 정렬 문구) → main(폭 꽉 채움) → footer(동의 고지·정책 링크). 위 블록은 남는 세로 공간에서 가운데, footer는 바닥에 붙는다. 진행 중 로딩은 main 카드 가운데에 겹친다 | `.hjm-auth-screen__block`(`justify-content: center`), `.hjm-auth-screen__pending` |
105
+ | 고정·스크롤 | 머리 띠만 고정이고 나머지는 넘치면 함께 스크롤한다. 위 safe area는 host(SafeAreaView top 등, ScreenLayout과 같다)가, 아래는 레이아웃이 `max(paddingBlock, 하단 inset)`으로 맡는다 — Web `env(safe-area-inset-bottom)`, Native `safeAreaBottom`. Native 키보드 inset은 자동 조정이 처리한다 | `react-native/src/auth-screen.tsx`, `.hjm-auth-screen` |
100
106
  | 좁은 폭·큰 글자 | 키 작은 화면·큰 글자에서는 `compact`를 고른다. 그래도 넘치면 스크롤되며 footer는 맨 아래로 밀린다 | `authScreenRecipe.densities` |
101
107
 
102
108
  ```text
@@ -136,7 +142,8 @@ import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react
136
142
 
137
143
  | 항목 | Web | Native |
138
144
  | --- | --- | --- |
139
- | 뿌리 요소 | `<main>` 또는 `as="section"`, ref 전달 | `ScrollView` |
145
+ | 뿌리 요소 | `<main>` 또는 `as="section"`, ref 전달. 머리 띠는 그 안의 `<header>` | `ScrollView`. `title`·`exit`가 있으면 `View`(머리 띠 + `ScrollView`)가 뿌리이고 `layoutStyle`은 그 `View`에 간다 |
146
+ | 머리 띠 위 safe area | 다루지 않는다(브라우저·host) | host가 SafeAreaView top으로 준다. TopBar `safeAreaTop`에 해당하는 prop은 없다(ScreenLayout과 같다) |
140
147
  | 배치·식별 | `layoutStyle`, `className`, HTML 속성 | `layoutStyle`, `testID` |
141
148
  | 진행 중 숨김 | `inert` + `aria-hidden` + `visibility: hidden`, 로딩은 `role="status"` | 터치·접근성 제외 + 불투명도 0, 로딩은 `progressbar` + live region |
142
149
  | 키보드 | 브라우저 기본 | 키보드 inset 자동 조정, 탭 유지(`handled`), iOS `interactive`·Android `on-drag` 내리기 |
@@ -145,6 +152,8 @@ import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react
145
152
 
146
153
  ## 함정
147
154
 
155
+ - `title`·`exit`를 쓰면 바깥에 얹던 TopBar·제품 헤더를 지운다. 둘 다 두면 머리가 두 줄이 된다. Native에서 host TopBar가 맡던 위 safe area(`safeAreaTop`)는 host의 SafeAreaView top으로 옮긴다.
156
+
148
157
  - Native에서 아래 홈 인디케이터를 `SafeAreaView edges={['bottom']}`·`paddingBottom: insets.bottom` 래퍼로 들이지 말고 `safeAreaBottom`으로 넘긴다. 둘을 같이 주면 inset + paddingBlock이 다시 더해진다.
149
158
  - 1.28.0까지 Native는 iOS `contentInsetAdjustmentBehavior="automatic"`으로 위·아래 safe area를 iOS에서만 더했다. 1.28.1부터 위는 host, 아래는 `safeAreaBottom`이다. 위 safe area를 아무도 들이지 않던 화면은 host에서 TopBar `safeAreaTop`이나 SafeAreaView top으로 들인다.
150
159
  - Native는 자체로 키보드 스크롤 여백을 준다. 부모가 다시 keyboard avoidance를 하면 여백이 중복될 수 있으니
@@ -159,6 +168,7 @@ import { AuthScreenFooter, AuthScreenHero, AuthScreenLayout } from "@hjmds/react
159
168
 
160
169
  ## 근거
161
170
 
171
+ - `title`·`exit`(1.31.0): 2026-10-18 앱 감사에서 utilverse·burntok 로그인이 "로그인" 제목과 뒤로/닫기만을 위해 레이아웃 위에 TopBar(burntok은 제품 `AppScreenHeader` 래퍼)를 따로 얹어, 로그인만 다른 머리 높이·제목 단계를 가졌다. 앱마다 띠를 조립하지 않도록 ScreenLayout의 `exit` 버튼과 가운데 머리 recipe를 그대로 쓰는 띠를 이 레이아웃이 소유한다. Native에서 TopBar를 가져오면 `navigation` 그래프 전체가 로그인 subpath에 들어오므로(ScreenLayout이 TopBar를 쓰지 않는 같은 이유) 띠는 `screenHeaderRecipe`로 그린다.
162
172
  - `safeAreaBottom`·링크 `target`/`rel`/`accessibilityHint`(1.28.1): 2026-10-17 1.28 채택에서 다에리·utilverse·spint가 레이아웃을 각자 아래 inset 래퍼로 감쌌고(iOS는 자동 content inset이 이미 더해 합산, Android는 아무도 안 들여 spint가 Android만 덧붙임), 다에리 웹은 정책 문서 새 탭 열기(`target="_blank"`)를, spint는 "브라우저에서 열림" 접근성 안내를 링크 슬롯이 없어 잃었다. ScreenLayout의 max 규칙을 그대로 쓰고 링크에 같은 의미의 보조 안내·anchor 속성을 연다.
163
173
  - `AuthScreenHero`·`AuthScreenFooter`와 히어로·링크 수치: 2026-10-17 사용자 피드백("굵기랑 크기 줄여", "간격 줄여").
164
174
  예제가 히어로에 `Top large`(display 32/40·600)를 넣어 402 폭에서 태그라인이 두 줄이 됐고, 정책 링크는 ghost Button
@@ -63,6 +63,7 @@ const insets = useSafeAreaInsets();
63
63
  | prop | 값 | 기본값 | 설명 |
64
64
  | --- | --- | --- | --- |
65
65
  | `primaryAction` | Web `{ label, onClick, accessibilityLabel?, disabled?, loading?, loadingLabel?, size?, tone? }` · Native `{ label, onPress, accessibilityLabel?, accessibilityHint?, disabled?, loading?, loadingLabel?, size?, tone? }` | 필수, tone `primary`, size `medium` | Web `onClick: (event: MouseEvent<HTMLButtonElement>) => void`, Native `onPress: (event: GestureResponderEvent) => void` |
66
+ | action `buttonRef` | Web `Ref<HTMLButtonElement>` · Native `Ref<View>` | — | 미게시. 그 행동의 버튼 자체를 가리킨다(컴포넌트 `ref`는 Web에서 바 `div`). 이 행동으로 연 Dialog·Sheet·`ImmersiveOverlay`가 닫힐 때 포커스를 돌려줄 곳으로 Native `returnFocusRef`에 넘긴다. Web Dialog는 연 요소로 스스로 돌아가므로 필요할 때만 쓴다 |
66
67
  | `secondaryAction` | 같은 action 객체 또는 제품이 만든 `ReactNode` | tone `secondary` | 객체면 HJM Button으로 그린다. Web은 `label`과 `onClick`이 있어야 객체로 본다 |
67
68
  | `description` | `string` | — | 행동 위 caption 한 줄 |
68
69
  | `accessibilityLabel` | `string` | — | 바 전체(Web `role="group"`, Native `toolbar`)의 이름 |
@@ -94,6 +95,15 @@ const insets = useSafeAreaInsets();
94
95
  큰 글자: [ 주 행동 ] / [ 보조 ] (주 행동 위)
95
96
  ```
96
97
 
98
+ ### 닫힌 뒤 여는 버튼으로 포커스 돌리기
99
+
100
+ ```tsx
101
+ // Native — ScreenLayout footer의 BottomCTA로 연 전체 화면 오버레이
102
+ const startRef = useRef<View>(null);
103
+ <ScreenLayout footer={<BottomCTA primaryAction={{ label: t("display.start"), onPress: open, buttonRef: startRef }} />}>…</ScreenLayout>
104
+ <ImmersiveOverlay open={presenting} onOpenChange={setPresenting} returnFocusRef={startRef} label={t("display.title")} exitLabel={t("common.close")}>…</ImmersiveOverlay>
105
+ ```
106
+
97
107
  ## 꼭 지킬 것
98
108
 
99
109
  - action `label`은 i18n 키로 넣고 비우지 않는다. Web은 빈 label에 `TypeError`를 던진다.
@@ -129,4 +139,5 @@ const insets = useSafeAreaInsets();
129
139
 
130
140
  ## 근거
131
141
 
142
+ - action `buttonRef`: 2026-10-18 유틸버스 전광판 — footer의 BottomCTA 주 행동으로 연 `ImmersiveOverlay`를 닫아도 Native는 명시한 `returnFocusRef`로만 포커스를 돌리는데, BottomCTA가 버튼 ref를 내주지 않아 포커스가 화면 처음으로 갔다.
132
143
  - footer 안 하단 inset은 합이 아니라 max: 2026-10-17 사용자 보고 "전체적으로 버튼 아래 여백이 많아". footer 16 위에 `safeAreaBottom`을 더해 홈 인디케이터 기기에서 버튼 아래가 50이 됐다. 마지막 컨트롤 아래 여백을 footer 위 여백 16과 같게 두고, 홈 인디케이터가 있으면 max(16, inset)만 둔다([ScreenLayout 근거](screen-layout.md#근거)).