@hjmds/design-contracts 1.46.0 → 1.47.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 (89) hide show
  1. package/dist/behaviors.d.ts +2 -1
  2. package/dist/behaviors.d.ts.map +1 -1
  3. package/dist/bottom-navigation.d.ts +4 -0
  4. package/dist/bottom-navigation.d.ts.map +1 -1
  5. package/dist/bottom-navigation.js +12 -0
  6. package/dist/bottom-navigation.js.map +1 -1
  7. package/dist/card.d.ts +2 -2
  8. package/dist/card.d.ts.map +1 -1
  9. package/dist/card.js +3 -2
  10. package/dist/card.js.map +1 -1
  11. package/dist/carousel.d.ts +1 -0
  12. package/dist/carousel.d.ts.map +1 -1
  13. package/dist/carousel.js +1 -0
  14. package/dist/carousel.js.map +1 -1
  15. package/dist/catalog.d.ts +130 -14
  16. package/dist/catalog.d.ts.map +1 -1
  17. package/dist/catalog.js +4 -3
  18. package/dist/catalog.js.map +1 -1
  19. package/dist/color-picker.d.ts +51 -1
  20. package/dist/color-picker.d.ts.map +1 -1
  21. package/dist/color-picker.js +56 -2
  22. package/dist/color-picker.js.map +1 -1
  23. package/dist/component-recipes.d.ts +23 -9
  24. package/dist/component-recipes.d.ts.map +1 -1
  25. package/dist/component-recipes.js +24 -18
  26. package/dist/component-recipes.js.map +1 -1
  27. package/dist/conversation-row.d.ts +1 -1
  28. package/dist/conversation-row.d.ts.map +1 -1
  29. package/dist/conversation-row.js +2 -1
  30. package/dist/conversation-row.js.map +1 -1
  31. package/dist/date-range.d.ts +122 -3
  32. package/dist/date-range.d.ts.map +1 -1
  33. package/dist/date-range.js +139 -5
  34. package/dist/date-range.js.map +1 -1
  35. package/dist/icon.d.ts +2 -1
  36. package/dist/icon.d.ts.map +1 -1
  37. package/dist/icon.js +2 -0
  38. package/dist/icon.js.map +1 -1
  39. package/dist/recipes.d.ts +1 -0
  40. package/dist/recipes.d.ts.map +1 -1
  41. package/dist/recipes.js +1 -0
  42. package/dist/recipes.js.map +1 -1
  43. package/dist/screen-patterns.d.ts +33 -2
  44. package/dist/screen-patterns.d.ts.map +1 -1
  45. package/dist/screen-patterns.js +27 -2
  46. package/dist/screen-patterns.js.map +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/dist/version.js.map +1 -1
  50. package/docs/bottom-navigation.md +16 -0
  51. package/docs/color-picker.md +24 -19
  52. package/docs/generated/component-maturity.md +3 -3
  53. package/docs/generated/renderer-evidence.json +59 -7
  54. package/docs/generated/renderer-evidence.md +3 -2
  55. package/docs/generated/showcase-manifest.json +18 -4
  56. package/docs/usage/README.md +5 -2
  57. package/docs/usage/SELECTION.md +109 -0
  58. package/docs/usage/components/avatar.md +31 -6
  59. package/docs/usage/components/bottom-navigation.md +16 -0
  60. package/docs/usage/components/button.md +11 -0
  61. package/docs/usage/components/card.md +6 -0
  62. package/docs/usage/components/chat-message.md +10 -2
  63. package/docs/usage/components/chat-room-members.md +4 -0
  64. package/docs/usage/components/chat-screen.md +10 -1
  65. package/docs/usage/components/color-picker.md +65 -28
  66. package/docs/usage/components/comment-thread-screen.md +16 -1
  67. package/docs/usage/components/conversation-list-screen.md +81 -0
  68. package/docs/usage/components/conversation-row.md +5 -1
  69. package/docs/usage/components/date-range-picker.md +15 -5
  70. package/docs/usage/components/event-attendance-card.md +4 -0
  71. package/docs/usage/components/follow-list-screen.md +109 -0
  72. package/docs/usage/components/follow-profile-card.md +4 -0
  73. package/docs/usage/components/list-row.md +6 -0
  74. package/docs/usage/components/menu.md +12 -0
  75. package/docs/usage/components/notice.md +8 -3
  76. package/docs/usage/components/notification-inbox-screen.md +10 -0
  77. package/docs/usage/components/notification-row.md +4 -0
  78. package/docs/usage/components/page-indicator.md +1 -0
  79. package/docs/usage/components/profile-summary.md +32 -3
  80. package/docs/usage/components/section.md +2 -2
  81. package/docs/usage/components/settings-screen.md +1 -1
  82. package/docs/usage/components/sheet.md +1 -0
  83. package/docs/usage/components/statistic.md +1 -1
  84. package/docs/usage/compositions/web-additions.md +5 -5
  85. package/docs/usage/screens/common-comments.md +1 -0
  86. package/docs/usage/screens/common-conversation-list.md +22 -1
  87. package/docs/usage/screens/common-follow-list.md +87 -0
  88. package/docs/usage/screens/common-settings.md +23 -0
  89. package/package.json +1 -1
@@ -56,3 +56,7 @@ Native는 동일 prop 계약으로 `@hjmds/react-native/communication`에서 가
56
56
  members·title·emptyLabel이 필수다. onSelectionChange가 있으면 선택 목록이다. `memberActions`는 멤버별 메뉴 이름(`label(member)`)·닫기 문구·허용된 행동만 전달하며, 메뉴는 가로 점 세 개 아이콘 버튼으로 열린다. `invite`를 주면 목록 위에 브랜드 색 "초대" 행이 생긴다(Telegram "구성원 추가"). 역할은 `member.role`로 이름 오른쪽에 브랜드 색 caption으로 표시하고, `detail`은 둘째 줄(접속·소개)에 둔다.
57
57
 
58
58
  데이터·문구·권한은 제품이 소유한다. 저장·개설·초대·삭제는 실제 성공 뒤 Promise를 resolve하고 실패는 reject한다. 입력과 선택은 실패 시 유지한다. Storybook의 메모리 예제를 운영 저장으로 사용하지 않는다. 방 전환 시 `key={roomId}`로 작업 수명을 구분한다.
59
+
60
+ ### 회원 프로필 이동 (2026-10-25)
61
+
62
+ 일반 참여자 목록은 `memberProfile={{label: member => openLabel(member.name), onPress: member => openMember(member.id)}}`로 사진과 행에서 해당 회원 화면을 연다. 명시적인 초대/선택 모드(onSelectionChange)는 선택 동작을 유지한다.
@@ -73,7 +73,7 @@ import { ChatScreen, MessageComposer } from "@hjmds/react-native/screens";
73
73
  | `composer`(필수) | `MessageComposer` 등 작성창 | `ready`와 `empty`에서 보인다(빈 대화의 첫 메시지 입력) |
74
74
  | `state` / `stateAction` | `loading`·`empty`·`error`·`restricted` + 지역화 `title` | ready 외에는 본문을 교체한다 |
75
75
  | `notice` | 새로고침 실패 같은 비차단 안내 | 초안 유지가 필요하면 `state` 대신 이것 |
76
- | `avatar` | 상대·방 사진 노드 | 미게시. 가운데 정렬 제목 앞에 놓는다. 단톡방은 인원 수를 `titleBadge`(흐린 글자)로 넘긴다. ScreenLayout `titleLeading`과 같은 자리라 ChatScreen은 `titleLeading`을 따로 받지 않는다. 장식이라 보조기술에서 숨는다(제목이 이름) |
76
+ | `avatar` | 상대·방 사진 노드 | 미게시. 가운데 정렬 제목 앞에 놓는다. 단톡방은 인원 수를 `titleBadge`(흐린 글자)로 넘긴다. ScreenLayout `titleLeading`과 같은 자리라 ChatScreen은 `titleLeading`을 따로 받지 않는다. 기본은 장식이라 보조기술에서 숨는다(제목이 이름). `avatarAction`을 주면 이름 있는 프로필 이동 버튼이 된다 |
77
77
  | `jumpToLatest` | `{ label, icon, onPress() }` | 미게시. 독자가 최신 메시지에서 벗어났을 때만 넘긴다. 작성창 위 끝 쪽에 원형 `IconButton size="small"`(secondary)로 떠 있고, 스크롤 위치 판단은 제품이 한다 |
