@hjmds/design-contracts 1.41.2 → 1.43.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 (150) hide show
  1. package/dist/affix.d.ts +18 -0
  2. package/dist/affix.d.ts.map +1 -1
  3. package/dist/affix.js +13 -1
  4. package/dist/affix.js.map +1 -1
  5. package/dist/alert-dialog.d.ts +32 -0
  6. package/dist/alert-dialog.d.ts.map +1 -1
  7. package/dist/alert-dialog.js +52 -0
  8. package/dist/alert-dialog.js.map +1 -1
  9. package/dist/behaviors.d.ts +6 -5
  10. package/dist/behaviors.d.ts.map +1 -1
  11. package/dist/behaviors.js +1 -1
  12. package/dist/behaviors.js.map +1 -1
  13. package/dist/catalog.d.ts +168 -15
  14. package/dist/catalog.d.ts.map +1 -1
  15. package/dist/catalog.js +4 -4
  16. package/dist/catalog.js.map +1 -1
  17. package/dist/character-count.d.ts +103 -0
  18. package/dist/character-count.d.ts.map +1 -0
  19. package/dist/character-count.js +68 -0
  20. package/dist/character-count.js.map +1 -0
  21. package/dist/chart.d.ts +30 -0
  22. package/dist/chart.d.ts.map +1 -1
  23. package/dist/chart.js +18 -0
  24. package/dist/chart.js.map +1 -1
  25. package/dist/collection-rail.d.ts +13 -1
  26. package/dist/collection-rail.d.ts.map +1 -1
  27. package/dist/collection-rail.js +9 -2
  28. package/dist/collection-rail.js.map +1 -1
  29. package/dist/communication.d.ts +25 -0
  30. package/dist/communication.d.ts.map +1 -1
  31. package/dist/communication.js +3 -0
  32. package/dist/communication.js.map +1 -1
  33. package/dist/component-recipes.d.ts +172 -2
  34. package/dist/component-recipes.d.ts.map +1 -1
  35. package/dist/component-recipes.js +122 -8
  36. package/dist/component-recipes.js.map +1 -1
  37. package/dist/design-system-provider.d.ts +13 -2
  38. package/dist/design-system-provider.d.ts.map +1 -1
  39. package/dist/design-system-provider.js +19 -3
  40. package/dist/design-system-provider.js.map +1 -1
  41. package/dist/foundations.d.ts +15 -0
  42. package/dist/foundations.d.ts.map +1 -1
  43. package/dist/foundations.js +7 -0
  44. package/dist/foundations.js.map +1 -1
  45. package/dist/icon-button-recipe.d.ts +33 -0
  46. package/dist/icon-button-recipe.d.ts.map +1 -1
  47. package/dist/icon-button-recipe.js +36 -1
  48. package/dist/icon-button-recipe.js.map +1 -1
  49. package/dist/image.d.ts +13 -0
  50. package/dist/image.d.ts.map +1 -1
  51. package/dist/image.js +17 -0
  52. package/dist/image.js.map +1 -1
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +2 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/recipes.d.ts +1 -1
  58. package/dist/recipes.d.ts.map +1 -1
  59. package/dist/recipes.js +1 -1
  60. package/dist/recipes.js.map +1 -1
  61. package/dist/screen-patterns.d.ts +95 -0
  62. package/dist/screen-patterns.d.ts.map +1 -1
  63. package/dist/screen-patterns.js +83 -2
  64. package/dist/screen-patterns.js.map +1 -1
  65. package/dist/selection-helpers.d.ts +6 -0
  66. package/dist/selection-helpers.d.ts.map +1 -1
  67. package/dist/selection-helpers.js +12 -0
  68. package/dist/selection-helpers.js.map +1 -1
  69. package/dist/sidebar.d.ts +17 -2
  70. package/dist/sidebar.d.ts.map +1 -1
  71. package/dist/sidebar.js +8 -2
  72. package/dist/sidebar.js.map +1 -1
  73. package/dist/steps.d.ts +19 -19
  74. package/dist/steps.d.ts.map +1 -1
  75. package/dist/steps.js +21 -9
  76. package/dist/steps.js.map +1 -1
  77. package/dist/timeline.d.ts +41 -0
  78. package/dist/timeline.d.ts.map +1 -1
  79. package/dist/timeline.js +47 -1
  80. package/dist/timeline.js.map +1 -1
  81. package/dist/top.d.ts +21 -2
  82. package/dist/top.d.ts.map +1 -1
  83. package/dist/top.js +17 -1
  84. package/dist/top.js.map +1 -1
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/dist/version.js.map +1 -1
  88. package/docs/affix.md +7 -0
  89. package/docs/avatar-fallback.md +19 -0
  90. package/docs/brand-boundary.md +24 -2
  91. package/docs/chart.md +7 -0
  92. package/docs/collection-rail.md +4 -1
  93. package/docs/generated/component-maturity.md +1 -1
  94. package/docs/generated/renderer-evidence.json +10 -6
  95. package/docs/generated/renderer-evidence.md +1 -1
  96. package/docs/generated/showcase-manifest.json +12 -5
  97. package/docs/image.md +5 -0
  98. package/docs/list-row.md +8 -0
  99. package/docs/optional-adapters.md +3 -0
  100. package/docs/overlay-stack.md +27 -0
  101. package/docs/screen-patterns.md +39 -2
  102. package/docs/sidebar.md +15 -0
  103. package/docs/steps.md +23 -19
  104. package/docs/theme-palette.md +3 -3
  105. package/docs/theming.md +1 -0
  106. package/docs/timeline.md +18 -1
  107. package/docs/top.md +7 -0
  108. package/docs/usage/README.md +9 -2
  109. package/docs/usage/components/affix.md +18 -4
  110. package/docs/usage/components/alert-dialog.md +28 -0
  111. package/docs/usage/components/avatar.md +30 -9
  112. package/docs/usage/components/badge.md +20 -5
  113. package/docs/usage/components/chart.md +19 -1
  114. package/docs/usage/components/chat-message.md +5 -0
  115. package/docs/usage/components/chat-room-actions.md +58 -0
  116. package/docs/usage/components/chat-room-create.md +58 -0
  117. package/docs/usage/components/chat-room-details.md +58 -0
  118. package/docs/usage/components/chat-room-invite.md +58 -0
  119. package/docs/usage/components/chat-room-members.md +58 -0
  120. package/docs/usage/components/chat-room-profile.md +58 -0
  121. package/docs/usage/components/checkbox-group.md +24 -1
  122. package/docs/usage/components/choice-row.md +19 -4
  123. package/docs/usage/components/composition-layout.md +6 -2
  124. package/docs/usage/components/counter-badge.md +2 -2
  125. package/docs/usage/components/design-system-provider.md +2 -1
  126. package/docs/usage/components/divider.md +12 -1
  127. package/docs/usage/components/field.md +51 -0
  128. package/docs/usage/components/icon-button.md +48 -4
  129. package/docs/usage/components/icon-tile.md +13 -2
  130. package/docs/usage/components/image.md +25 -0
  131. package/docs/usage/components/list-row.md +8 -4
  132. package/docs/usage/components/list.md +1 -1
  133. package/docs/usage/components/notification-item.md +5 -1
  134. package/docs/usage/components/permission-screen.md +25 -4
  135. package/docs/usage/components/screen-layout.md +27 -1
  136. package/docs/usage/components/segmented-control.md +16 -3
  137. package/docs/usage/components/select.md +1 -0
  138. package/docs/usage/components/settings-screen.md +5 -4
  139. package/docs/usage/components/sheet.md +2 -1
  140. package/docs/usage/components/sidebar.md +34 -6
  141. package/docs/usage/components/steps.md +29 -8
  142. package/docs/usage/components/surface.md +2 -1
  143. package/docs/usage/components/text-area.md +2 -1
  144. package/docs/usage/components/timeline.md +25 -4
  145. package/docs/usage/components/top.md +15 -2
  146. package/docs/usage/compositions/chat-room.md +110 -0
  147. package/docs/usage/screens/common-settings.md +3 -2
  148. package/docs/usage/screens/flow-permission.md +3 -3
  149. package/docs/usage/tokens/elevation-opacity.md +14 -2
  150. package/package.json +7 -1