78
78
 
79
79
  ## 축과 기본값
@@ -115,3 +115,12 @@ import { ChatScreen, MessageComposer } from "@hjmds/react-native/screens";
115
115
  | 높이 | host가 남은 높이 제공 | `flex: 1` |
116
116
  | 스크롤 연결 | 없음 | `scrollRef`, `scrollProps`(`refreshControl`, `keyboardDismissMode` 등) — `scroll="screen"`이거나 상태 교체 중일 때만 ScrollView가 있다 |
117
117
  | 테스트 id | 없음 | `testID` |
118
+
119
+ ### 상대 프로필 이동 (2026-10-25, 개발 소스)
120
+
121
+ 헤더 사진을 회원 프로필로 연결할 때 `avatarAction={{ label: t("profile.open"), onPress: () => openMember(peer.id) }}`를 준다.
122
+ Web·Native 모두 같은 계약이며 제품이 회원 ID·이동 경로·번역 문구를 소유한다. 단체방의 헤더는 방을 나타내므로
123
+ 회원 한 명의 프로필로 연결하지 않는다. 메시지 작성자는 `ChatMessage.avatarAction`으로 따로 연결한다.
124
+ 동작이 없으면 기존 장식 표시를 유지한다. 동작이 있으면 44px/pt 버튼으로 노출하고 장식 사진만 보조기기에서 숨긴다.
125
+ 기존에는 헤더 사진 전체가 숨겨져 프로필 이동 컨트롤을 넣어도 접근할 수 없었다.
126
+ 일반 `ScreenLayout`의 같은 자리는 `titleLeadingAction`을 사용한다.
@@ -2,22 +2,25 @@
2
2
 
3
3
  - 단계: 컴포넌트
4
4
  - 상태: 배포
5
- - 지원: Web
5
+ - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
8
- - 근거: [ColorPicker — Web sRGB 색상 입력](../../color-picker.md), recipe `colorPickerRecipe`(`src/color-picker.ts`)
7
+ - 검토일: 2026-10-25
8
+ - 근거: [ColorPicker — sRGB 색상 입력](../../color-picker.md), recipe `colorPickerRecipe`(`src/color-picker.ts`)
9
9
  - 스토리북: `배포/컴포넌트/입력/색상 선택기`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
13
- 사용자가 콘텐츠 색(라벨 색, 태그 색, 테마 편집기의 사용자 값 등)을 sRGB HEX로 고르는 폼 입력에 쓴다.
14
- 브라우저 색상 선택기, HEX 텍스트 입력, 선택적 불투명도 슬라이더, 프리셋 견본을 한 fieldset으로 묶는다.
13
+ 사용자가 콘텐츠 색(라벨 색, 태그 색, 손그림 펜 색, 테마 편집기의 사용자 값 등)을 sRGB HEX로 고를 때 쓴다.
14
+ 채도·밝기 영역, 색조 막대, 선택적 불투명도 막대, HEX 입력, 프리셋 견본을 한 묶음으로 제공한다. Web과 Native의 모양과 동작이 같다.
15
+
16
+ 자주 쓰는 색 몇 개를 한 줄로 빠르게 고르는 곳(그리기 도구 막대 등)에는 동반 부품 `ColorSwatchPicker`를 두고,
17
+ "더 많은 색" 행동으로 `ColorPicker`를 [Sheet](sheet.md)에 연다.
15
18
 
16
19
  ## 쓰지 않을 때
17
20
 
18
21
  | 상황 | 대신 쓸 것 |
19
22
  | --- | --- |
20
- | 정해진 몇 가지 색 중 하나만 고름 | [RadioGroup](radio-group.md), [SegmentedControl](segmented-control.md) |
23
+ | 정해진 몇 가지 색 중 하나만 고름 | `ColorSwatchPicker`(이 문서). 색이 아닌 선택지는 [RadioGroup](radio-group.md), [SegmentedControl](segmented-control.md) |
21
24
  | 숫자 하나를 범위에서 고름 | [Slider](slider.md) |
22
25
  | 제품 브랜드·테마 색을 화면마다 바꾸기 | 컴포넌트가 아니다. 제품 테마 토큰(`brandPalette`)으로 한다 |
23
26
 
@@ -25,10 +28,12 @@
25
28
 
26
29
  | 이름 | 역할 | Web | Native |
27
30
  | --- | --- | --- | --- |
28
- | `ColorPicker` | 기본 | `/color-picker` | 없음 |
29
- | `ColorPickerLabels`(타입) | 보조(문구 묶음) | `/color-picker` | 없음 |
31
+ | `ColorPicker` | 기본 | `/color-picker` | `/color-picker` |
32
+ | `ColorSwatchPicker` | 동반(견본 하나 고르기) | `/color-picker` | `/color-picker` |
33
+ | `ColorPickerLabels`(타입) | 보조(문구 묶음) | `/color-picker` | `/color-picker` |
30
34
 
31
- 루트 barrel에는 없다. React Native 구현은 없다.
35
+ 루트 barrel에는 없다. Native는 `react-native-svg`(선택 peer)가 필요하다. Chart와 같다.
36
+ Native 구현과 2026-10-25 개편은 개발 후보이며 패키지 게시·기기 검증은 별도다.
32
37
 
33
38
  ## 최소 사용 예
34
39
 
@@ -39,8 +44,8 @@ import { ColorPicker } from "@hjmds/react/color-picker";
39
44
  <ColorPicker
40
45
  label={t("label.color")}
41
46
  labels={{
42
- color: t("color.choose"), hex: t("color.hex"),
43
- opacity: t("color.opacity"), invalid: t("color.invalid"),
47
+ hex: "HEX", hue: t("color.hue"), saturation: t("color.saturation"), brightness: t("color.brightness"),
48
+ opacity: t("color.opacity"), invalid: t("color.invalid"), presets: t("color.presets"),
44
49
  }}
45
50
  value={color}
46
51
  onValueChange={setColor}
@@ -48,39 +53,71 @@ import { ColorPicker } from "@hjmds/react/color-picker";
48
53
  />
49
54
  ```
50
55
 
51
- Native: 없음. 제품의 색 선택 화면이나 플랫폼 피커를 쓴다.
56
+ ```tsx
57
+ // Native: 도구 막대의 빠른 색 줄 + 시트 안의 전체 선택기
58
+ import { ColorPicker, ColorSwatchPicker } from "@hjmds/react-native/color-picker";
59
+
60
+ <ColorSwatchPicker label={t("pen.color")} labelVisibility="hidden" layout="scroll"
61
+ colors={quickColors} value={quickColors.includes(color) ? color : null} onValueChange={setColor} />
62
+ <Sheet open={open} onOpenChange={setOpen} title={t("color.more")} closeLabel={t("action.close")}>
63
+ <ColorPicker label={t("pen.color")} labels={labels} value={color} onValueChange={setColor} />
64
+ </Sheet>
65
+ ```
52
66
 
53
67
  ## 축과 기본값
54
68
 
55
69
  | prop | 값 | 기본값 | 설명 |
56
70
  | --- | --- | --- | --- |
57
- | `value` + `onValueChange` | 소문자 `#rrggbb`(`alpha`면 `#rrggbbaa`) + `(value: string) => void` | 필수(controlled 전용) | 정규화한 값이 바뀔 때만 부른다 |
58
- | `label` | `string` | 필수 | fieldset legend |
59
- | `labels` | `{ color: string; hex: string; opacity: string; invalid: string }` | 필수 | 견본 입력·HEX 입력·불투명도·오류 문구 |
60
- | `alpha` | `boolean` | `false` | 켜면 0–100% 불투명도 슬라이더가 생긴다. 3·6자리 HEX를 치면 현재 불투명도를 유지한다 |
61
- | `disabled` | `boolean` | `false` | fieldset으로 모든 입력과 프리셋을 막는다 |
62
- | `presets` | `readonly string[]`(HEX) | `[]` | 정규화 후 중복을 없앤다. 선택된 견본은 `aria-pressed` |
63
- | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 `fieldset` 배치 |
64
- | HEX 입력 | — | — | Enter·blur에 확정, Escape는 마지막 `value`로 되돌린다. 잘못된 입력은 `labels.invalid`를 alert로 보이고 외부 값은 바꾸지 않는다 |
71
+ | `value` + `onValueChange` | 소문자 `#rrggbb`(`alpha`면 `#rrggbbaa`) + `(value: string) => void` | 필수(controlled 전용) | 정규화한 값이 바뀔 때만 부른다. 영역·막대를 끄는 동안 연속으로 부른다 |
72
+ | `label` | `string` | 필수 | 묶음 이름(Web legend) |
73
+ | `labels` | `{ hex; hue; saturation; brightness; opacity; invalid; presets? }` | 필수 | 영역의 두 축·색조·불투명도 막대의 접근성 이름, HEX 입력, 오류, 프리셋 묶음 이름. `presets`는 `presets`가 있을 때 필수 |
74
+ | `alpha` | `boolean` | `false` | 켜면 0–100% 불투명도 막대와 % 표시가 생긴다. 3·6자리 HEX를 치면 현재 불투명도를 유지한다 |
75
+ | `disabled` | `boolean` | `false` | 영역·막대·HEX·프리셋을 모두 막는다 |
76
+ | `presets` | `readonly string[]`(HEX) | `[]` | 정규화 후 중복을 없앤다. `ColorSwatchPicker`로 그린다 |
77
+ | `getPresetLabel` | `(color) => string` | HEX 코드 | 견본의 접근성 이름. 제품이 지역화한 색 이름이 있으면 넘긴다 |
78
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 묶음 배치 |
79
+ | HEX 입력 | — | — | Enter·blur(Native는 submit·blur)에 확정, Web Escape는 마지막 `value`로 되돌린다. 잘못된 입력은 `labels.invalid`를 보이고 외부 값은 바꾸지 않는다 |
80
+
81
+ `ColorSwatchPicker`
82
+
83
+ | prop | 값 | 기본값 | 설명 |
84
+ | --- | --- | --- | --- |
85
+ | `label` | `string` | 필수 | 묶음 이름 |
86
+ | `colors` | `readonly string[]`(HEX) | 필수 | 정규화 후 중복 제거 |
87
+ | `value` + `onValueChange` | HEX 또는 `null` | 필수 | 현재 색이 견본에 없으면 `null`(아무 견본도 선택 표시 안 함). 이미 선택된 견본을 누르면 부르지 않는다 |
88
+ | `layout` | `"wrap"` \| `"scroll"` | `"wrap"` | `scroll`은 가로 한 줄 스크롤(도구 막대) |
89
+ | `labelVisibility` | `"visible"` \| `"hidden"` | `"visible"` | `hidden`이면 보조기술에만 묶음 이름을 준다 |
90
+ | `alpha`, `disabled`, `getColorLabel`, `layoutStyle` | — | — | `ColorPicker`와 같은 뜻 |
65
91
 
66
92
  ## 배치
67
93
 
68
94
  | 항목 | 값 | 근거 |
69
95
  | --- | --- | --- |
70
- | 크기 | 부모 폭을 채우는 fieldset(테두리 1px, `radius.md` 12). 입력·프리셋 버튼 최소 높이 44(`minTargetSize`), 색 견본 입력 폭 3rem(48), HEX 입력 기준 폭 10rem(160)에서 늘어난다. 미리보기 띠 높이 1.5rem(24) | `colorPickerRecipe.minTargetSize`, `.hjm-color-picker*` |
71
- | 간격 | 안쪽 1rem(16, `spacing.md`와 같은 값), 색 견본↔HEX 0.75rem(12), 불투명도·미리보기·프리셋 위 0.75rem(12), 프리셋 사이 0.5rem(8). CSS가 spacing 변수 대신 rem 값을 쓴다 | `.hjm-color-picker*` |
72
- | 순서·정렬 | 위→아래 [legend] → [색 견본 입력][HEX 입력] 한 줄 → [오류(alert)] → [불투명도(`alpha`일 때)] → [미리보기] → [프리셋 줄]. 설정 폼 안에 블록으로 둔다 | `react/src/color-picker.tsx` |
73
- | 고정·스크롤 | 고정 영역이 없다 | — |
74
- | 좁은 폭·큰 글자 | 견본·HEX 줄과 프리셋 줄이 줄바꿈된다(`flex-wrap: wrap`). 문구는 `overflow-wrap: anywhere` | `.hjm-color-picker__row`, `.hjm-color-picker__presets` |
96
+ | 크기 | 부모 폭을 채운다. 영역 4:3(`area.aspectRatio`), 영역 손잡이 24(흰 테두리 3), 막대 높이 24·손잡이 28, 미리보기 원 44, 견본 누름 영역 44·원 32·선택 고리 2(간격 2) | `colorPickerRecipe` |
97
+ | 간격 | 묶음 안쪽 16, 부품 사이 `spacing.md`, 견본 사이 2(Web 0.25rem·Native `spacing.xxs`) | `.hjm-color-picker*`, Native `color-picker.tsx` |
98
+ | 순서·정렬 | 위→아래 [label] → [채도·밝기 영역] → [색조 막대] → [불투명도 막대(`alpha`)] → [미리보기 원][HEX 입력][불투명도 %] → [오류] → [프리셋 견본] | `react/src/color-picker.tsx`, `react-native/src/color-picker.tsx` |
99
+ | 방향 | 영역과 막대는 RTL에서도 왼쪽→오른쪽이다(색 공간 좌표이지 읽기 순서가 아니다). HEX 입력도 LTR로 고정한다 | 같은 파일 주석 |
100
+ | 고정·스크롤 | 영역은 터치를 잡으면 상위 스크롤에 넘기지 않는다(Web `touch-action: none`, Native 응답자 종료 거절). 막대는 Slider처럼 가로 의도가 확인될 때까지 세로 스크롤에 양보한다 | 같은 파일, [Slider](slider.md) |
101
+ | 좁은 폭·큰 글자 | 영역·막대는 폭에 맞춰 줄고, HEX 입력이 남은 폭을 채운다. `wrap` 견본은 줄바꿈된다 | — |
75
102
 
76
103
  ## 꼭 지킬 것
77
104
 
78
- - `label`과 `labels`의 네 문구는 모두 비어 있지 않아야 한다. 하나라도 비면 렌더 중 `TypeError`가 난다.
105
+ - `label`과 `labels`의 문구는 모두 비어 있지 않아야 한다(`presets`가 있으면 `labels.presets`도). 하나라도 비면 렌더 중 `TypeError`가 난다.
79
106
  - `value`와 `presets`는 유효한 HEX여야 한다. `alpha={false}`인데 불투명하지 않은 8자리 값을 주면 투명도를 버리지 않고
80
107
  `TypeError`를 던진다. 저장된 값을 넘기기 전에 제품 어댑터에서 형식을 맞춘다.
81
108
  - 고른 색을 본문·배경에 쓸 때의 대비 검사는 제품이 한다. 견본 색은 콘텐츠 데이터이며 HJM 토큰이 아니다.
82
- - 배치는 `layoutStyle`(바깥 `fieldset`)로 한다. `className`·`style` prop은 없다.
109
+ - 견본 줄을 제품이 버튼으로 다시 만들지 않는다. `ColorSwatchPicker`를 쓴다.
110
+ - 배치는 `layoutStyle`로 한다. `className`·`style` prop은 없다.
83
111
 
84
112
  ## 함정
85
113
 