@@ -0,0 +1,58 @@
1
+ # ChatRoomDetails
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.43.0
7
+ - 검토일: 2026-10-23
8
+ - 근거: 양 renderer `src/communication.tsx`와 [채팅방 비교·구성 지침](../compositions/chat-room.md)
9
+ - 스토리북: `배포/구성/입력과 작성/채팅방 관리`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 방 프로필·도구·참여자·관리 행동을 묶음형 정보 화면으로 보여 준다.
14
+
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 메시지 기록과 작성창만 필요한 화면 | [ChatScreen](chat-screen.md) |
20
+ | 제품의 서버·권한·저장 규칙 구현 | 제품 데이터 계층 |
21
+
22
+ ## 공개 이름과 import
23
+
24
+ | 이름 | 역할 | Web | Native |
25
+ | --- | --- | --- | --- |
26
+ | `ChatRoomDetails` | 방 프로필·도구·참여자·관리 행동을 묶음형 정보 화면으로 보여 준다. | `@hjmds/react/communication` | `@hjmds/react-native/communication` |
27
+
28
+ ## 최소 사용 예
29
+
30
+ ```tsx
31
+ // Web
32
+ import { ChatRoomDetails } from "@hjmds/react/communication";
33
+ <ChatRoomDetails profile={room} photoLabel="채팅방 사진" memberCountLabel={memberCountLabel} members={memberListProps} errorLabel="작업을 완료하지 못했어요." />
34
+ ```
35
+
36
+ ```tsx
37
+ // Native
38
+ import { ChatRoomDetails } from "@hjmds/react-native/communication";
39
+ <ChatRoomDetails profile={room} photoLabel="채팅방 사진" memberCountLabel={memberCountLabel} members={memberListProps} errorLabel="작업을 완료하지 못했어요." />
40
+ ```
41
+
42
+ Native는 동일 prop 계약으로 `@hjmds/react-native/communication`에서 가져온다. 전체 실행 예제와 연결 props는 [채팅방 관리](../compositions/chat-room.md)의 양 플랫폼 예제를 따른다.
43
+
44
+ ## 배치
45
+
46
+ | 항목 | 값 | 근거 |
47
+ | --- | --- | --- |
48
+ | 크기 | 부모 본문 폭, 프로필 Avatar 64·참여자 40 | 기존 Avatar·ListRow recipe |
49
+ | 간격 | 내부 sm·입력 md·그룹 lg | 양 renderer 공통 구성 |
50
+ | 순서·정렬 | 상단 탐색·확인 → 작업 본문 → 오류 | 구성 소스 |
51
+ | 고정·스크롤 | 스크롤·키보드 처리는 host 소유 | 웹·Native의 화면 host 계약 |
52
+ | 좁은 폭 | 도구 동폭, 이름·설명 줄바꿈 | 공통 레이아웃 |
53
+
54
+ ## 꼭 지킬 것
55
+
56
+ profile·photoLabel·memberCountLabel·members·errorLabel이 필수다. edit·tools·actions는 권한과 제품 경로가 있는 경우 전달한다.
57
+
58
+ 데이터·문구·권한은 제품이 소유한다. 저장·개설·초대·삭제는 실제 성공 뒤 Promise를 resolve하고 실패는 reject한다. 입력과 선택은 실패 시 유지한다. Storybook의 메모리 예제를 운영 저장으로 사용하지 않는다. 방 전환 시 `key={roomId}`로 작업 수명을 구분한다.
@@ -0,0 +1,58 @@
1
+ # ChatRoomInvite
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.43.0
7
+ - 검토일: 2026-10-23
8
+ - 근거: 양 renderer `src/communication.tsx`와 [채팅방 비교·구성 지침](../compositions/chat-room.md)
9
+ - 스토리북: `배포/구성/입력과 작성/채팅방 관리`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 선택한 새 참여자의 실제 초대 결과를 기다린다.
14
+
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 메시지 기록과 작성창만 필요한 화면 | [ChatScreen](chat-screen.md) |
20
+ | 제품의 서버·권한·저장 규칙 구현 | 제품 데이터 계층 |
21
+
22
+ ## 공개 이름과 import
23
+
24
+ | 이름 | 역할 | Web | Native |
25
+ | --- | --- | --- | --- |
26
+ | `ChatRoomInvite` | 선택한 새 참여자의 실제 초대 결과를 기다린다. | `@hjmds/react/communication` | `@hjmds/react-native/communication` |
27
+
28
+ ## 최소 사용 예
29
+
30
+ ```tsx
31
+ // Web
32
+ import { ChatRoomInvite } from "@hjmds/react/communication";
33
+ <ChatRoomInvite {...memberSelectionProps} onInvite={inviteMembers} confirmLabel="초대하기" errorLabel="초대하지 못했어요." />
34
+ ```
35
+
36
+ ```tsx
37
+ // Native
38
+ import { ChatRoomInvite } from "@hjmds/react-native/communication";
39
+ <ChatRoomInvite {...memberSelectionProps} onInvite={inviteMembers} confirmLabel="초대하기" errorLabel="초대하지 못했어요." />
40
+ ```
41
+
42
+ Native는 동일 prop 계약으로 `@hjmds/react-native/communication`에서 가져온다. 전체 실행 예제와 연결 props는 [채팅방 관리](../compositions/chat-room.md)의 양 플랫폼 예제를 따른다.
43
+
44
+ ## 배치
45
+
46
+ | 항목 | 값 | 근거 |
47
+ | --- | --- | --- |
48
+ | 크기 | 부모 본문 폭, 프로필 Avatar 64·참여자 40 | 기존 Avatar·ListRow recipe |
49
+ | 간격 | 내부 sm·입력 md·그룹 lg | 양 renderer 공통 구성 |
50
+ | 순서·정렬 | 상단 탐색·확인 → 작업 본문 → 오류 | 구성 소스 |
51
+ | 고정·스크롤 | 스크롤·키보드 처리는 host 소유 | 웹·Native의 화면 host 계약 |
52
+ | 좁은 폭 | 도구 동폭, 이름·설명 줄바꿈 | 공통 레이아웃 |
53
+
54
+ ## 꼭 지킬 것
55
+
56
+ members·title·emptyLabel·onSelectionChange·onInvite·confirmLabel·errorLabel이 필수다. disabled 멤버는 요청에서 제외한다.
57
+
58
+ 데이터·문구·권한은 제품이 소유한다. 저장·개설·초대·삭제는 실제 성공 뒤 Promise를 resolve하고 실패는 reject한다. 입력과 선택은 실패 시 유지한다. Storybook의 메모리 예제를 운영 저장으로 사용하지 않는다. 방 전환 시 `key={roomId}`로 작업 수명을 구분한다.
@@ -0,0 +1,58 @@
1
+ # ChatRoomMembers
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.43.0
7
+ - 검토일: 2026-10-23
8
+ - 근거: 양 renderer `src/communication.tsx`와 [채팅방 비교·구성 지침](../compositions/chat-room.md)
9
+ - 스토리북: `배포/구성/입력과 작성/채팅방 관리`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 참여자를 표시하고 선택 또는 권한별 메뉴를 제공한다.
14
+
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 메시지 기록과 작성창만 필요한 화면 | [ChatScreen](chat-screen.md) |
20
+ | 제품의 서버·권한·저장 규칙 구현 | 제품 데이터 계층 |
21
+
22
+ ## 공개 이름과 import
23
+
24
+ | 이름 | 역할 | Web | Native |
25
+ | --- | --- | --- | --- |
26
+ | `ChatRoomMembers` | 참여자를 표시하고 선택 또는 권한별 메뉴를 제공한다. | `@hjmds/react/communication` | `@hjmds/react-native/communication` |
27
+
28
+ ## 최소 사용 예
29
+
30
+ ```tsx
31
+ // Web
32
+ import { ChatRoomMembers } from "@hjmds/react/communication";
33
+ <ChatRoomMembers members={members} title="참여자" emptyLabel="참여자가 없습니다." />
34
+ ```
35
+
36
+ ```tsx
37
+ // Native
38
+ import { ChatRoomMembers } from "@hjmds/react-native/communication";
39
+ <ChatRoomMembers members={members} title="참여자" emptyLabel="참여자가 없습니다." />
40
+ ```
41
+
42
+ Native는 동일 prop 계약으로 `@hjmds/react-native/communication`에서 가져온다. 전체 실행 예제와 연결 props는 [채팅방 관리](../compositions/chat-room.md)의 양 플랫폼 예제를 따른다.
43
+
44
+ ## 배치
45
+
46
+ | 항목 | 값 | 근거 |
47
+ | --- | --- | --- |
48
+ | 크기 | 부모 본문 폭, 프로필 Avatar 64·참여자 40 | 기존 Avatar·ListRow recipe |
49
+ | 간격 | 내부 sm·입력 md·그룹 lg | 양 renderer 공통 구성 |
50
+ | 순서·정렬 | 상단 탐색·확인 → 작업 본문 → 오류 | 구성 소스 |
51
+ | 고정·스크롤 | 스크롤·키보드 처리는 host 소유 | 웹·Native의 화면 host 계약 |
52
+ | 좁은 폭 | 도구 동폭, 이름·설명 줄바꿈 | 공통 레이아웃 |
53
+
54
+ ## 꼭 지킬 것
55
+
56
+ members·title·emptyLabel이 필수다. onSelectionChange가 있으면 선택 목록이다. actions에는 허용된 행동만 전달한다.
57
+
58
+ 데이터·문구·권한은 제품이 소유한다. 저장·개설·초대·삭제는 실제 성공 뒤 Promise를 resolve하고 실패는 reject한다. 입력과 선택은 실패 시 유지한다. Storybook의 메모리 예제를 운영 저장으로 사용하지 않는다. 방 전환 시 `key={roomId}`로 작업 수명을 구분한다.
@@ -0,0 +1,58 @@
1
+ # ChatRoomProfile
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.43.0
7
+ - 검토일: 2026-10-23
8
+ - 근거: 양 renderer `src/communication.tsx`와 [채팅방 비교·구성 지침](../compositions/chat-room.md)
9
+ - 스토리북: `배포/구성/입력과 작성/채팅방 관리`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 사진·이름·소개를 편집하고 실제 저장 결과를 기다린다.
14
+
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 메시지 기록과 작성창만 필요한 화면 | [ChatScreen](chat-screen.md) |
20
+ | 제품의 서버·권한·저장 규칙 구현 | 제품 데이터 계층 |
21
+
22
+ ## 공개 이름과 import
23
+
24
+ | 이름 | 역할 | Web | Native |
25
+ | --- | --- | --- | --- |
26
+ | `ChatRoomProfile` | 사진·이름·소개를 편집하고 실제 저장 결과를 기다린다. | `@hjmds/react/communication` | `@hjmds/react-native/communication` |
27
+
28
+ ## 최소 사용 예
29
+
30
+ ```tsx
31
+ // Web
32
+ import { ChatRoomProfile } from "@hjmds/react/communication";
33
+ <ChatRoomProfile value={draft} onValueChange={setDraft} onSave={saveProfile} labels={labels} />
34
+ ```
35
+
36
+ ```tsx
37
+ // Native
38
+ import { ChatRoomProfile } from "@hjmds/react-native/communication";
39
+ <ChatRoomProfile value={draft} onValueChange={setDraft} onSave={saveProfile} labels={labels} />
40
+ ```
41
+
42
+ Native는 동일 prop 계약으로 `@hjmds/react-native/communication`에서 가져온다. 전체 실행 예제와 연결 props는 [채팅방 관리](../compositions/chat-room.md)의 양 플랫폼 예제를 따른다.
43
+
44
+ ## 배치
45
+
46
+ | 항목 | 값 | 근거 |
47
+ | --- | --- | --- |
48
+ | 크기 | 부모 본문 폭, 프로필 Avatar 64·참여자 40 | 기존 Avatar·ListRow recipe |
49
+ | 간격 | 내부 sm·입력 md·그룹 lg | 양 renderer 공통 구성 |
50
+ | 순서·정렬 | 상단 탐색·확인 → 작업 본문 → 오류 | 구성 소스 |
51
+ | 고정·스크롤 | 스크롤·키보드 처리는 host 소유 | 웹·Native의 화면 host 계약 |
52
+ | 좁은 폭 | 도구 동폭, 이름·설명 줄바꿈 | 공통 레이아웃 |
53
+
54
+ ## 꼭 지킬 것
55
+
56
+ value·onValueChange·onSave·labels가 필수다. onPickPhoto는 업로드한 URL을 반환하며 null은 취소다.
57
+
58
+ 데이터·문구·권한은 제품이 소유한다. 저장·개설·초대·삭제는 실제 성공 뒤 Promise를 resolve하고 실패는 reject한다. 입력과 선택은 실패 시 유지한다. Storybook의 메모리 예제를 운영 저장으로 사용하지 않는다. 방 전환 시 `key={roomId}`로 작업 수명을 구분한다.
@@ -69,6 +69,7 @@ import { CheckboxGroup } from "@hjmds/react-native/inputs";
69
69
  | `label` · `accessibilityLabel` | `string` | — | 둘 중 하나는 필수 |