86
- - 브라우저 색상 팝업은 RGB만 고른다. 팝업 모양과 동작은 OS·브라우저 소유이며 HJM 검증 대상이 아니다.
114
+ - 검정·회색은 색조가 없고 검정은 채도도 없다. 컴포넌트는 직전 색조·채도를 기억해 손잡이가 빨강(0°)으로 튀지 않게 한다.
115
+ 외부에서 다른 색을 넣으면 그 색에서 다시 계산한다.
116
+ - `onValueChange`는 끄는 동안 여러 번 불린다. 저장·네트워크 전송은 제품이 시트를 닫을 때 등으로 모은다.
117
+
118
+ ## 개편 근거 (2026-10-25)
119
+
120
+ Diairy 손그림 프로필에서 색 선택이 부족하다는 요청으로 Native를 추가했고, 같은 날 "다른 곳 사례를 참고해 개편" 요청으로 HSV 편집기로 바꿨다.
121
+ 첫 Native 후보의 RGB 0–255 막대 세 개는 "조금 더 연한 분홍" 같은 생각을 세 막대 퍼즐로 만들었고, Web의 브라우저 색 팝업은 OS마다 모양이 달랐다.
122
+ Figma·Procreate Classic·iOS 색 선택기(Spectrum)처럼 채도·밝기 영역과 색조 막대를 기본으로 하고, React Aria `ColorSwatchPicker`처럼 견본 고르기를 별도 부품으로 분리했다.
123
+ Procreate처럼 자주 쓰는 색을 바깥 한 줄에 두는 구성은 견본 부품으로 제품이 짠다(최근 색 저장은 제품 데이터다).
@@ -86,7 +86,9 @@ import { MessageComposer } from "@hjmds/react-native/screens";
86
86
  | `onLike(id)`·`onReply(id)` | 기본 좋아요 버튼·답글 버튼의 콜백 |
87
87
  | `replyLabel`·`repliesLabel(count, expanded)` | 지역화 문구. 복수형·펼침 상태는 제품 i18n이 만든다. 펼침 버튼 이름이 이 문구이므로 `expanded`에 따라 "답글 3개 보기"/"답글 숨기기"처럼 상태가 드러나게 만든다 |
88
88
  | `composer` | 작성창(보통 `MessageComposer`의 `replyTo`·`sendIcon`). `ready`·`empty`일 때만 보인다 |
89
- | `threadFooter` | 더 보기·커서 페이지네이션 |
89
+ | `threadFooter` | 최상위 댓글 목록의 더 보기·커서 페이지네이션 |
90
+ | `moreRepliesLabel(remaining)`(미게시) | 펼친 답글 묶음의 "더 보기" 문구(예: "답글 7개 더 보기"). 주면 아래 답글 5개 정책이 켜지고, 생략하면 받은 답글을 모두 보이는 이전 동작이다. 포트폴리오 제품은 모두 준다 |
91
+ | `revealedReplyId`(미게시) | 방금 보낸 답글·알림 대상처럼 열린 페이지 밖이어도 보여야 하는 답글 id. 묶음 펼침은 여전히 `expandedIds`가 정한다 |
90
92
  | `commentMenu`(미게시) | 댓글별 더보기 메뉴. `label(item)` 버튼·메뉴의 지역화 접근성 이름(예: "seoyeon 댓글 더 보기"), `items(item)` 그 댓글에 허용된 항목(`{ id, label, tone?: "danger", disabled? }`, Menu·ContextMenu와 같은 항목 어휘), `onAction(item, id)` 메뉴가 닫힌 뒤 호출. Native는 메뉴 시트의 닫기 문구 `dismissLabel`도 받는다. 항목이 비면 그 행에는 버튼을 그리지 않는다. 같은 prop을 `CommentThreadRow`도 받는다 |
91
93
 
92
94
  `CommentThreadItem`: `id`, `parentId`(`null`이면 최상위), `author`, `body`(노드), `timeLabel`, `likeCountLabel`,
@@ -98,6 +100,15 @@ import { MessageComposer } from "@hjmds/react-native/screens";
98
100
  받은 답글이 없어도 전체 개수·복구 안내·더 보기 슬롯으로 답글 영역을 열 수 있다. 접으면 세 영역이 함께 숨는다.
99
101
  조회·커서·재시도·문구·데이터 보존은 제품이 맡고 오류/페이지 행동은 HJM Notice·Button/LoadMore로 공급한다.
100
102
  Spint에서 페이지 오류와 더 보기가 원글 body에 들어가 답글을 접어도 남던 2026-10-18 이관이 확장 근거다.
103
+
104
+ ### 답글 5개씩 (2026-10-25 정책, 미게시)
105
+
106
+ 펼친 답글 묶음은 `commentThreadRecipe.replyPageSize`(5)개만 먼저 보이고, 묶음 끝의 흐린 글자 버튼
107
+ (`moreRepliesLabel(남은 수)`)이 5개씩 더 연다. 받은 답글이 남아 있으면 HJM이 바로 보이고, 받은 답글을 다 보였는데
108
+ `repliesCount`가 더 크면 최상위 항목의 `repliesPage.onLoadMore()`를 불러 제품이 다음 서버 페이지(5개)를 받게 한다.
109
+ 요청 중에는 `repliesPage.pending`으로 버튼을 막는다. 오류는 `repliesNotice`, 그 밖의 묶음 끝 내용은 `repliesFooter`에 둔다.
110
+ 열린 개수는 HJM 화면 상태이고, 데이터·커서·정렬·펼침은 제품이 맡는다. 판단 규칙은 `resolveCommentReplyPage`다.
111
+ 계기: Spint가 답글 더 보기를 가운데 브랜드색 링크로 직접 만들고 번뚝은 답글을 접지 않아, 같은 기능이 앱마다 달랐다.
101
112
  실행 원본: [Web 답글 복구](../../../../../showcase/web/src/components/comment-thread-replies-preview.tsx),
102
113
  [Native 답글 복구](../../../../../showcase/native/src/components/comment-thread-replies-preview.tsx).
103
114
 
@@ -179,3 +190,7 @@ threadHeader도 댓글과 함께 숨겨지므로 항상 보여야 하는 안내
179
190
  2026-10-20 메뉴의 대상 연결을 분명히 하자는 결정으로 `commentMenu.presentation`은 Web·Native 모두
180
191
  `anchored`가 기본이다. `grouped`/`bottom`으로 바꿀 때는 제품 상호작용 이유와 지역화한 `dismissLabel`을 함께 제공한다.
181
192
  행마다 제품 Menu를 새로 조립하지 않는다. [Menu의 세 표현](menu.md#세-가지-표시-방식--개발-소스-미게시)을 따른다.
193
+
194
+ ### 회원 프로필 이동 (2026-10-25)
195
+
196
+ `CommentThreadItem.avatarAction={label,onPress}`로 작성자 사진·이니셜을 회원 프로필에 연결한다. 아바타 버튼은 댓글 hold/답장/좋아요 영역 밖에 위치한다.
@@ -0,0 +1,81 @@
1
+ # ConversationListScreen
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.46.1 이후)
7
+ - 검토일: 2026-10-25
8
+ - 근거: 번뚝 목록의 중복 여백·구분선 재발, 양 renderer `src/screens.tsx`
9
+ - 스토리북: `배포/화면/소통/대화 목록`
10
+
11
+ ## 언제 쓰나
12
+
13
+ DM·단체방 대화 목록의 공통 화면이다. 행은 ConversationRow, 데이터·이동·행 메뉴는 제품이 연결한다.
14
+ ScreenLayout은 일반 화면 골격이며 대화 목록에서는 여백을 다시 조합하지 않고 이 API를 사용한다.
15
+
16
+ ## 쓰지 않을 때
17
+
18
+ | 상황 | 대신 쓸 것 |
19
+ | --- | --- |
20
+ | 일반 검색 | SearchScreen |
21
+ | 알림 | NotificationInboxScreen |
22
+ | 목록과 상세 전환 | ListDetailScreen |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `ConversationListScreen` | 대화 목록 화면 | `@hjmds/react/screens` | `@hjmds/react-native/screens` |
29
+
30
+ 같은 경로에서 ConversationListScreenProps를 공개한다.
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { ConversationListScreen } from '@hjmds/react/screens';
37
+ <ConversationListScreen title={title} search={searchField} actions={composeAction} state={state}>
38
+ {conversationLinks}
39
+ </ConversationListScreen>
40
+ ```
41
+
42
+ ```tsx
43
+ // Native
44
+ import { ConversationListScreen } from '@hjmds/react-native/screens';
45
+ <ConversationListScreen title={title} search={searchField} actions={composeAction} state={state} scroll="content">
46
+ {virtualizedConversationList}
47
+ </ConversationListScreen>
48
+ ```
49
+
50
+ ## 축과 기본값
51
+
52
+ | prop | 값 | 기본값 | 설명 |
53
+ | --- | --- | --- | --- |
54
+ | search | ReactNode | 없음 | 검색 입력 콘텐츠 |
55
+ | notice | ReactNode | 없음 | 비차단 조회 오류·안내 |
56
+ | children | ReactNode | 없음 | ConversationRow 링크/메뉴 또는 가상화 host |
57
+ | actions | ReactNode | 없음 | 새 대화 등 헤더 도구 |
58
+ | state, stateAction | ScreenLayout 계약 | ready, 없음 | 로딩·빈 상태·오류·권한 상태와 행동 |
59
+ | scroll | screen · content | screen | 가상화 host만 content |
60
+
61
+
62
+ 나머지 prop은 [ScreenLayout](screen-layout.md)을 따른다. contentInset은 받지 않고 body-edge를 고정한다.
63
+
64
+ ## 배치
65
+
66
+ | 항목 | 값 | 근거 |
67
+ | --- | --- | --- |
68
+ | 크기 | 본문 폭·헤더 높이는 ScreenLayout, 행 크기는 ConversationRow | 두 공개 구현 |
69
+ | 간격 | 헤더·검색 좌우 16, 본문 좌우 0, 행 좌우 16, 검색–안내 12 | screenPatternRecipe.padding, conversationRowRecipe.paddingInline, Stack sm |
70
+ | 순서·정렬 | 헤더 → 검색/안내 → 본문 → footer | ConversationListScreen → ScreenLayout |
71
+ | 고정·스크롤 | 기본 본문 스크롤, 가상화 host만 scroll=content | ScreenLayout |
72
+ | 좁은 폭·큰 글자 | 행과 상태의 기존 반응형 동작 유지 | ConversationRow·ScreenLayout, 최대 글자 조건 제외 |
73
+
74
+ 검색/안내가 모두 없으면 notice 영역을 만들지 않는다. 카드·구분선을 추가하지 않는다.
75
+
76
+ ## 꼭 지킬 것
77
+
78
+ 본문을 카드나 추가 좌우 여백으로 감싸지 않는다. 제품은 데이터·라우팅·메뉴를 연결한다.
79
+ 배포 표기는 기존 Storybook 화면의 분류이며 새 API의 npm 게시 완료를 뜻하지 않는다.
80
+ 실제 행을 사용한 브라우저의 시작점 확인과 Native host 검사를 기기 검증으로 확대하지 않는다.
81
+ 전체 예시는 [대화 목록 화면](../screens/common-conversation-list.md)을 따른다.
@@ -63,7 +63,7 @@ import { ConversationRow } from "@hjmds/react-native/conversation-row";
63
63
 
64
64
  | 항목 | 값 | 근거 |
65
65
  | --- | --- | --- |
66
- | 크기 | 아바타 56, 읽지 않음 점 10 | conversationRowRecipe |
66
+ | 크기 | 아바타 48, 읽지 않음 점 10 | conversationRowRecipe |
67
67
  | 간격 | 가로 sm, 세로 10 | 같은 recipe |
68
68
  | 순서·정렬 | 아바타 → 제목/미리보기·시각 → 읽지 않음 점 | 양 renderer |
69
69
  | 고정·스크롤 | 바깥 목록 host 소유 | 별도 scroller 없음 |
@@ -99,3 +99,7 @@ host는 상대·미리보기·시각·읽지 않은 수가 포함된 현지화
99
99
 
100
100
  - 공개 구성으로 분리: 2026-10-15 전사 적용 중 제품마다 복제한 DM 행의 배치가 달라졌다.
101
101
  - 행 메뉴의 위험 항목 위치: 2026-10-16 인스타그램 DM 실측.
102
+
103
+ ### 회원 프로필 이동 (2026-10-25)
104
+
105
+ `avatarAction={label,onPress}`로 상대 프로필을 열고 `renderBody(body)`에 제품의 실제 Link/Pressable을 연결해 대화로 이동한다. 이 조합의 바깥에 행 전체 Link를 다시 감싸지 않는다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-25
8
8
  - 근거: [DateRange](../../date-range.md), 격자는 [Calendar](../../calendar.md), 규칙 함수 `src/date-range.ts`
9
9
  - 스토리북: `배포/컴포넌트/입력/기간 선택`
10
10
 
@@ -76,17 +76,27 @@ import { DateRangePicker } from "@hjmds/react-native/date-range";
76
76
  | `descriptor.onFocusedMonthChange` | `(month: string, reason: "previous" \| "next" \| "jump") => void` | — | 없으면 이동 버튼이 비활성 |
77
77
  | `composeAccessibleName` | `(info: { date, isToday, isSelected, disabled, content? }) => string` | — (필수) | 구간 접미사는 컴포넌트가 붙인다 |
78
78
  | `rangeLabels` | `{ start: string, end: string, between: string }` | — (필수) | 셀 이름 뒤에 붙는 구간 위치 접미사 |
79
+ | `maxRange` | `{ years?, months?, days? } \| null` | `{ years: 1 }` | 끝을 포함한 최대 길이. 달력 단위라 윤년을 낀 1월 1일~12월 31일도 1년이다. 고르는 중 범위 밖 날은 비활성, `null`이면 제한 없음. 넘는 `value`를 넘기면 `RangeError` |
79
80
  | `previousMonth` · `nextMonth` | `{ month: string, label: string }` | — | 없으면 이동 버튼이 없다 |
80
- | `renderCellContent` | `(date: string) => ReactNode` | — | 날짜 아래 보조 표시 |
81
+ | `renderCellContent` | `(date: string) => ReactNode` | — | 날짜 아래 보조 표시. 넘길 때만 행에 보조 줄이 생긴다 |
82
+ | `summary` | `{ startLabel, endLabel, placeholder, formatDate(date) }` | — | 격자 위 시작일·종료일 칸. 지금 고르는 쪽(`resolveDateRangeActiveEdge`: 비었으면 시작, 고르는 중이면 끝, 완성되면 없음)을 테두리로 표시한다. 문구는 i18n, 날짜 형식은 제품 소유 |
83
+ | `presets` | `{ id, label, length: { days } \| { months } }[]` | — | 요약 아래 단일 선택 Chip 줄(최근 7일·1개월 등). 모두 `grid.todayDate`로 끝나고(`resolveDateRangePreset`), 값이 그 구간과 같을 때만 선택으로 보인다. 누르면 끝 달로 `onFocusedMonthChange(month, "jump")`를 요청한다 |
81
84
  | Web `layoutStyle` | 배치 전용 style | — | 루트 배치 |
82
85
 
83
86
  ## 배치
84
87
 
88
+ 2026-10-25 개편(Spint 기간 시트 사용자 지적): 구간이 날짜 아래 점으로만 보여 이어져 보이지 않았고, 점 줄 때문에
89
+ 모든 행이 라벨 한 줄만큼 높아졌다. 오늘 테두리와 단일 선택 원이 섞여 시작·끝도 구분되지 않았다. Material 3·Airbnb
90
+ 기간 선택을 모티브로 두 끝은 같은 채운 원, 사이는 날짜 원 높이의 띠 하나로 잇고, 띠는 주(행)가 시작·끝나는 곳에서
91
+ 둥글게 닫는다. 기간 설정 안내 문장과 결과 문장은 `summary`의 시작일·종료일 칸이 대신한다(지금 무엇을 고르는지 강조).
92
+ 월 이동은 `calendarRecipe.header.navIcon` 화살표 아이콘이다(이전 텍스트 ‹ ›는 약 8pt로 보였다).
93
+
85
94
  | 항목 | 값 | 근거 |
86
95
  | --- | --- | --- |
87
96
  | 크기 | 날짜 셀은 `medium` `control.minTouchTarget` 44, `large` `glyph.xxl` 44로 같고 7열이다. Web 격자는 최소 폭 7×44 = 308을 잡는다. 월 이동 버튼 44 | `src/calendar.ts`(`calendarRecipe`), `packages/react/src/styles.css` `.hjm-calendar*` |
88
- | 간격 | 월 머리 안 간격·세로 간격 `spacing.xs` 8. Native 구간 점은 날짜 아래 `spacing.xxs` 4, 점 지름 구간 안 4·시작·끝 6. 스토리는 격자 아래 문구를 `Stack gap="md"`(16)로 띄운다 | `packages/react-native/src/date-range.tsx`, `showcase/web/src/patterns/DateRange.stories.tsx` |
89
- | 순서·정렬 | 오버레이 없이 본문(폼·필터 영역·[Section](section.md)) 안에 격자를 펼쳐 둔다. 위에서 월 머리(이전 · 월 이름 · 다음) → 요일 → 날짜 격자. 선택 결과 문구와 제출 버튼은 격자 **아래**에 둔다(스토리는 `role="status"` 문구). 제출 버튼은 `isCompleteDateRange(value)`가 참일 때만 활성이다 | 같은 파일 |
97
+ | 간격 | 월 머리 안 간격·세로 간격 `spacing.xs` 8. `summary`·`presets`·격자 사이 `spacing.md` 16, 칩 사이 `spacing.xs` 8, 두 칸 사이 `spacing.xs` 8, 칸 안쪽 8×12, 칸 라벨·값 사이 4 | `dateRangeRecipe` |
98
+ | 색 | 띠 `primary` 15% tint(두 끝과 같은 브랜드색. 처음 쓴 `surfaceSelected`는 기본 팔레트에서 #f0f1f3 회색이라 구간이 거의 보이지 않았다), 두 끝 Calendar 선택 원(`action.brand`). 칸 바탕 `surface.sunken`, 다음에 고를 칸 테두리 2 `action.brand`, 빈 값 `content.tertiary` | `dateRangeRecipe` |
99
+ | 순서·정렬 | 오버레이 없이 본문(폼·필터 영역·[Section](section.md)) 안에 격자를 펼쳐 둔다. 위에서 `summary`(시작일 · 종료일) → `presets` 칩 줄 → 월 머리(이전 · 월 이름 · 다음) → 요일 → 날짜 격자. 제출 행동은 격자 **아래**에 둔다. 시트에서는 `footer`에 `BottomCTA` [보조(전체 날짜 등)][주(적용)] 한 줄로 두고 시트 높이는 `auto`로 맞춘다. `summary`를 쓰면 별도 안내·결과 문장을 두지 않는다. 제출 버튼은 `isCompleteDateRange(value)`가 참일 때만 활성이다 | 같은 파일 |
90
100
  | 고정·스크롤 | 본문과 함께 스크롤한다(오버레이 없음) | — |
91
101
  | 좁은 폭·큰 글자 | Web 격자 최소 폭 308보다 좁은 컨테이너에 두지 않는다 | `packages/react/src/styles.css` `.hjm-calendar__grid` |
92
102
 
@@ -102,7 +112,7 @@ import { DateRangePicker } from "@hjmds/react-native/date-range";
102
112
 
103
113
  | 항목 | Web | Native |
104
114
  | --- | --- | --- |
105
- | 구간 표시 | 셀 `data-range` band, 마우스 hover로 끝 미리보기 | 날짜 아래 점(고르는 중에는 반투명)과 셀 이름 |
115
+ | 구간 표시 | 두 끝 채운 원 + 띠(`data-selected`·`data-range-band`), 마우스 hover로 끝 미리보기 | 두 끝 채운 원 + 띠, 셀 이름 |
106
116
  | 배치 | `layoutStyle`(그 밖에 `className`) | 없음(감싸는 View로 배치) |
107
117
 
108
118
  ## 함정
@@ -102,3 +102,7 @@ import { EventAttendanceCard } from "@hjmds/react-native/social-compositions";
102
102
  | --- | --- | --- |
103
103
  | 진행 중 이름 | `aria-label={labels.pending}` | `loadingLabel={labels.pending}` |
104
104
  | 기본 아바타 | `Avatar aria-hidden` | `Avatar decorative` |
105
+
106
+ ### 회원 프로필 이동 (2026-10-25)
107
+
108
+ `participants[].avatarAction={label,onPress}`로 각 참여자 사진을 해당 회원 프로필에 연결한다. 참석 응답/캘린더 행동과 분리한다.
@@ -0,0 +1,109 @@
1
+ # FollowListScreen
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 실험
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.46.1 이후)
7
+ - 검토일: 2026-10-25
8
+ - 근거: 사용자 Instagram 팔로워 화면 요청, 양 renderer src/follow-list.tsx
9
+ - 스토리북: `실험/화면/소통/팔로워와 팔로잉`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 팔로워/팔로잉을 전환하고 사람을 검색하며 각 행에서 팔로우할 때 사용한다.
14
+ 2026-10-25 사용자 요청에 따라 [Instagram 참고 화면](https://www.soy502.com/articulo/asi-lucira-nueva-barra-busqueda-instagram)의 두 탭·검색·아바타·이름·행 우측 행동 구성을 참고했다. 최신 버전 전체를 복제한다는 의미는 아니다.
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 한 프로필의 팔로워 수와 행동 | FollowProfileCard |
20
+ | 대화 미리보기 목록 | ConversationListScreen |
21
+
22
+ FollowProfileCard는 한 프로필의 수치와 행동을 보여 주므로 다수 사람 탐색 화면을 대신하지 않는다.
23
+ ConversationListScreen은 대화 미리보기용이다. 이 구성은 ScreenLayout, Tabs, TabPanel, SearchField,
24
+ List/ListRow, Avatar, Button과 기존 action session을 합성하며 새 선택/요청 상태 엔진을 만들지 않는다.
25
+
26
+ ## 공개 이름과 import
27
+
28
+ | 이름 | 역할 | Web | Native |
29
+ | --- | --- | --- | --- |
30
+ | FollowListScreen | 관계 목록 화면 | @hjmds/react/follow-list | @hjmds/react-native/follow-list |
31
+
32
+ `@hjmds/react/follow-list`, `@hjmds/react-native/follow-list`에서
33
+ `FollowListScreen`, `FollowListScreenProps`, `FollowListPerson`을 가져온다.
34
+
35
+ - `title`, `exit`: 대상 사용자 이름과 뒤로가기. Web은 onClick, Native는 onPress.
36
+ - `tab`, `onTabChange`: followers 또는 following의 controlled 선택.
37
+ - `labels`: followers/following은 서버 수치를 포함한 지역화된 완성 문구다. tabs/search/clearSearch/searching/empty/follow/followed/pending/failed도 제품이 제공한다.
38
+ - `people`: 현재 탭·검색에 해당하는 id/username/name/following, 선택적 avatar/disabled.
39
+ - `toolbar`, `searchVisible`: 정렬·검색 도구 슬롯과 검색 필드 표시 여부(기본 true).
40
+ - `people[].message`: label/accessibilityLabel/onPress로 관계 버튼 대신 메시지 행동을 연결한다. `people[].action`은 삭제 아이콘·관계 메뉴 슬롯이다.
41
+ - `query`, `onQueryChange`: 검색 입력. 서버 검색·필터링·페이지네이션은 제품이 소유한다.
42
+ - `onOpenProfile(id)`: 행의 프로필 탐색. 행 오른쪽 버튼과 독립된 누르기 대상이다.
43
+ - `onFollowChange(id, following)`: 실제 요청과 서버 확정 people 갱신 후 resolve; 실패는 reject.
44
+ - `state`, `stateAction`, `footer`: 공통 조회 상태·재시도·추가 로드 연결. 빈 ready 목록은 labels.empty를 중앙 표시한다.
45
+
46
+ ## 최소 사용 예
47
+
48
+ ```tsx
49
+ // Web: currentPeople, labels, callbacks는 제품이 공급한다.
50
+ import { FollowListScreen } from "@hjmds/react/follow-list";
51
+ <FollowListScreen title={title} tab={tab} onTabChange={setTab} labels={labels}
52
+ people={currentPeople} query={query} onQueryChange={setQuery}
53
+ onOpenProfile={openProfile} onFollowChange={changeFollow} />;
54
+ ```
55
+
56
+ ```tsx
57
+ // Native: 같은 controlled 계약을 사용한다.
58
+ import { FollowListScreen } from "@hjmds/react-native/follow-list";
59
+ <FollowListScreen title={title} tab={tab} onTabChange={setTab} labels={labels}
60
+ people={currentPeople} query={query} onQueryChange={setQuery}
61
+ onOpenProfile={openProfile} onFollowChange={changeFollow} />;
62
+ ```
63
+
64
+ 양 Showcase의 `src/components/FollowListScreen.stories.tsx`는 공개 import로 실행하는 controlled 예제다.
65
+ 제품의 현재 탭 목록을 `people`, 조회 상태를 `state`로 공급한다.
66
+
67
+ ## 배치
68
+
69
+ | 항목 | 값 | 근거 |
70
+ | --- | --- | --- |
71
+ | 크기 | Avatar large 48, ListRow comfortable | avatarRecipe/listRowRecipe |
72
+ | 간격 | 탭·검색 spacing.sm 12, 행 행동 spacing.xs 8 | Stack props |
73
+ | 순서·정렬 | 헤더 → 탭 → 정렬·검색 도구 → 선택적 검색 → 목록, 행동은 우측 | FollowListScreen |
74
+ | 고정·스크롤 | 헤더·검색 유지, 목록은 screen 또는 제품 가상화 host | ScreenLayout.scroll |
75
+ | 좁은 폭·큰 글자 | 좁은 폭은 공통 행의 축소·줄바꿈 사용, 최대 글자 조건은 범위 제외 | 루트 지침 |
76
+
77
+ ```text
78
+ ← 사용자 이름
79
+ 팔로워 1,284 팔로잉 328
80
+ ━━━━━━━━━━━
81
+ [ 검색 ]
82
+ (아바타) username [팔로우]
83
+ 표시 이름
84
+ (아바타) username [메시지 보내기]
85
+ 표시 이름
86
+ ```
87
+
88
+ ScreenLayout 기본 본문 여백 안에서 List appearance=plain, separator=none을 사용한다.
89
+ 탭은 fitted/standard/underline, 검색과 탭 간격은 spacing.sm(12)이다.
90
+ 행은 ListRow comfortable/circle, leadingSize=large와 Avatar large(48), 우측 Button small을 쓴다.
91
+ 이미 팔로우한 회원에게는 `person.message`로 secondary 메시지 버튼을 제공한다. 미팔로우는 제품 테마의 primary다. 팔로우 취소는 제품 action의 더보기 메뉴로 분리한다. 독립 카드와 행 구분선은 없다.
92
+ 검색 입력으로 진입 즉시 키보드가 열리지 않도록 initialFocus=none을 기본으로 한다.
93
+
94
+ ## 꼭 지킬 것
95
+
96
+ 요청 중 해당 버튼은 로딩과 중복 입력 방지를 제공하며 확인된 관계 문구를 유지한다.
97
+ 실패하면 같은 행 아래 오류를 알리고 기존 관계를 유지한다. 인물 id가 바뀌거나 행이 사라지면
98
+ 기존 action session이 해제된다. 탭/검색을 바꿀 때 query 초기화·응답 순서·캐시 정책은 제품 소유다.
99
+ 실행 예제는 양 Showcase의 `src/components/FollowListScreen.stories.tsx`다.
100
+ Web/Native 구현과 테스트는 npm 게시·소비 앱 채택·기기 확인과 별도다.
101
+
102
+ ## 제품 연결
103
+
104
+ `people[].showFollow=false`는 본인·관계 미확인 시 행동을 숨긴다. `followLabel`과
105
+ `followAccessibilityLabel`은 맞팔로우 등 제품 문구, `action`은 삭제 확인 버튼 등 추가 행동이다.
106
+ HJM이 두 행동을 `Stack axis=inline gap=xs(8)`로 배치한다.
107
+ `notice`는 검색 아래 비차단 조회 오류에 쓰며, Web `href/renderLink`는 링크 탐색을 보존한다.
108
+ Native `renderPeople(people, renderPerson)`은 FlatList host를 연결한다. `scroll=content`를 함께 쓰고
109
+ renderPerson을 호출해 공통 행과 요청 상태를 유지한다. 이름 보조줄 `name`은 없으면 생략한다.
@@ -98,3 +98,7 @@ import { FollowProfileCard } from "@hjmds/react-native/social-compositions";
98
98
  | 진행 중 이름 | 버튼 `aria-label`=pending | Button `loadingLabel`=pending |
99
99
  | 기본 아바타 | `Avatar aria-hidden` | `Avatar decorative` |
100
100
  | 수치 전환 의존성 | `statistic-motion`의 `@number-flow/react` peer 필요 | 기존 ContentTransition |
101
+
102
+ ### 회원 프로필 이동 (2026-10-25)
103
+
104
+ `avatarAction={label,onPress}`로 카드의 회원 사진을 해당 회원 프로필 화면에 연결한다. 팔로우 버튼은 별도 행동이다.
@@ -161,3 +161,9 @@ Web도 Native와 같이 `trailingAction`을 지원한다. 이 슬롯은 행의 l
161
161
  - 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로 측정됐다.
162
162
  - 글자를 여백 선에, 표면은 바깥으로: 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)로 옮겼다.
163
163
  - 구분선이 행의 글자 열을 따름: 2026-10-17 사용자 보고(leading 없는 행에서 구분선이 중간에서 잘림). 위 52는 leading 있는 행의 글자 위치일 뿐이라 leading 없는 행(글자 16)에서는 구분선이 68에서 시작했다. 행이 leading 여부를 List에 알리도록 바꿔 구분선 시작이 각 행의 글자 열과 같아졌다([List 근거](list.md#근거)).
164
+
165
+ ### 회원 프로필 이동 (2026-10-25)
166
+
167
+ `leadingAction={label,onPress}`를 주면 leading 아바타를 본문 행과 별도 컨트롤로 렌더링한다. `onClick`/`onPress`/`href` 및 `trailingAction`과 중첩되지 않는다. NotificationItem도 같은 prop을 받는다.
168
+
169
+ 회원 아바타가 놓인 `leadingShape="circle"` 행은 `leadingSize="large"`(48) 프레임을 기본으로 쓴다(2026-10-25 사용자 크기 통일). 촘촘한 기존 목록은 `leadingSize="default"`로 40 프레임을 유지한다. 아이콘 등 다른 leading의 기본 프레임은 유지한다.
@@ -52,6 +52,18 @@ commentMenu={{ presentation: "anchored", label: commentMenuLabel,
52
52
  dismissLabel: t("common.close"), items: allowedItems, onAction: handleCommentAction }}
53
53
  ```