70
70
  | `orientation` | `vertical` · `horizontal` | `vertical` | — |
71
71
  | `presentation` | `card` · `plain` · `grouped` · `chips` | `card` | — |
72
+ | `overflow` | `wrap` · `scroll` | `wrap` | 미게시. `chips`의 줄 넘침. `scroll`은 한 줄 가로 스크롤([칩 한 줄 스크롤](#칩-한-줄-스크롤-미게시)). `chips`가 아니거나 `grid`와 함께 쓰면 `TypeError` |
72
73
  | `size` | `medium` · `small` | `medium` | — |
73
74
  | `renderLeading` | Web `(item, appearance: { selected, color: "currentColor", size }) => ReactNode` · Native `(item, props: { checked, selected, disabled, readOnly, color, size }) => ReactNode` | — | 항목 아이콘 |
74
75
  | `layoutStyle` | margin·width·flex·`alignSelf` | — | 그룹 외곽(Web `fieldset`) 배치 |
@@ -80,7 +81,7 @@ import { CheckboxGroup } from "@hjmds/react-native/inputs";
80
81
  | 크기 | 폭을 채우는 묶음. 각 행은 [Checkbox](checkbox.md) 규격(최소 높이 `medium` 56 · `small` 44, 표시 24 · 20) | `selectionControlRecipe.sizes`, `.hjm-choice` |
81
82
  | 간격 | 그룹 이름(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
83
  | 순서·정렬 | 위→아래 [그룹 이름(medium 500)] → [설명] → [항목들] → [오류]. 항목은 시작 쪽 정렬. `grouped`는 항목들을 테두리 1px·`radius.lg` 16 카드 하나에 붙여 담는다 | `selectionGroupRecipe.slots`, `.hjm-checkbox-group[data-presentation="grouped"]` |
83
- | 고정·스크롤 | 고정 영역이 없다. 목록이 길어도 그룹 안에서 스크롤 영역을 만들지 않는다 | `.hjm-checkbox-group__items` |
84
+ | 고정·스크롤 | 고정 영역이 없다. 세로 목록은 그룹 안에서 스크롤 영역을 만들지 않는다. `chips` + `overflow="scroll"`만 가로 한 줄 스크롤이다 | `.hjm-checkbox-group__items`, `.hjm-checkbox-group__scroller` |
84
85
  | 좁은 폭·큰 글자 | `horizontal`은 줄바꿈된다(`flex-wrap: wrap`). 좁은 폭·큰 글자에서는 `vertical`을 쓴다 | `.hjm-checkbox-group[data-orientation="horizontal"]` |
85
86
 
86
87
  ## 꼭 지킬 것
@@ -136,3 +137,25 @@ Chip/직접 wrap으로 조립하고 있었다. `presentation="chips"`는 기존
136
137
  ### 같은 행의 카드 높이 (checkout 수정·미게시)
137
138
 
138
139
  `presentation="card"` 항목은 같은 Grid 행에서 가장 높은 콘텐츠에 맞춰 칸을 채운다. Native의 Grid 칸 안에서 카드가 내용 높이만 사용하던 문제를 공통 ChoiceRow에서 수정했다(2026-10-20 다에리 공개 범위 선택 카드 지적). 제품에서 높이를 고정하거나 설명을 줄여 맞추지 않는다. 행별 높이는 해당 행의 콘텐츠로 결정한다.
140
+
141
+ ### 칩 한 줄 스크롤 (미게시)
142
+
143
+ `presentation="chips" overflow="scroll"`은 여러 개를 고르는 칩을 한 줄에 두고 가로로 스크롤한다.
144
+ 단일 선택 [SegmentedControl](segmented-control.md)의 `overflow="scroll"`과 같은 뜻이며, 다중 선택 의미(체크박스·`Set` 값)는 그대로다.
145
+
146
+ ```tsx
147
+ <CheckboxGroup accessibilityLabel={t("filter.teams")} presentation="chips" size="small" overflow="scroll"
148
+ items={teams.map((team) => ({ id: team, label: team }))} value={selected} onValueChange={setSelected}
149
+ renderLeading={(item) => <TeamBadge team={item.id} size="small" />} />
150
+ ```
151
+
152
+ | 항목 | 값 | 근거 |
153
+ | --- | --- | --- |
154
+ | 줄 | 줄바꿈 없이 한 줄, 칩 사이 `spacing.xs` 8. 칩은 줄어들지 않는다 | `selectionGroupRecipe.orientations.*.gap.chips` |
155
+ | 끝 여백 | 스크롤 영역 안쪽 사방 `selectionGroupRecipe.overflow.scrollInset` `spacing.xxs` 4, 바깥은 같은 값만큼 당겨 첫 칩이 열 가장자리에 맞는다. 마지막 칩 뒤에도 같은 4가 남는다 | `.hjm-checkbox-group__scroller`, Native `ScrollView` padding/margin |
156
+ | 키보드·포커스 | Web은 Tab으로 칩마다 이동하고 Space로 켜고 끈다. 포커스한 칩이 가려져 있으면 최소 거리만 스크롤해 inset까지 보인다. 4는 포커스 테두리(2 + 간격 2)와 작은 칩의 터치 여유(4)가 잘리지 않는 값이다 | `resolveScrollRevealOffset`(behaviors) |
157
+ | 처음 위치 | 시작 쪽에서 시작한다. 선택을 바꿔도 스크롤하지 않는다(여러 개를 켜는 동안 줄이 튀지 않게) | — |
158
+
159
+ SegmentedControl의 스크롤 노출 계산도 같은 `resolveScrollRevealOffset`(inset 0)을 쓴다.
160
+ 근거: 2026-10-23 야잘알 `TeamGameFilter.tsx`가 SegmentedControl(단일 선택)에만 있는 가로 스크롤을 쓰지 못해
161
+ 다중 선택 칩을 제품 `ScrollView`로 직접 감쌌다(`gap: 6`, `paddingRight: spacing.xs`).
@@ -14,6 +14,20 @@
14
14
  누르면 시트에서 고른다. 저장이 비동기면 시트가 저장 중으로 잠기고, 실패하면 시트를 연 채 오류를 보여 준다.
15
15
  행에는 제품이 저장을 확인한 값만 보이므로, 실패한 선택이 저장된 것처럼 남지 않는다.
16
16
 
17
+ ### 설정 값 선택의 기준
18
+
19
+ 2026-10-23 정리: ChoiceRow 지침은 화면 테마를 ChoiceRow 용도로 적었지만 SettingsScreen 지침과 두 플랫폼의 앱 설정 예제는
20
+ 테마·언어를 `Select` 또는 `ListRow`+`Sheet`+`RadioGroup`으로 안내했다. 그래서 선택의창은 ChoiceRow, Unairplane은 Sheet+RadioGroup을 써서
21
+ 같은 설정이 앱마다 갈렸다. ChoiceRow는 바로 이 반복(언어·테마 행을 제품마다 조립, 1.41.0 도입 근거)을 없애려고 만든 API이므로 기준을 하나로 둔다.
22
+
23
+ | 설정 | 쓸 것 |
24
+ | --- | --- |
25
+ | 설정 화면의 즉시 저장 한 값 선택(언어·화면 테마·기본 정렬·셸 테마) | ChoiceRow. 항목마다 그림이 필요하면 `ChoiceRowItem.leading` |
26
+ | 폼 안에서 다른 필드와 함께 제출하는 한 값 | [Select](select.md) 또는 [RadioGroup](radio-group.md) |
27
+ | 선택 외 입력·설명이 많은 편집 패널 | [Sheet](sheet.md)를 직접 구성 |
28
+
29
+ `ListRow`+`Sheet`+`RadioGroup`으로 같은 행과 시트를 다시 조립하지 않는다. 행의 확인된 값, 시트의 선택·저장 중·실패 유지는 ChoiceRow가 소유한다.
30
+
17
31
  ## 쓰지 않을 때
18
32
 
19
33
  | 상황 | 대신 쓸 것 |
@@ -53,11 +67,11 @@ import { ChoiceRow } from "@hjmds/react-native/choice-row";
53
67
  | prop | 값 | 기본값 | 설명 |
54
68
  | --- | --- | --- | --- |
55
69
  | `value` | 선택지 값 또는 null | 필수 | 제품이 확인한 값. null이면 `placeholder`가 필요하다 |
56
- | `items` | `ChoiceRowItem[]` | 필수 | 2개 이상, 값 중복 금지. `description`·`disabled` 지원 |
70
+ | `items` | `ChoiceRowItem[]` | 필수 | 2개 이상, 값 중복 금지. `description`·`disabled`·`leading`(항목 그림, 개발 소스·미게시) 지원 |
57
71
  | `onValueChange` | `(value) => void` 또는 `(value) => Promise<void>` | 필수 | 동기 저장은 시트를 바로 닫는다. Promise를 돌려주면 끝날 때까지 시트가 잠기고, 거부되면 시트를 연 채 `errorLabel`을 보인다 |
58
72
  | `errorLabel` | string | 비동기 저장일 때 필수 | 동기 저장에는 줄 수 없다. 시트 안 실패가 없기 때문이다(1.41.2) |
59
73
  | `sheetTitle`·`sheetDescription` | string | `title`·없음 | 시트 머리 |
60
- | `leading`·`trailing` | ReactNode | 없음 | 제품 아이콘·꼬리 표시. ListRow 슬롯 그대로 |
74
+ | `leading`·`trailing` | ReactNode | `leading`은 확인된 항목의 `leading`, `trailing`은 없음 | 제품 아이콘·꼬리 표시. ListRow 슬롯 그대로. 행 `leading`을 주면 항목 그림보다 우선한다 |
61
75
  | `density` | ListRow density | ListRow 기본값 | 주변 설정 행과 같은 밀도(예: `compact`)를 맞춘다 |
62
76
  | Native `accessibilityHint` | string | 없음 | 행을 눌렀을 때 일어날 일(예: 언어 목록 열기)을 제품 언어로 |
63
77
  | `disabled`·`loading` | boolean | false | ListRow의 비활성·자리 유지 로딩 |
@@ -66,9 +80,9 @@ import { ChoiceRow } from "@hjmds/react-native/choice-row";
66
80
 
67
81
  | 항목 | 값 | 근거 |
68
82
  | --- | --- | --- |
69
- | 크기 | ListRow 기본 높이와 터치 영역. 시트는 내용 높이(`size="auto"`) | [ListRow](list-row.md#배치), [Sheet](sheet.md#배치) |
83
+ | 크기 | ListRow 기본 높이와 터치 영역. 시트는 내용 높이(`size="auto"`). 항목 그림은 `choiceRowRecipe.itemLeading` 40×40 상자 가운데에 놓이고 넘치면 잘린다(ListRow leading과 같은 크기) | [ListRow](list-row.md#배치), [Sheet](sheet.md#배치), `choiceRowRecipe` |
70
84
  | 간격 | ListRow·RadioGroup recipe 간격을 그대로 쓴다 | 같은 recipe |
71
- | 순서·정렬 | 행: leading → 제목·현재 값(description) → trailing. 시트: 제목 → 설명 → 세로 선택지 → 오류 | [설정 화면](../screens/common-settings.md#버튼과-행동-위치) |
85
+ | 순서·정렬 | 행: leading → 제목·현재 값(description) → trailing. 시트: 제목 → 설명 → 세로 선택지(선택 표시 → 그림 → 이름·설명) → 오류. 그림이 있는 항목이 하나라도 있으면 그림 없는 항목도 빈 상자를 둬 이름 시작선을 맞춘다 | [설정 화면](../screens/common-settings.md#버튼과-행동-위치) |
72
86
  | 고정·스크롤 | 행은 바깥 화면 스크롤을 따른다. 선택지가 길면 시트 본문이 스크롤된다 | Sheet |
73
87
  | 좁은 폭·큰 글자 | 제목과 현재 값이 줄바꿈되며 행 높이가 늘어난다 | ListRow |
74
88
 
@@ -79,3 +93,4 @@ import { ChoiceRow } from "@hjmds/react-native/choice-row";
79
93
  - 저장 중에는 시트를 닫거나 다른 값을 고를 수 없다. 다시 고르면 이전 오류는 지워진다.
80
94
  - 행은 시트가 열렸는지를 낭독한다(Web `aria-expanded`·`aria-haspopup="dialog"`, Native `expanded`). 시트는 머리를 고정하고 선택지만 스크롤한다(1.41.1, utilverse 언어 행 이관에서 확인).
81
95
  - 문구·선택지·저장은 제품이 공급한다. 언어 목록이나 테마 적용 로직은 이 컴포넌트에 없다.
96
+ - 항목 그림(`leading`)은 장식이다. 보조기술은 항목 이름(`label`)만 읽으므로 그림에만 있는 정보를 두지 않는다. 크기·정렬을 제품 style로 다시 맞추지 않는다(2026-10-23 유틸버스 셸 테마 그림 때문에 ChoiceRow 대신 `ListRow`+`Sheet`+`RadioGroup`을 조립했던 사례).
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.23.0
7
- - 검토일: 2026-10-20
7
+ - 검토일: 2026-10-23
8
8
  - 근거: 양 renderer `src/screens.tsx`, [소유권](../SELECTION.md#소유권). 제품별 제목·배지·상태·하단 영역 재조립을 줄이기 위해 기존 Stack·상태 계약을 합성한다.
9
9
  - 스토리북: `배포/컴포넌트/레이아웃/구성 골격`, `배포/화면/설정/알림 설정`
10
10
 
@@ -62,6 +62,7 @@ import { CompositionLayout } from "@hjmds/react-native/screens";
62
62
  | footer | ReactNode | 없음 | 본문 다음 보조/주 행동; 고정은 바깥 ScreenLayout 소유 |
63
63
  | state | ScreenContentState | ready | 본문 교체; loading은 Spinner만 표시. empty 제목은 body(14), 오류·접근 제한 제목은 title |
64
64
  | stateAction | ReactNode | 없음 | 본문 상태와 중앙 정렬되는 복구 행동 |
65
+ | stateHeight | fit · compact · regular · large | fit | loading·empty·error·restricted가 본문을 대신할 때 그 자리에서 확보하는 최소 높이(`screenPatternRecipe.compositionState.minHeight`: compact 96 · regular 160 · large 220, fit은 상태 자체 높이). 부모가 높이를 주지 않아도 상태가 그 영역 가운데에 온다. ready 본문에는 적용하지 않는다. 개발 소스, 미게시 |
65
66
  | layoutStyle | HjmCompositionStyleProp | 없음 | 부모가 제공하는 영역 크기·배치 |
66
67
 
67
68
  ## 배치
@@ -75,7 +76,7 @@ footer
75
76
 
76
77
  | 항목 | 값 | 근거 |
77
78
  | --- | --- | --- |
78
- | 크기 | 부모의 가용 폭·높이 | 별도 고정 높이나 스크롤 없음 |
79
+ | 크기 | 부모의 가용 폭·높이. 상태 교체 영역만 `stateHeight` 단계의 최소 높이를 가진다 | 별도 고정 높이나 스크롤 없음 |
79
80
  | 제목·상태 문구 | 제목은 두 플랫폼 모두 `Heading level5`(문서 단계는 `headingLevel`). 상태 블록은 제목(`Text title`)·설명이 블록 줄로 가운데 정렬된다 | renderer. 2026-10-17 화면 검토: Native 제목이 작은 `Text title`이었고 Web 상태 문구가 한 줄로 이어져 왼쪽 정렬됐다 |
80
81
  | 간격 | 영역 spacing.md 16, 제목 행 spacing.sm 12, 배지 spacing.xs 8, 제목/설명 spacing.xxs 4 | Stack props |
81
82
  | 순서·정렬 | 머리글 → notice → 본문 → footer | renderer |
@@ -87,6 +88,7 @@ footer
87
88
  - 데이터·권한·초안·저장·배지 의미는 제품에서 연결한다. 전송 성공을 추정하거나 초안을 자동으로 지우지 않는다.
88
89
  - 본문 교체는 초기 조회에, 기존 입력을 유지하는 실패는 notice에 연결한다.
89
90
  - 중앙 상태는 이 구성에 주어진 영역을 기준으로 한다. 화면 높이를 임의 여백으로 흉내 내지 않는다.
91
+ - 부모 높이가 없는 구획(카드·패널·탭 본문)에서 상태 자리를 확보할 때는 `stateHeight`를 쓴다. `<View style={{ minHeight: N }}>`로 감싸고 `layoutStyle={{ flex: 1 }}`을 주는 조립을 하지 않는다. 단계 선택: 레일·인라인 저장 오류·공유 카드 행은 `compact`, 목록·표 구획 본문은 `regular`, 필드·차트 패널은 `large`, 한 줄 인라인 로딩은 `fit`.
90
92
  - 별도 스크롤을 중첩하지 않는다. 기본 구조를 children 안에서 다시 만들지 않는다.
91
93
 
92
94
  ## 플랫폼 차이
@@ -101,4 +103,6 @@ footer
101
103
  2026-10-19 사용자 지시: 앱·모바일 웹뷰의 큰 글씨를 지양하므로 구획 제목은 두 renderer 모두 18(level5)을 기본으로 쓴다.
102
104
  headingLevel은 문서 구조만 결정한다. 기존 24는 상한이며 일반 카드·선택 영역 제목의 기본값이 아니다.
103
105
 
106
+ `stateHeight` 근거: 2026-10-23 소비 감사. 야잘알이 QueryRegion·LeadersRail·MatchFieldPanel 등 13곳 이상에서 상태 영역을 `minHeight` 36·68·88·96·120·160·180·220 View로 감쌌다. 이 값들을 세 단계(68–96 → 96, 120–180 → 160, 220)로 묶어 recipe가 소유한다.
107
+
104
108
  `aside`는 제목·본문 묶음의 끝 쪽에 놓이고 footer는 카드 전체 폭을 사용한다. 지원 그림 자체의 크기·브랜드는 제품이 결정하며, 위치·본문의 가변 폭·footer 줄바꿈은 이 구성이 소유한다.
@@ -72,13 +72,13 @@ import { CounterBadge } from "@hjmds/react-native/data-display";
72
72
  | --- | --- | --- |
73
73
  | 크기 | `medium` 높이·최소 폭 20, `small` 높이·최소 폭 16, `dot` 8×8, 모두 `radius.full`. 자릿수가 늘면 폭만 늘어난다(`99+`). 터치 대상이 아니므로 누르는 것은 부모다 | `src/counter-badge-recipe.ts`, `packages/react/src/styles.css` `.hjm-counter-badge` |
74
74
  | 간격 | 좌우 여백 `medium` `spacing.xs` 8, `small` `spacing.xxs` 4. `floating` 테두리 `stroke.strong` 2가 아이콘과 배지를 떼어 보인다 | `src/counter-badge-recipe.ts` |
75
- | 순서·정렬 | `inline`: 탭 라벨·목록 행의 끝 쪽에 텍스트와 나란히 둔다. `floating`: 컴포넌트가 스스로 위치를 잡지 않으므로 부모를 relative로 두고 배지를 위·끝 모서리(`top: 0`, 끝 0)에 absolute로 놓는다. 공개된 `NotificationBell`(`/notification-bell`)이 IconButton 위에 이 배치를 그대로 쓴다 | `packages/react/src/notification-bell.tsx`, `packages/react-native/src/notification-bell.tsx` |
75
+ | 순서·정렬 | `inline`: 탭 라벨·목록 행의 끝 쪽에 텍스트와 나란히 둔다. `floating`: 컴포넌트가 스스로 위치를 잡지 않으므로 부모를 relative로 두고 배지를 위·끝 모서리에 absolute로 놓는다. 아이콘 위에 둘 때는 숫자 판이 아이콘을 덮지 않게 올린다. 공개된 `NotificationBell`(`/notification-bell`)은 `iconButtonRecipe.badge`(`small`, 끝 0, 아이콘 칸에 테두리 2만 겹치는 높이)를 쓴다 | `packages/react/src/notification-bell.tsx`, `packages/react-native/src/notification-bell.tsx` |
76
76
  | 고정·스크롤 | — | — |
77
77
  | 좁은 폭·큰 글자 | Web은 `flex-shrink: 0`이라 좁아져도 줄지 않는다 | `packages/react/src/styles.css` `.hjm-counter-badge` |
78
78
 
79
79
  ```text
80
80
  inline (행 끝) floating (아이콘 모서리)
81
- ┌─────────────────────────────┐ ┌──────┐(3) ← top 0, 끝 0
81
+ ┌─────────────────────────────┐ ┌──────┐(3) ← 끝 0, 테두리만 아이콘에 닿게 위로
82
82
  │ 받은 편지함 (12) │ │ 🔔 │
83
83
  └─────────────────────────────┘ └──────┘ IconButton 44×44
84
84
  ```
@@ -12,7 +12,7 @@
12
12
 
13
13
  앱 루트에 한 번 둔다. theme(light/dark/system)·방향·글자 배율·reduced motion을 해석하고, 제품 브랜드색을
14
14
  HJM semantic token 위에 얹는 **유일한 진입점**이다. Native HJM 컴포넌트는 테마를 이 Provider에서 읽으므로
15
- Provider 없이 렌더하면 예외가 난다. 화면 일부의 밀도·테마만 바꿀 때는 중첩 Provider를 둔다.
15
+ Provider 없이 렌더하면 예외가 난다. 화면 일부의 밀도·테마만 바꿀 때는 중첩 Provider를 둔다. 화면 일부의 바탕·표면 같은 몇 색 역할만 바꿀 때는 중첩 Provider에 그 역할만 `brandPalette`로 넘기고 `brandPaletteMode="extend"`를 준다. 제품에서 상위 덮어쓰기를 누적하는 scope 컴포넌트를 만들지 않는다.
16
16
 
17
17
  ## 쓰지 않을 때
18
18
 
@@ -86,6 +86,7 @@ expect(checkBrandPaletteContrast(PRODUCT_BRAND_PALETTE)).toEqual({ light: [], da
86
86
  | `reducedMotion` | `true` · `false` | `false` | — |
87
87
  | `minimumVisualTarget` | `true` · `false` | `false` | 작은 컨트롤의 표시 높이를 최소 44로 올리는 선택 기능. 2026-10-21 사용자 지시로 포트폴리오 앱·웹은 `false`를 사용한다. 터치 확장 `hitSlop`과 별개다 |
88
88
  | `brandPalette` | `{ light?, dark? }` | — | 각각 `ThemeColors` 20개 key 중 필요한 것만 넘긴다(부분 병합). 상태 강조색은 덮을 수 없다. 중첩 Provider는 가장 가까운 상위의 값을 물려받는다 |
89
+ | `brandPaletteMode` | `"replace"` · `"extend"` | `"replace"` | 중첩 Provider에서 `brandPalette`가 상위 팔레트를 대체할지(`replace`), theme별로 상위 위에 넘긴 역할만 덮을지(`extend`) 정한다. `brandPalette` 없이는 효과 없음(개발 소스·미게시, [브랜드 경계 §1.1](../../brand-boundary.md#11-하위-범위의-일부-역할만-바꾸기--brandpalettemode-개발-소스미게시)) |
89
90
  | `designProfile` | `HjmDesignProfile` | 가장 가까운 상위 프로필 또는 없음 | 기본 프로필·표면 질감은 1.15.0 추가. `hjmDesignPresets` 또는 `defineHjmDesignProfile` 결과만 넣는다. 글자 역할·관련 구성 개선은 1.16.0 변경. [프로필 계약](../../design-profile.md)의 토큰·질감·전환·구성·화면 기본값을 상속한다 |
90
91
  | `value` | `DesignSystemProviderValue`(`resolveDesignSystemProviderValue` 결과) | — | 테스트·스토리·임베딩용. 환경 prop·`brandPalette`·`designProfile`과 함께 쓸 수 없고(타입이 막는다), 주면 OS theme·모션 관찰과 상위 `brandPalette` 상속이 멈춘다 |
91
92
  | `surfaceEffects`(Native) | `{ renderBackdrop?, insetShadows? }` | 상위 host 또는 없음 | 1.15.0 실험 API. 실제 blur host·inset capability를 한 번 등록하며 `{}`로 하위 영역을 해제. [Surface 질감 계약](../../design-profile.md#surfacecard의-유리클레이-질감) |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-23
8
8
  - 근거: recipe `dividerRecipe`(`src/component-recipes.ts`)
9
9
  - 스토리북: `배포/컴포넌트/레이아웃/구분선`
10
10
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  서로 다른 내용 묶음 사이에 얇은 구분선이 필요할 때 쓴다. 가로선은 블록 사이, 세로선은
14
14
  한 줄 안의 항목 사이(예: 툴바의 무리 구분)에 쓴다. 색은 `border` semantic 역할이다.
15
+ 한 표면(카드) 안에서 "다른 이야기가 이어진다"는 가벼운 나눔은 `lineStyle="dashed"`다. 실선은 카드 하나를 둘로 읽히게 한다(개발 소스, 미게시).
15
16
 
16
17
  ## 쓰지 않을 때
17
18
 
@@ -54,6 +55,7 @@ import { Divider } from "@hjmds/react-native/data-display";
54
55
  | `orientation` | `horizontal` · `vertical` | `horizontal` | 가로선·세로선 |
55
56
  | `inset` | Web `none` · `start` · `both` / Native 숫자(pt, 토큰 값으로 넘긴다) | Web `none`(0) / Native 0 | Web `start`·`both`는 `spacing.md`. Native는 가로선이면 좌우, 세로선이면 위아래 여백이 된다 |
56
57
  | Web `decorative` | `boolean` | `false` | `true`면 `role="presentation"`·`aria-hidden` |
58
+ | `lineStyle` | `solid` · `dashed` | `solid` | `dashed`는 `dividerRecipe.lineStyles.dashed`의 dash 4 · gap 4를 두께 1로 그리며 시작 쪽 끝에서 dash로 시작한다. 색·두께는 실선과 같다(개발 소스, 미게시) |
57
59
  | `layoutStyle` | 배치 전용 style | — | 루트 배치. Native `style`은 제거된 속성 |
58
60
 
59
61
  콜백 prop은 없다.
@@ -70,6 +72,7 @@ import { Divider } from "@hjmds/react-native/data-display";
70
72
 
71
73
  ## 꼭 지킬 것
72
74
 
75
+ - 점선을 `border-*: dashed` CSS나 SVG `Line`으로 따로 그리지 않는다. `lineStyle="dashed"`를 쓴다.
73
76
  - 색·두께를 `style`/`className`으로 바꾸지 않는다. 구분선 색은 제품 테마의 `border` key로 바뀐다.
74
77
  배치는 `layoutStyle`로 한다. Native `style`은 제거된 속성 — `layoutStyle` 또는 `orientation`·`inset`으로 옮긴다.
75
78
  - Web에서 의미가 없는 장식선은 `decorative`로 표시한다(`role="presentation"`, `aria-hidden`).
@@ -82,8 +85,16 @@ import { Divider } from "@hjmds/react-native/data-display";
82
85
  | `inset` 타입 | `"none" \| "start" \| "both"` | `number`(0 이상) |
83
86
  | 접근성 | `separator`(기본) 또는 `decorative` | 항상 `accessible={false}`, `decorative` prop 없음 |
84
87
  | 요소 | 가로 `hr`, 세로 `div` | `View` |
88
+ | 점선 그리기 | `repeating-linear-gradient`(recipe의 dash·gap을 Provider CSS 변수로) | 측정한 길이에 recipe 크기의 조각 `View`를 나열(iOS는 한쪽 `borderStyle: "dashed"`를 실선으로 그린다). optional SVG peer를 쓰지 않는다 |
85
89
 
86
90
  ## 함정
87
91
 
88
92
  - Native `inset`에 음수·NaN을 넘기면 `RangeError`가 난다.
89
93
  - Native Divider는 테마를 읽으므로 `HjmNativeProvider` 밖에서 렌더하면 예외가 난다.
94
+
95
+ ## 근거
96
+
97
+ - `lineStyle="dashed"`(2026-10-23 소비 감사): 다에리가 같은 의미의 점선을 Web `.cozy-nest-next`의 `border-block-start: 1px dashed`(브라우저가 정한 간격)와
98
+ Native `DashedRule`(SVG `strokeDasharray="4 4"`)로 따로 그렸다. 의미가 같은 선을 두 번 구현해 간격이 갈랐으므로 Divider의 선 모양 축으로 두고,
99
+ 간격·두께는 recipe가 소유한다. 새 컴포넌트가 아니라 축인 이유는 색·두께·inset·접근성이 실선과 같고 패턴만 다르기 때문이다.
100
+ 이관: Web `<Divider lineStyle="dashed" />`, Native `<Divider lineStyle="dashed" />`로 `DashedRule`과 CSS 점선을 대체한다.
@@ -77,6 +77,7 @@ import { Field } from "@hjmds/react-native/forms";
77
77
  | `onValueChange`(`TextField`) | `(value: string) => void` | — | Web·Native 같은 이름·모양. Web은 DOM `onChange`(이벤트)도 함께 불린다. 공용 폼 코드는 `onValueChange`를 쓴다 |
78
78
  | `children`(`Field` render-prop) | `(control: FieldControlProps) => ReactNode` 또는 `ReactNode` | — | Web `FieldControlProps`는 `{ id, required, disabled, "aria-invalid"?: true, "aria-describedby"? }`, Native는 `{ accessibilityLabel, accessibilityHint?, accessibilityState: { disabled } }`(hint는 오류 우선) |
79
79
  | `busy`(Native `TextField`) | `boolean` | `false` | 편집을 막고 접근성 busy를 싣는다 |
80
+ | `characterCount`(`TextField`·`TextArea`) | `CharacterCountOptions` | — | 미게시. 틀 아래 `n/max`와 상태 문장. [글자 수 표시](#글자-수-표시-미게시) |
80
81
 
81
82
  - 상태 테두리는 recipe가 정한다: 기본 `borderControl`, 포커스 `contentBrand`, 오류 `danger`.
82
83
  - 도움말은 오류가 생겨도 사라지지 않고 둘 다 보인다(Web은 `aria-describedby`로 둘 다 연결).
@@ -119,6 +120,56 @@ Native는 제품 프로필의 `tokens.fontFamily.ui`를 실제 텍스트/입력
119
120
  - 커스텀 컨트롤에는 render-prop 값을 그대로 펼쳐 라벨 연결을 끊지 않는다. Web `Field`는 `controlId`가 필수다.
120
121
  - 비활성인 이유는 도움말(`description`)에 적는다. 도움말·오류는 흐려지지 않으므로 제품에서 따로 흐리게 하거나 색을 바꾸지 않는다.
121
122
 
123
+ ### 글자 수 표시 (미게시)
124
+
125
+ `TextField`·`TextArea`의 `characterCount`는 틀 바로 아래, 설명(`description`) 위에 한 줄을 그린다. 앞에는 제품 문장,
126
+ 끝에는 `n/max` 숫자가 온다. 계산과 상태 판정은 `@hjmds/design-contracts/character-count`의 `resolveCharacterCount`가 맡는다.
127
+ 숫자는 `locale`로 HJM이 형식화하고(`1,024/1,000`), 문장은 제품이 `messages`로 준다.
128
+
129
+ ```tsx
130
+ // Web (Native는 onValueChange={setPrompt})
131
+ <TextArea label={t("create.prompt")} description={t("create.countHint")} value={prompt}
132
+ onChange={(event) => setPrompt(event.currentTarget.value)}
133
+ characterCount={{
134
+ min: 5, max: 500, nearLimit: 40, measure: "trimmed", overflow: "allow", locale,
135
+ messages: {
136
+ range: ({ min, max }) => t("create.range", { min, max }),
137
+ remaining: ({ count }) => t("create.remaining", { count }),
138
+ needed: ({ count }) => t("create.needed", { count }),
139
+ over: ({ count }) => t("create.over", { count }),
140
+ },
141
+ }} />
142
+ ```
143
+
144
+ | 옵션 | 값 | 기본값 | 설명 |
145
+ | --- | --- | --- | --- |
146
+ | `max` | 양의 정수 | 필수 | 상한 |
147
+ | `min` | 0 이상 정수 | `0` | 하한. 빈 값은 `short`가 아니다(손대지 않은 칸은 오류가 아니다) |
148
+ | `overflow` | `block` · `allow` | `block` | `block`은 플랫폼 `maxLength`를 걸어 상한에서 입력이 멈춘다. `allow`는 붙여 넣은 긴 초안을 지우지 않고 계속 받으며 초과를 표시한다. 이때 제출은 제품이 `status !== "over"`로 막는다 |
149
+ | `measure` | `length` · `trimmed` | `length` | `length`는 UTF-16 길이(플랫폼 `maxLength`와 같은 단위), `trimmed`는 앞뒤 공백을 뺀 길이 |
150
+ | `nearLimit` | 0 이상 정수 | `ceil(max × 0.1)` | 남은 수가 이 값 이하면 `near`(`characterCountRecipe.nearLimitRatio`) |
151
+ | `locale` | BCP 47 | 필수 | 숫자 형식. 환경 기본값으로 새지 않도록 필수다 |
152
+ | `messages` | `range` · `remaining` · `needed` · `over` 함수 | — | 상태별 문장. 없으면 숫자만 보인다 |
153
+
154
+ | 상태(`status`) | 조건(우선순위 순) | 문장 | 모양 |
155
+ | --- | --- | --- | --- |
156
+ | `over` | 길이 > `max` | `over({ count: 초과 수 })` | 행 `danger`, 숫자 semibold, 칸 오류 테두리 2·`aria-invalid` |
157
+ | `short` | 0 < 길이 < `min` | `needed({ count: 부족 수 })` | 행 `danger`, 숫자 semibold. 칸 테두리는 그대로 |
158
+ | `near` | 남은 수 ≤ `nearLimit` | `remaining({ count: 남은 수 })` | `textMuted` |
159
+ | `within` | 그 밖 | `range({ min, max })` | `textMuted` |
160
+
161
+ - 배치: `characterCountRecipe` — `label` 글자(12/18), 문장과 숫자 사이 `spacing.sm` 8, 숫자는 끝 정렬·고정폭 숫자(`tabular-nums`).
162
+ 비활성일 때도 설명처럼 흐리지 않는다.
163
+ - 접근성: Web은 행에 id를 주고 컨트롤의 `aria-describedby`(자체 → 글자 수 → 설명 → 오류 순)에 넣어 포커스할 때 읽는다.
164
+ Native는 행을 접근성 트리에서 숨기고 문장·숫자를 입력의 `accessibilityHint` 뒤에 붙인다.
165
+ 알림은 `near`·`short`·`over`로 **들어갈 때 한 번**만 정중하게(Web `aria-live="polite"` 영역, Native `announceForAccessibility`) 한다.
166
+ 같은 상태 안에서 글자마다 알리지 않는다. 입력한 글자 낭독을 덮기 때문이다.
167
+ - `maxLength`를 함께 넘기면 `TypeError`다. 상한은 `characterCount.max` 하나로 둔다.
168
+ - `SearchField`·`PasswordField`·`OtpField`에는 없다. 자유 글 상한이 아니기 때문이다.
169
+
170
+ 근거: 2026-10-23 번뚝 웹 `components/ComposerCount.tsx`와 앱 `components/TextLengthHint.tsx`가 같은 규칙(부족·초과·남은 수, `n/max`,
171
+ 초과 시 오류 색)을 따로 그렸고, 웹만 남은 50자에서 숫자를 브랜드색으로 강조해 기준이 어긋나 있었다. 브랜드 강조는 옮기지 않았다.
172
+
122
173
  ### GestureSheetInput (Native optional-extension)
123
174
 
124
175
  `GestureSheet` 안에서 키보드를 따라 움직이는 입력이다. `BottomSheetTextInput`의 props를 그대로 받고