54
54
 
55
+ ### 관리 메뉴의 짧은 헤더 — 개발 소스, 미게시 (2026-10-25)
56
+
57
+ 번뚝의 긴 분기 이력·반복 설명이 하단 메뉴를 과도하게 키운 사용자 관측에 따라,
58
+ 양 renderer에 `title`, `description`, `dismissPlacement="header" | "footer" | "none"`를 제공한다.
59
+ 2026-10-25 사용자의 관리 메뉴 상단 제거 요청으로 `none`을 추가했다. 제목·보조 설명을 생략하고 `none`을 지정하면 목록만 표시하며 바깥 누름·Escape/back으로 닫는다. 접근성 이름과 `dismissLabel`은 유지한다.
60
+ 기본은 기존 `footer`다. `header`는 grouped/bottom에서 제목 옆에 지역화한
61
+ `dismissLabel`의 ✕를 두고 하단 닫기 버튼을 생략한다. 바깥 누름·Escape/back과
62
+ 닫힌 후 실행 계약은 유지한다. `anchored`에는 헤더를 표시하지 않는다.
63
+ 보조 설명은 대상 이름·현재 버전 등 짧은 맥락에 쓰며 한 줄 넘침은 말줄임한다.
64
+ 분기 이력은 기록 화면, 현재 설정값은 trailing, 실행 결과가 불명확한 행동만 description으로 전달한다.
65
+ 행·아이콘·본문·안전 영역과 스크롤은 기존 Menu가 소유한다.
66
+
55
67
  `bottom`도 안정적인 id의 행동/선택 목록이면 Menu다. 입력 본문이 생기면 Dialog·Sheet,
56
68
  확인 문구와 버튼만 필요하면 AlertDialog를 사용한다. Menu가 닫힌 후 여는 원칙은 세 표현 모두 동일하다.
57
69
  실행 예제는 양 Showcase의 Menu `Grouped`·`Anchored`·`Bottom`이다.
@@ -88,6 +88,10 @@ import { Notice } from "@hjmds/react-native/feedback";
88
88
 
89
89
  ## 배치
90
90
 
91
+ 2026-10-25 개편: 다에리 오프라인 오류에서 붉은 면·붉은 테두리·붉은 제목이 겹치고, 재시도 버튼이 문구 아래 따로 놓여
92
+ 오류 한 줄이 화면에서 가장 시끄러운 블록이 됐다(사용자 지적). 테두리를 없애고 tone은 면 tint·앞쪽 막대·아이콘에만 둔다.
93
+ 제목은 본문색으로 읽히고, 행동은 Native도 Web처럼 같은 줄 끝에 둔다.
94
+
91
95
  2026-10-20 선택의창 지도 위에서 Native 안내 배경의 alpha가 지도 지명·도로를 그대로 비춰 문구와 겹쳤다.
92
96
  `panel`은 불투명한 테마 바탕 위에 tone tint를 합성한다(Web의 기존 color-mix와 같은 방식).
93
97
  지도·사진 위에서도 아래 콘텐츠가 비치지 않는다. `status`는 기존처럼 배경 없이 유지한다.
@@ -95,11 +99,12 @@ import { Notice } from "@hjmds/react-native/feedback";
95
99
 
96
100
  | 항목 | 값 | 근거 |
97
101
  | --- | --- | --- |
98
- | 크기 | 본문 폭을 꽉 채운다. 아이콘 20(`glyph.sm`), 테두리 1(`stroke.default`), radius `radius.md` 12 | `noticeRecipe` |
102
+ | 크기 | 본문 폭을 꽉 채운다. 아이콘 20(`glyph.sm`), 테두리 없음, 앞쪽 tone 막대 4(`accentBar`), radius `radius.md` 12 | `noticeRecipe` |
103
+ | 색 | 면은 tone tint(info는 중립 `surface.sunken`), 막대·아이콘은 tone 색, 제목은 `content.primary`, 설명은 `content.body`. 제목을 tone 색으로 칠하지 않는다 | `noticeRecipe.tones`·`title.color` |
99
104
  | 간격 | 안쪽 `spacing.md` 16, 아이콘·문구·행동 사이 `spacing.xs` 8, 제목·설명 사이 `spacing.xxs` 4. 이웃 블록과는 `layout.contentGap` 16 | `noticeRecipe`, `.hjm-notice*` |
100
- | 순서·정렬 | 아이콘 — 제목·설명 — 행동. 행동은 하나, `Button` `size="small"` `tone="secondary"`(또는 `ghost`), `primary` 금지. Web은 행동이 같은 줄 끝, Native는 행동이 문구 줄 **아래**. Web은 제목·설명 전체 묶음과 행동의 세로 중심을 행 높이의 중앙에 맞춘다. 첫 줄에 버튼 높이 기반 여백을 더하지 않는다. Native 아이콘 칸은 제목 첫 줄 높이(토큰×글자 배율) | `.hjm-notice`·`.hjm-notice__icon`·`.hjm-notice__action`, `react-native/src/feedback.tsx` |
105
+ | 순서·정렬 | 아이콘 — 제목·설명 — 행동. 행동은 하나, `Button` `size="small"` `tone="secondary"`(또는 `ghost`), `primary` 금지. 두 플랫폼 모두 행동은 문구와 같은 줄 끝이며, 제목·설명 묶음과 행동의 세로 중심을 행 중앙에 맞춘다. 첫 줄에 버튼 높이 기반 여백을 더하지 않는다. Native 아이콘 칸은 제목 첫 줄 높이(토큰×글자 배율) | `.hjm-notice`·`.hjm-notice__icon`·`.hjm-notice__action`, `react-native/src/feedback.tsx` |
101
106
  | 고정·스크롤 | 본문 흐름 안. 먼저 읽어야 하는 경고·입력 오류는 영향을 받는 내용 바로 위(화면 상태는 제목 아래, 폼 검증은 제출 버튼 위). 제출 후 성공·실패 결과는 구성 지침이 지정한 행동 인접 위치에 둔다. 떠 있거나 고정되지 않고 함께 스크롤되며 여러 개 쌓지 않는다 | — |
102
- | 좁은 폭·큰 글자 | Web은 문구 칸이 60% 아래로 줄면 행동이 다음 줄로 감긴다. 제목·설명은 줄바꿈된다 | `.hjm-notice__content`(`flex: 1 1 60%`) |
107
+ | 좁은 폭·큰 글자 | 문구 칸이 60%(`contentMinWidthPercent`) 아래로 줄면 행동이 다음 줄로 감긴다. 제목·설명은 줄바꿈된다 | `.hjm-notice__content`(`flex: 1 1 60%`), Native `flexBasis` |
103
108
 
104
109
  ## 꼭 지킬 것
105
110
 
@@ -95,3 +95,13 @@ import { NotificationInboxScreen } from "@hjmds/react-native/screens";
95
95
  | --- | --- | --- |
96
96
  | 배치·식별 | `layoutStyle`, `className`, `as` | `layoutStyle`, `testID` |
97
97
  | 스크롤 | 없음 | `scrollRef`, `scrollProps`(`refreshControl` 등) |
98
+
99
+
100
+ ## 행 종류와 여백 — 개발 소스, 미게시 (2026-10-25)
101
+
102
+ `rowPresentation="item"`(기본)은 NotificationItem/ListRow용으로 화면이 여백을 소유한다.
103
+ 아바타·썸네일을 가진 NotificationRow에는 `rowPresentation="preview"`를 사용한다.
104
+ 이때 공통 화면이 body-edge를 선택하며 제품은 contentInset을 별도로 조정하지 않는다.
105
+ 같은 알림함에 서로 다른 행 종류가 있어 기본값을 일괄 변경하지 않는다.
106
+ 필터·notice가 모두 없으면 빈 notice 영역을 만들지 않는다.
107
+ 번뚝의 화면 여백과 행 여백 중복 및 빈 notice 간격을 공통 합성에서 예방하기 위한 변경이다.
@@ -91,3 +91,7 @@ HJM이 44px 아바타·썸네일, 20px 유형 배지, 2줄 미리보기와 간
91
91
  ## 관련 지침
92
92
 
93
93
  [알림 화면](../screens/common-inbox.md), [ConversationRow](conversation-row.md), [ListRow](list-row.md).
94
+
95
+ ### 회원 프로필 이동 (2026-10-25)
96
+
97
+ `avatarAction={label,onPress}`는 발신자 프로필, `action`은 알림 목적지다. 두 컨트롤은 형제로 렌더링되며 프로필 누르기로 알림 읽음 처리를 실행하지 않는다.
@@ -67,6 +67,7 @@ import { PageIndicator } from "@hjmds/react-native/carousel";
67
67
  | `count` | 1 이상 정수 | 필수 | 페이지 수. 밖이면 `RangeError` |
68
68
  | `maxVisibleDots` | 1 이상 정수 | 전체 | 현재 페이지를 포함하는 연속 창만 표시. 이름·이동은 전체 페이지 기준이며 창 크기는 페이지 수를 넘지 않는다(checkout 미게시) |
69
69
  | `interactive` | `boolean` | `true` | `false`이면 위치만 표시하며 점은 누름 대상/키보드/조절 행동을 만들지 않는다. 별도 이전·다음 컨트롤이 있는 작은 표시용(checkout 미게시) |
70
+ | `density` | `comfortable / compact` | `comfortable` | compact는 점 중심 간격과 가로 누름 영역을 24로 줄이고 세로 44는 유지한다. 좁은 상단 표시용 개발 API |
70
71
  | `currentIndex` | 0부터 `count - 1` | 필수 | 페이저가 확정한 현재 페이지. 컴포넌트는 따로 기억하지 않는다 |
71
72
  | `onCurrentIndexChange` | `(index: number) => void` | 필수 | 점 누름·Web 화살표 키·Native 조절 동작이 요청한 페이지. 처음·끝에서는 같은 값이 다시 올 수 있다(넘어가지 않고 멈춘다) |
72
73
  | `composeAccessibleName` | `({ position, total }) => string` | 필수 | 점 하나의 이름. 숫자 문법은 제품 문구다. 빈 문자열은 `TypeError` |