@hjmds/design-contracts 1.47.0 → 1.49.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 (56) hide show
  1. package/dist/card.d.ts +5 -0
  2. package/dist/card.d.ts.map +1 -1
  3. package/dist/card.js +3 -0
  4. package/dist/card.js.map +1 -1
  5. package/dist/catalog.d.ts +56 -18
  6. package/dist/catalog.d.ts.map +1 -1
  7. package/dist/catalog.js +15 -11
  8. package/dist/catalog.js.map +1 -1
  9. package/dist/component-recipes.d.ts +5 -1
  10. package/dist/component-recipes.d.ts.map +1 -1
  11. package/dist/component-recipes.js +4 -2
  12. package/dist/component-recipes.js.map +1 -1
  13. package/dist/data-table.d.ts +12 -0
  14. package/dist/data-table.d.ts.map +1 -1
  15. package/dist/data-table.js +2 -0
  16. package/dist/data-table.js.map +1 -1
  17. package/dist/drawing-editor.d.ts +96 -0
  18. package/dist/drawing-editor.d.ts.map +1 -0
  19. package/dist/drawing-editor.js +43 -0
  20. package/dist/drawing-editor.js.map +1 -0
  21. package/dist/share-sheet.d.ts +115 -0
  22. package/dist/share-sheet.d.ts.map +1 -0
  23. package/dist/share-sheet.js +62 -0
  24. package/dist/share-sheet.js.map +1 -0
  25. package/dist/timeline.d.ts +7 -0
  26. package/dist/timeline.d.ts.map +1 -1
  27. package/dist/timeline.js +7 -0
  28. package/dist/timeline.js.map +1 -1
  29. package/dist/version.d.ts +1 -1
  30. package/dist/version.js +1 -1
  31. package/dist/version.js.map +1 -1
  32. package/docs/dialog.md +2 -3
  33. package/docs/drawing-editor.md +14 -0
  34. package/docs/generated/component-maturity.md +13 -12
  35. package/docs/generated/renderer-evidence.json +122 -22
  36. package/docs/generated/renderer-evidence.md +18 -16
  37. package/docs/generated/showcase-manifest.json +70 -19
  38. package/docs/optional-adapters.md +21 -5
  39. package/docs/stable-core.md +37 -0
  40. package/docs/timeline.md +13 -0
  41. package/docs/usage/README.md +13 -11
  42. package/docs/usage/components/badge.md +10 -1
  43. package/docs/usage/components/card.md +1 -0
  44. package/docs/usage/components/data-table.md +2 -0
  45. package/docs/usage/components/dialog.md +3 -3
  46. package/docs/usage/components/drawing-editor.md +133 -0
  47. package/docs/usage/components/follow-list-screen.md +3 -3
  48. package/docs/usage/components/image-choice-group.md +1 -1
  49. package/docs/usage/components/share-sheet.md +139 -0
  50. package/docs/usage/components/shared-transition-element.md +1 -1
  51. package/docs/usage/components/sheet.md +13 -1
  52. package/docs/usage/components/timeline.md +27 -3
  53. package/docs/usage/compositions/interaction-adapters.md +2 -2
  54. package/docs/usage/compositions/optional-adapters.md +2 -2
  55. package/docs/usage/screens/common-follow-list.md +3 -3
  56. package/package.json +12 -1
package/docs/timeline.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Timeline contract
2
2
 
3
+ ## 기본 여행 일지 (2026-10-27, 개발 소스·미게시)
4
+
5
+ 사용자 승인 시안 「여행 일지」를 기본형으로 지정했다. 기본 `variant="journal"`은 배경 박스 없이
6
+ 8px dot·1px connector와 위아래 16px 여백을 사용한다. 수동 기록은 전체 설명과 독립 행동을 유지하고,
7
+ 선택형은 선택한 기록의 설명만 펼친다. 같은 기록을 다시 누르면 지도 선택을 유지하면서 상세를 접는다.
8
+ 새 선택(외부 지도·날짜 포함)은 해당 기록을 펼친다. 제목·시각은 남는다.
9
+
10
+ Web은 disclosure button 목록과 `aria-expanded`·`aria-controls`, Native는 selected·expanded 상태를 제공한다.
11
+ 선택 색을 진행 상태로 해석하지 않으며 사건 tone과 행동/가상화 경계는 보존한다.
12
+ 이전 listbox·면/테두리 표현은 `variant="record"`로 명시할 때 제공한다.
13
+ 이하 2026-10-23까지의 선택 면/listbox 설명은 이 호환 표현의 이력이다.
14
+ 상세 소비·영향 범위는 [사용 지침](usage/components/timeline.md)을 따른다.
15
+
3
16
  ## 문제
4
17
 
5
18
  이미 일어난 일들을 시간 순서로 보여준다 — 야잘알의 경기 플레이 기록(PBP, "1회 초 안타 →
@@ -7,7 +7,7 @@
7
7
  [선택 기준](SELECTION.md)으로 그대로 사용·공개 슬롯 확장·공통 API 보완·제품 고유 기능 연결을 고른다. 설치 버전의 지침을 본다: `node_modules/@hjmds/design-contracts/docs/usage/`.
8
8
  이름을 모르면 [쉬운 말로 찾고 비교하기](FINDING.md)에서 일상 표현과 선택 기준·요청 예시로 시작한다.
9
9
  제공 기준은 확인한 기본 API 버전이며 최초 출시 버전이 아니다. 구성·화면은 각 API 지침의 제공 버전을 확인한다.
10
- API 안정성은 1.46.1의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
10
+ API 안정성은 1.48.0의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
11
11
  Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품 반영을 의미하지 않는다.
12
12
  `분류`는 Storybook 제목 `<배포|실험>/<단계>/<분류>/<항목>`의 셋째 마디이고, 토큰·구성·화면은 Storybook 메뉴와 같은 순서다.
13
13
 
@@ -57,8 +57,8 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
57
57
  | [Card](components/card.md) | — | 인용문과 출처를 묶으면 미게시 동반 API `QuoteCard`를 사용한다. | | | Web stable / Native stable | |
58
58
  | [Carousel](components/carousel.md) | 데이터 표시 | 한 번에 카드 하나만 보이고 사용자가 순서대로 넘겨 보는 유한한 묶음에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
59
59
  | [Celebration](components/celebration.md) | 구성/직접 조작과 모션 | 목표 달성, 첫 완료처럼 드물게 일어나는 성공 순간에 한 번 터지는 색종이 효과에 쓴다. | 1.12.1 | Web · Native | 개별 API 계약 참조 | 배포 |
60
- | [Chart](components/chart.md) | 데이터 표시 | 수치의 흐름·비교·몫·모양·관계를 그림으로 읽게 할 때 쓴다. | 1.21.0 | Web · Native | Web beta / Native beta | 배포 |
61
- | [ChatDateSeparator](components/chat-date-separator.md) | 데이터 표시 | 대화 타임라인에서 날짜(또는 오래 쉰 뒤의 시각)가 바뀌는 자리에 가운데 정렬 구분 줄을 둘 때 쓴다. | 1.31.0 | Web · Native | Web beta / Native beta | 배포 |
60
+ | [Chart](components/chart.md) | 데이터 표시 | 수치의 흐름·비교·몫·모양·관계를 그림으로 읽게 할 때 쓴다. | 1.21.0 | Web · Native | Web stable / Native stable | 배포 |
61
+ | [ChatDateSeparator](components/chat-date-separator.md) | 데이터 표시 | 대화 타임라인에서 날짜(또는 오래 쉰 뒤의 시각)가 바뀌는 자리에 가운데 정렬 구분 줄을 둘 때 쓴다. | 1.31.0 | Web · Native | Web stable / Native stable | 배포 |
62
62
  | [ChatMessage](components/chat-message.md) | 데이터 표시 | DM·대화 타임라인의 메시지 한 개에 쓴다. | 1.17.1 | Web · Native | Web stable / Native stable | 배포 |
63
63
  | [ChatRoomActions](components/chat-room-actions.md) | 구성/입력과 작성 | 나가기·내 기록 삭제·방 삭제를 구분해 확인한다. | 1.45.0 | Web · Native | 개별 API 계약 참조 | 배포 |
64
64
  | [ChatRoomCreate](components/chat-room-create.md) | 구성/입력과 작성 | 참여자를 검색·선택하고 사진·이름을 정해 방을 개설한다. | 1.45.0 | Web · Native | 개별 API 계약 참조 | 배포 |
@@ -74,7 +74,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
74
74
  | [CodeBlock](components/code-block.md) | 데이터 표시 | 코드·명령·설정 조각을 읽기 전용으로 보여 주고 사용자가 선택·복사하게 할 때 쓴다. | 1.12.1 | Web · Native | 개별 API 계약 참조 | 배포 |
75
75
  | [CohortRetentionTable](components/cohort-retention-table.md) | 구성/정보 표시 | 가입 시점(코호트)별로 기간마다 남은 인원 비율을 직사각형 표와 heat 색으로 비교할 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
76
76
  | [Collapsible](components/collapsible.md) | 데이터 표시 | 이웃 없이 혼자 접었다 펴는 한 덩어리에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
77
- | [ColorPicker](components/color-picker.md) | 입력 | 사용자가 콘텐츠 색(라벨 색, 태그 색, 테마 편집기의 사용자 값 등)을 sRGB HEX로 고르는 폼 입력에 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
77
+ | [ColorPicker](components/color-picker.md) | 입력 | 사용자가 콘텐츠 색(라벨 색, 태그 색, 손그림 펜 색, 테마 편집기의 사용자 값 등)을 sRGB HEX로 고를 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
78
78
  | [Combobox](components/combobox.md) | 입력 | 주어진 목록에서 하나를 고르는데 목록이 길어 입력으로 좁혀야 할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
79
79
  | [CommandPalette](components/command-palette.md) | 오버레이 | ⌘K 스타일로 앱 전체의 **행동**을 검색해 실행하는 모달에 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
80
80
  | [CommentThreadRow](components/comment-thread-row.md) | 데이터 표시 | 가상화 목록(FlatList·가상 스크롤러)처럼 제품이 댓글 목록을 직접 그려야 할 때 댓글 한 행을 그린다. | 1.23.0 | Web · Native | Web stable / Native stable | 배포 |
@@ -88,13 +88,14 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
88
88
  | [ConversionFunnel](components/conversion-funnel.md) | 구성/정보 표시 | 순차 단계(열기 → 작성 → 저장 등)의 인원을 가로 막대로 보여 주고, 단계를 골라 전체 대비·직전 대비 전환율과 이탈 수를 문구로 읽을 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
89
89
  | [CorrelationMatrix](components/correlation-matrix.md) | 구성/정보 표시 | 여러 지표 사이의 상관계수(-1~1)를 정사각 대칭 표와 양·음 두 방향 heat 색으로 보여 줄 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
90
90
  | [CounterBadge](components/counter-badge.md) | 데이터 표시 | 읽지 않은 알림·메시지·장바구니 수처럼 **셀 수 있는 개수**를 아이콘·행 옆에 작게 보일 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
91
- | [DataTable](components/data-table.md) | 데이터 표시 | 여러 행의 데이터를 열로 맞춰 훑고, 열 기준으로 정렬하거나 행을 골라 일괄 작업할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native beta | 배포 |
91
+ | [DataTable](components/data-table.md) | 데이터 표시 | 여러 행의 데이터를 열로 맞춰 훑고, 열 기준으로 정렬하거나 행을 골라 일괄 작업할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
92
92
  | [DatePicker](components/date-picker.md) | 입력 | 폼·필터 자리에서 날짜 **하나**를 고를 때 쓴다(생년월일, 방문일, 시작일 필터). 평소에는 필드 트리거만 보이고, 누르면 Web은 필드에 붙은 팝오버, Native는 Sheet 안에 같은 달력 격자를 연다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
93
93
  | [DateRangePicker](components/date-range-picker.md) | 입력 | 시작~끝 날짜 **구간**을 고를 때 쓴다(통계 기간, 예약, 검색 필터). 필드 트리거나 오버레이 없이 달력 격자를 그 자리에 펼쳐 둔다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
94
94
  | [DescriptionList](components/description-list.md) | 데이터 표시 | 라벨-값 쌍의 묶음을 보여 줄 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
95
95
  | [DesignSystemProvider](components/design-system-provider.md) | 기반 기능 | 앱 루트에 한 번 둔다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
96
96
  | [Dialog](components/dialog.md) | 오버레이 | 화면 흐름을 잠시 멈추고 사용자의 주의가 필요한 **짧은 작업**에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
97
97
  | [Divider](components/divider.md) | 레이아웃 | 서로 다른 내용 묶음 사이에 얇은 구분선이 필요할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
98
+ | [DrawingEditor](components/drawing-editor.md) | 입력 | 펜·지우개로 그림을 그리고 이미지로 저장하는 전체 편집 화면에 쓴다. | 미게시(1.48.0 이후) | Web · Native | Web beta / Native beta | 실험 |
98
99
  | [EditorScreen](components/editor-screen.md) | 레이아웃 | 글쓰기·프로필 수정처럼 한 화면 전체가 편집 흐름일 때 쓴다. | 1.17.1 | Web · Native | Web stable / Native stable | 배포 |
99
100
  | [EffectSurface](components/effect-surface.md) | 시각 효과 | 환영·온보딩·빈 히어로처럼 분위기를 주는 배경이 필요할 때 내용 뒤에 장식 레이어(mesh·glow·grain)를 깐다. | 1.12.1 | Web · Native | 개별 API 계약 참조 | 배포 |
100
101
  | [EmptyState](components/empty-state.md) | 상태와 알림 | 목록이 비었거나 검색 결과가 0건이라 **아직 없음**을 알릴 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
@@ -104,7 +105,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
104
105
  | [FilePicker](components/file-picker.md) | 입력 | 사용자가 업로드할 로컬 파일을 고르게 할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
105
106
  | [FilterRecovery](components/filter-recovery.md) | 구성/피드백과 복구 | 검색·필터 결과가 비었을 때 실제로 결과가 생기는 필터 완화 제안을 보여 주고, 사용자가 고른 하나를 명시적으로 적용할 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
106
107
  | [FloatingActionButton](components/floating-action-button.md) | 동작 | 목록·피드처럼 스크롤되는 콘텐츠 위에 떠 있는 **단일 생성 행동**(새 기록 추가, 새 글 작성)에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
107
- | [FollowListScreen](components/follow-list-screen.md) | 화면/소통 | 팔로워/팔로잉을 전환하고 사람을 검색하며 각 행에서 팔로우할 때 사용한다. | 미게시(1.46.1 이후) | Web · Native | 개별 API 계약 참조 | 실험 |
108
+ | [FollowListScreen](components/follow-list-screen.md) | 화면/소통 | 팔로워/팔로잉을 전환하고 사람을 검색하며 각 행에서 팔로우할 때 사용한다. | 1.47.0 | Web · Native | 개별 API 계약 참조 | 배포 |
108
109
  | [FollowProfileCard](components/follow-profile-card.md) | 구성/정보 표시 | 프로필 카드에서 팔로우·팔로잉 전환과 팔로워 수를 실제 mutation 결과와 함께 보여 줄 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
109
110
  | [Form](components/form.md) | 입력 | 여러 Field를 한 화면에 쌓고 한 번에 제출할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
110
111
  | [Grid](components/grid.md) | 레이아웃 | 카드·타일처럼 같은 모양의 자식을 창 크기에 따라 열 수를 바꿔 배치할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
@@ -114,7 +115,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
114
115
  | [IconButton](components/icon-button.md) | 동작 | 보이는 글자 없이 아이콘만으로 표시하는 행동에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
115
116
  | [IconTile](components/icon-tile.md) | 데이터 표시 | 프로필·런처 격자의 아이콘 위/제목 아래 항목에 쓴다. | 1.26.0 | Web · Native | Web stable / Native stable | 배포 |
116
117
  | [Image](components/image.md) | 데이터 표시 | 원본 크기를 아는 사진·차트 이미지를 로드 전에 자리를 잡아 두고, 실패해도 의미를 잃지 않게 보여 줄 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
117
- | [ImageChoiceGroup](components/image-choice-group.md) | 입력 | 아바타 얼굴, 프로필 사진, 모음에 넣을 사진처럼 **그림을 보고 고르는** 단일·다중 선택에 쓴다. | 미게시(1.26.1 이후) | Web · Native | Web beta / Native beta | 배포 |
118
+ | [ImageChoiceGroup](components/image-choice-group.md) | 입력 | 아바타 얼굴, 프로필 사진, 모음에 넣을 사진처럼 **그림을 보고 고르는** 단일·다중 선택에 쓴다. | 미게시(1.26.1 이후) | Web · Native | Web stable / Native stable | 배포 |
118
119
  | [ImageComparison](components/image-comparison.md) | 데이터 표시 | 같은 좌표와 비율의 두 이미지를 겹쳐 변화량을 비교할 때 쓴다. | 1.14.0 | Web · Native | 개별 API 계약 참조 | 배포 |
119
120
  | [ImportFieldMapping](components/import-field-mapping.md) | 구성/입력과 작성 | CSV 등 원본 열을 제품의 대상 필드에 연결하고, 검증을 통과한 연결만 가져오기로 넘길 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
120
121
  | [InlineEdit](components/inline-edit.md) | 구성/입력과 작성 | 목록·상세의 제목처럼 이미 확정된 짧은 값 하나를 그 자리에서 명시적으로 편집·저장·취소할 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
@@ -145,8 +146,8 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
145
146
  | [OnboardingScreen](components/onboarding-screen.md) | 레이아웃 | 첫 실행 소개·초기 설정처럼 **몇 단계를 차례로 넘기는 화면**에 쓴다. | 1.14.0 | Web · Native | Web stable / Native stable | 배포 |
146
147
  | [OtpField](components/otp-field.md) | 입력 | 문자·메일로 받은 **숫자 인증번호**를 칸 모양으로 입력받을 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
147
148
  | [OverviewScreen](components/overview-screen.md) | 레이아웃 | 같은 데이터와 기능을 유지하면서 테마별 행·카드·격자와 도구 배치를 선택하는 목록 화면에 쓴다. | 1.15.0 | Web · Native | 개별 API 계약 참조 | 배포 |
148
- | [PageIndicator](components/page-indicator.md) | 탐색 | 제품이 직접 만든 가로 페이저(Native 페이징 `FlatList`, Web scroll-snap 줄) 아래에 지금 몇 번째 페이지인지 점으로 보여 주고, 점을 눌러 그 페이지로 옮길 때 쓴다. | 1.31.0 | Web · Native | Web beta / Native beta | 배포 |
149
- | [Pagination](components/pagination.md) | 탐색 | 총 개수(또는 총 페이지 수)가 정해진 결과 집합에서 사용자가 **임의의 페이지로 바로 이동**해야 할 때 Web·Native에서 쓴다. | 1.12.1 | Web · Native | Web stable / Native beta | 배포 |
149
+ | [PageIndicator](components/page-indicator.md) | 탐색 | 제품이 직접 만든 가로 페이저(Native 페이징 `FlatList`, Web scroll-snap 줄) 아래에 지금 몇 번째 페이지인지 점으로 보여 주고, 점을 눌러 그 페이지로 옮길 때 쓴다. | 1.31.0 | Web · Native | Web stable / Native stable | 배포 |
150
+ | [Pagination](components/pagination.md) | 탐색 | 총 개수(또는 총 페이지 수)가 정해진 결과 집합에서 사용자가 **임의의 페이지로 바로 이동**해야 할 때 Web·Native에서 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
150
151
  | [PasswordField](components/password-field.md) | 입력 | 비밀번호를 입력받고, 필요할 때만 값을 눈으로 확인하게 할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
151
152
  | [PasswordRulesField](components/password-rules-field.md) | 구성/입력과 작성 | 새 비밀번호 입력 아래에 제품이 정한 규칙의 충족 개수와 규칙별 상태를 함께 보여 줄 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
152
153
  | [PermissionScreen](components/permission-screen.md) | 레이아웃 | 카메라·위치·알림 같은 권한이 **왜 필요한지 설명하고 다음 행동을 고르게 하는** 화면에 쓴다. | 1.17.1 | Web · Native | Web stable / Native stable | 배포 |
@@ -176,6 +177,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
176
177
  | [SettingsScreen](components/settings-screen.md) | 레이아웃 | 앱의 설정 화면 전체에 쓴다. | 1.17.1 | Web · Native | Web stable / Native stable | 배포 |
177
178
  | [SharedTransitionElement](components/shared-transition-element.md) | 구성/직접 조작과 모션 | 목록의 카드(사진·썸네일)를 눌러 상세 화면으로 갈 때, 같은 요소가 두 화면 사이에서 확대·축소되어 이어지는 공유 요소 전환에 쓴다. | 1.12.1 | Native | 개별 API 계약 참조 | 배포 |
178
179
  | [SharedTransitionScreen](components/shared-transition-screen.md) | 구성/직접 조작과 모션 | `createHjmTransitionStack()`으로 만든 stack에서 공유 요소 전환을 쓸 때, **각 라우트 본문**을 감싼다. | 1.12.1 | Native | 개별 API 계약 참조 | 배포 |
180
+ | [ShareSheet](components/share-sheet.md) | 오버레이 | 링크 하나를 **앱 안의 받는 사람에게 보내거나(선택), 링크를 복사하거나, 기기 공유 시트로 내보내는** 공유 시트에 쓴다. | 미게시(1.47.0 이후) | Web · Native | 개별 API 계약 참조 | 실험 |
179
181
  | [Sheet](components/sheet.md) | 오버레이 | 현재 화면 위에 모달로 띄우는 보조 작업 패널에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
180
182
  | [ShortcutRecorder](components/shortcut-recorder.md) | 구성/직접 조작과 모션 | Web 앱에서 사용자가 수정키+키 조합을 눌러 기록하고, 확인 뒤 실제 단축키로 등록하게 할 때 쓴다. | 1.21.0 | Web | 개별 API 계약 참조 | 배포 |
181
183
  | [Sidebar](components/sidebar.md) | 탐색 | 데스크톱 Web의 세로 내비게이션에 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
@@ -216,7 +218,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
216
218
  | [TransferList](components/transfer-list.md) | 입력 | 한 항목 집합을 두 목록으로 나누고 사용자가 항목을 오가게 할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
217
219
  | [TransferTask](components/transfer-task.md) | 구성/피드백과 복구 | 업로드·다운로드·동기화처럼 실제 전송 계층이 진행량을 알려 주는 작업에 시작·취소·진행률·결과를 한 묶음으로 붙인다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
218
220
  | [Tree](components/tree.md) | 데이터 표시 | 깊이가 정해지지 않은 계층 데이터를 펼치고 접으며 탐색하고, 그 안에서 하나 또는 여럿을 고를 때 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
219
- | [TypingIndicator](components/typing-indicator.md) | 상태와 알림 | 대화 타임라인에서 상대가 지금 입력 중임을 보여 줄 때 쓴다. | 1.31.0 | Web · Native | Web beta / Native beta | 배포 |
221
+ | [TypingIndicator](components/typing-indicator.md) | 상태와 알림 | 대화 타임라인에서 상대가 지금 입력 중임을 보여 줄 때 쓴다. | 1.31.0 | Web · Native | Web stable / Native stable | 배포 |
220
222
  | [UploadItem](components/upload-item.md) | 데이터 표시 | 사용자가 고른 파일 **한 개**의 업로드 상태(대기·전송 중·완료·실패)를 한 행으로 보여 줄 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
221
223
  | [VirtualList](components/virtual-list.md) | 데이터 표시 | 행 높이가 **모두 같은** 긴 목록(수백~수천 행)을 정해진 높이 안에서 스크롤할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
222
224
  | [VisuallyHidden](components/visually-hidden.md) | 기반 기능 | Web에서 화면에는 보이지 않지만 스크린 리더는 읽어야 하는 **문맥 문구**를 덧붙일 때 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
@@ -324,7 +326,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
324
326
  | [신고와 차단](screens/flow-moderation.md) | 소통 | ModerationScreen을 사용해 신고와 차단 흐름을 구성한다. | 구성 API 지침 참조 | Web · Native | 구성 API별 | 배포 |
325
327
  | [알림함](screens/common-inbox.md) | 소통 | NotificationInboxScreen을 사용해 알림함 흐름을 구성한다. | 구성 API 지침 참조 | Web · Native | 구성 API별 | 배포 |
326
328
  | [채팅](screens/common-chat.md) | 소통 | ChatScreen을 사용해 채팅 흐름을 구성한다. | 구성 API 지침 참조 | Web · Native | 구성 API별 | 배포 |
327
- | [팔로워와 팔로잉](screens/common-follow-list.md) | 소통 | 두 관계 목록을 검색하고 프로필로 이동하며 팔로우한다. | 미게시(1.46.1 이후) | Web · Native | 구성 API별 | 실험 |
329
+ | [팔로워와 팔로잉](screens/common-follow-list.md) | 소통 | 두 관계 목록을 검색하고 프로필로 이동하며 팔로우한다. | 1.47.0 | Web · Native | 구성 API별 | 배포 |
328
330
  | [기록 표현 비교](screens/reference-comparison.md) | 화면 틀과 도구 | ScreenLayout을 사용해 같은 기록의 세 가지 구성 흐름을 구성한다. | 구성 API 지침 참조 | Web · Native | 구성 API별 | 배포 |
329
331
  | [목업 편집](screens/mockup-studio.md) | 화면 틀과 도구 | 제품 화면 캡처를 휴대폰·브라우저 프레임에 넣어 스토어·소개용 이미지(PNG)와 짧은 장면 영상을 만드는 Web 전용 작업 도구 화면이다. | 1.12.1 | Web | 구성 API별 | 배포 |
330
332
  | [화면 골격과 상태](screens/common-shell.md) | 화면 틀과 도구 | ScreenLayout을 사용해 화면 골격 흐름을 구성한다. | 구성 API 지침 참조 | Web · Native | 구성 API별 | 배포 |
@@ -70,7 +70,7 @@ const seriesColor = { light: "#216BA8", dark: "#216BA8" } as const; // 제품
70
70
  | --- | --- | --- | --- |
71
71
  | `tone` | `neutral` · `strong` · `brand` · `info` · `success` · `warning` · `attention` · `danger` | `neutral` | 단색 배경과 흰색 굵은 글자를 사용한다. `neutral`은 회색, `strong`은 더 진한 회색, 나머지는 각 의미의 solid fill이다. `outline`은 투명 배경과 의미색 글자다 |
72
72
  | `categoryColor` | `{ light: string; dark: string }`(6자리 hex) | — | `tone` 대신 쓰는 제품 분류 색. 글자색은 HJM이 고르고 AA를 검사한다(위 절). 개발 소스, 미게시 |
73
- | `variant` | `filled` · `outline` | `filled` | — |
73
+ | `variant` | `filled` · `outline` · `plain` | `filled` | — |
74
74
  | `size` | `small` · `medium` | `medium` | — |
75
75
  | `leading` · `trailing` | `ReactNode \| (context: BadgeAdornmentContext) => ReactNode` | — | 라벨 왼쪽·오른쪽 아이콘. 양쪽 동시 사용 가능하며 보조기기에서 숨겨진다 |
76
76
  | Web `children` · Native `label` | `ReactNode` · `string \| number`(필수) | — | 표시 라벨 |
@@ -108,3 +108,12 @@ Web·Native 모두 `radius.sm`(8)의 둥근 사각형을 사용한다. 2026-10-1
108
108
  | 배치 | `layoutStyle` | `layoutStyle`(`style`은 제거된 속성) |
109
109
  | 라벨 스타일 훅 | 없음 | `labelStyle` 제거된 속성 — tone/토큰 |
110
110
  | 기본 폭 | inline | `alignSelf: "flex-start"` |
111
+
112
+ ### 테두리 없는 상태 — 개발 소스, 미게시
113
+
114
+ 2026-10-26 오늘야구 라이브 표시 요청에 따라 `variant="plain"`을 추가했다.
115
+ outline과 같은 의미색 글자를 유지하고 배경·테두리를 없앤다. 점은 `leading`에 장식 노드로 전달한다.
116
+
117
+ ```tsx
118
+ <Badge label="진행 중" tone="danger" variant="plain" size="small" />
119
+ ```
@@ -82,6 +82,7 @@ Web·Native 모두 기존 `raised` Surface의 테두리·그림자를 재사용
82
82
  | prop | 값 | 기본값 | 설명 |
83
83
  | --- | --- | --- | --- |
84
84
  | `tone` | `default` · `raised` · `accent` · `sunken` · `subtle` · `selected`(Surface tone) | `raised` | 테두리와 공통 raised shadow로 입체감 표시. 평면 카드는 `tone="default"` 지정 |
85
+ | `badgesPlacement` | `body` · `corner` | `body` | 개발 소스·미게시. `corner`는 짧은 비상호작용 배지를 카드 우상단 8px에 12도 기울여 겹친다. 본문 높이를 늘리지 않는다. 다에리 NEW 배지 유무로 카드 높이가 달라진 문제를 해결하며 기존 `mediaBadges`의 배경 그림 전용 분류와 구분한다 |
85
86
  | `badgesAlign` | `start` · `center` | `start` | 미게시. 들판 대표 카드처럼 그림 아래의 배지 묶음을 가운데 배치할 때 `center` 사용. 줄바꿈된 행에도 적용 |
86
87
  | `bordered` | `boolean` | `true` | Surface 기본 `false`와 다르다 |
87
88
  | `padding` | spacing 이름 | `md`(16) | — |
@@ -212,3 +212,5 @@ ReactNode에서 자식 텍스트나 이미지 prop을 추측하지 않는다. We
212
212
  Native는 고정 열의 행 target 하나만 이름을 갖고, 고정 열이 없는 표도 같은 행 이름을 갖는다.
213
213
  `onRowPress`를 쓰는 분할 행의 스크롤 영역은 터치·포인터로 같은 행을 열지만 별도 접근성·키보드 대상은 아니다.
214
214
  개발 브라우저에서도 이 구분을 유지하며, 고정 행 이름에는 스크롤 열의 값까지 포함한다.
215
+
216
+ 2026-10-26 사용자 요청: LineScore 현재 이닝은 카테고리 선택과 같은 `selection.background`/`selection.content`를 쓴다. MatrixTable의 내 팀 강조는 기존 브랜드 역할을 유지한다. 개발 소스, 미게시.
@@ -118,7 +118,7 @@ await handle.closed; // portal 제거·초점 복구 뒤. 다음 오버레이는
118
118
  | `size` | `small` · `medium` · `large` | `medium` | Native recipe 최대 폭 320 · 420 · 640. Web 최대 폭 28rem · 36rem · 48rem(아래 배치 표) |
119
119
  | `dismissible` | `true` · `false` | `true` | `false`면 닫기 버튼과 바깥·Escape/back 닫기가 사라진다 |
120
120
  | `busy` | `true` · `false` | `false` | 비동기 작업 중에는 `open`을 유지하고 `busy`로 반복 행동과 닫기를 막는다 |
121
- | `motionOrigin` | `{ x, y, width, height }`(`TransitionRect`) | 없음 | 열기 직전 측정한 출발 영역. Web viewport/Native window의 물리 좌표를 쓴다. 1.14.0 양 renderer의 게시 타입에 포함. 선택적 실험 표현이며 기기·성능 검증 완료와 구분한다 |
121
+ | `motionOrigin` | `{ x, y, width, height }`(`TransitionRect`) | 없음 | 열기 직전 측정한 출발 영역. Web viewport/Native window의 물리 좌표를 쓴다. 1.14.0 양 renderer의 게시 타입에 포함. 선택적 stable 표현 계약이며 모든 기기·성능 검증 완료와 구분한다 |
122
122
  | `closeLabel` | 현지화 문자열 | — | 양쪽 필수. 닫기 버튼의 접근성 이름 |
123
123
  | `open`·`defaultOpen` | `boolean` | `false` | 제어형은 `open`+`onOpenChange`. Web 비제어형은 `trigger`가 필수다 |
124
124
  | `onOpenChange` | `(open: boolean, detail: { reason }) => void` | — | Web `reason`: `"trigger"`·`"close-action"`·`"escape"`·`"outside"`. Native: `"close-action"`·`"back"`·`"outside"`. `busy`이거나 `dismissible={false}`면 닫기 요청을 보내지 않는다 |
@@ -260,7 +260,7 @@ import { ImmersiveOverlay } from "@hjmds/react-native/overlays";
260
260
  - Native는 제어형과 비제어형을 렌더 중에 바꾸면 예외가 난다.
261
261
  - Native 기본 렌더러 예제는 실제 저장 서버가 없는 동기 완료 예시다. 제품에서는 저장 Promise를 반환하며, 예제의 영문 고정 문구는 제품 i18n 키로 치환한다. 닫기는 action이 요청하는 `close-action`이 맡는다.
262
262
 
263
- ### 선택적인 트리거 형태 전환 (게시 API·실험 표현)
263
+ ### 선택적인 트리거 형태 전환 (게시 API·stable 표현 계약)
264
264
 
265
265
  2026-10-07 [확장 카드·그리드 대조](../../../../../docs/plans/aceternity-interaction-adoption-2026-10-07.md) 중
266
266
  실제 npm 1.14.0 양 renderer tarball의 `dist/overlays.d.ts`에서 이 옵션을 확인했다. 기존 미게시 표기를
@@ -278,7 +278,7 @@ Web은 닫기 전환이 끝나야 portal을 제거하고 onDismissComplete와
278
278
  Native는 실제 Modal 콘텐츠를 측정하며 콜백이 오지 않으면 기존 enter 시간 내 일반 표시로
279
279
  복귀한다. 뒤늦은 측정으로 닫힌 세션을 되살리지 않는다. 측정이 없거나 모션 감소이면
280
280
  공간 전환 없이 기존 표현을 쓴다. Native에서 진입 후 크기가 달라지면 이전 측정으로
281
- 닫지 않고 일반 fade로 복귀한다. 다음 open에서 다시 측정한다. 이 옵션의 실험은 `버튼에서 이어지는 편집` 구성이다.
281
+ 닫지 않고 일반 fade로 복귀한다. 다음 open에서 다시 측정한다. 이 옵션의 사용 예는 `버튼에서 이어지는 편집` 구성이다.
282
282
 
283
283
  초안은 제품 상태에 두고 닫을 때 삭제하지 않는다. 내용·화면 회전·키보드로 목적지 크기가
284
284
  달라지는 흐름은 실제 기기 검증 후 채택한다. 원점이 다른 좌표계를 혼합하지 않는다.
@@ -0,0 +1,133 @@
1
+ # DrawingEditor
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 실험
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.48.0 이후)
7
+ - 검토일: 2026-10-27
8
+ - 근거: 다에리와 스핀트의 프로필 손그림 편집기가 복제돼 도구·이력·레이아웃이 갈라지는 것을 막으라는 사용자 요청
9
+ - 스토리북: `실험/컴포넌트/입력/손그림 편집기`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 펜·지우개로 그림을 그리고 이미지로 저장하는 전체 편집 화면에 쓴다. 프로필 사진뿐 아니라 사각 그림에도 사용할 수 있다.
14
+ 도구 배치·그리기 상태·실행 취소·다시 실행·전체 지우기·펜 굵기·최근 색·배경·저장 진행·실패·닫기 확인을 공통으로 제공한다.
15
+ 사진 선택·자르기에는 제품 이미지 피커를 사용한다. 문서 필기 인식이나 필압·벡터 파일 편집은 제공하지 않는다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 사진을 고르거나 촬영 | 제품 이미지 피커 |
22
+ | 색상 하나만 선택 | [ColorPicker](color-picker.md) |
23
+ | 일반 텍스트 편집 | [EditorScreen](editor-screen.md) |
24
+
25
+ ## 공개 이름과 import
26
+
27
+ | 이름 | 역할 | Web | Native |
28
+ | --- | --- | --- | --- |
29
+ | `DrawingEditor` | 전체 편집 화면 | `@hjmds/react/drawing-editor` | `@hjmds/react-native/drawing-editor` |
30
+ | `DrawingEditorProps`, `DrawingEditorLabels`, `DrawingImage` | 공통 타입 | 같은 경로 | 같은 경로 |
31
+ | `drawingEditorRecipe`, `drawingReducer`, `drawingInkPath` | 배치·이력·선 생성 계약 | `@hjmds/design-contracts/components/drawing-editor` | 같음 |
32
+
33
+ 루트 barrel로 내보내지 않는다. Web는 기존 optional `lucide-react`, Native는 `react-native-svg`와 `lucide-react-native` peer가 필요하다.
34
+ 새 라이브러리는 추가하지 않는다. 기존 ColorPicker와 EditorScreen을 합성하며 별도 색상 피커나 화면 셸을 만들지 않는다.
35
+
36
+ ## 최소 사용 예
37
+
38
+ ```tsx
39
+ // Native
40
+ import { DrawingEditor } from '@hjmds/react-native/drawing-editor';
41
+
42
+ <DrawingEditor
43
+ labels={localizedLabels}
44
+ cropPreview="circle"
45
+ safeAreaBottom={insets.bottom}
46
+ onCancel={close}
47
+ onSave={async image => {
48
+ // 제품이 계정 세대를 검증하고 서버가 받는 형식으로 변환한다.
49
+ await uploadProfileImage(image);
50
+ close();
51
+ }}
52
+ />
53
+ ```
54
+
55
+ ```tsx
56
+ // Web
57
+ import { DrawingEditor } from '@hjmds/react/drawing-editor';
58
+
59
+ <DrawingEditor labels={localizedLabels} cropPreview="circle"
60
+ onSave={uploadAndClose} onCancel={close} />
61
+ ```
62
+
63
+ ## 축과 기본값
64
+
65
+ | prop | 값 | 기본값 | 설명 |
66
+ | --- | --- | --- | --- |
67
+ | labels | `DrawingEditorLabels` | 필수 | 모든 도구·오류·닫기 확인 문구 |
68
+ | onSave | `(image: DrawingImage) => Promise<void>` | 필수 | 이미지 업로드 성공/실패를 제품이 확정 |
69
+ | onCancel | `() => void` | 필수 | 변경 취소 확인 후 나가기 |
70
+ | cropPreview | `circle`, `none` | `none` | 출력에 포함하지 않는 원형 잘림 안내 |
71
+ | safeAreaBottom | number | 0 | Native host의 아래 안전 영역 |
72
+ | testID | string | `drawing-editor` | 테스트 식별자와 캔버스 접두사 |
73
+
74
+ ## 배치
75
+
76
+ ```text
77
+ EditorScreen: 안전 영역 + 닫기/제목
78
+ 되돌리기·다시 실행 배경·전체 지우기
79
+ 정사각 캔버스(원형 잘림 안내 선택)
80
+ 펜 / 지우개
81
+ 굵기
82
+ 빠른 색상 스트립 · 더 많은 색
83
+ EditorScreen: 저장 footer + 아래 안전 영역
84
+ ```
85
+
86
+ | 항목 | 값 | 근거 |
87
+ | --- | --- | --- |
88
+ | 크기 | 캔버스 최대 400, Native 가용 영역 안 정사각 | `drawingEditorRecipe.maxCanvas` |
89
+ | 간격 | Native 도구 묶음 12, Web Stack md | 기존 양 표면의 편집기 배치 보존 |
90
+ | 순서·정렬 | 이력 → 캔버스 → 도구 → 저장 | 원형 예고는 캔버스 위 오버레이 |
91
+ | 고정·스크롤 | Native 본문 비스크롤, 저장 footer | 세로 획을 부모 스크롤에서 보호 |
92
+ | 좁은 폭·큰 글자 | 가용 폭 안에서 캔버스 축소, 색상은 가로 스크롤 | 최대 글자 조건은 전사 지침대로 제외 |
93
+
94
+ ## 꼭 지킬 것
95
+
96
+ - 그림 도구·이력을 제품에서 다시 조립하지 않는다.
97
+ - 저장 실패는 reject하고 성공 후 화면 이동은 제품이 수행한다.
98
+ - 업로드 전 계정 세대와 이미지 입력 한도를 검사한다.
99
+
100
+ ## 상태와 제품 경계
101
+
102
+ `labels`는 모든 문구를 요구한다. 제품은 번역·계정·업로드·형식 변환·성공 후 탐색만 연결한다.
103
+ `onSave`가 reject되면 오류를 보이고 그림과 이력을 보존한다. 성공을 HJM이 추정해 화면을 닫지 않는다.
104
+ 저장 중 반복 저장과 편집을 막으며 비어 있는 기본 배경은 저장할 수 없다. 배경만 바꾼 그림은 저장 가능하다.
105
+ 이미지는 Native SVG의 PNG 또는 Web Canvas의 JPEG base64와 실제 `mimeType`으로 전달된다. 크기·압축·업로드 한도는 제품 계약에 맞춘다.
106
+ 계정이 바뀌는 동안 이미지 추출이 끝날 수 있으므로 소비자는 렌더 시점의 계정 세대를 캡처해 업로드 전에 다시 검증한다.
107
+
108
+ ## 레이아웃과 상호작용
109
+
110
+ 상단 이력·배경·지우기 → 정사각 캔버스 → 펜/지우개 → 굵기 → 색상 스트립 순서다. 저장과 닫기는 EditorScreen이 소유한다.
111
+ Native는 부모 스크롤이 세로 획을 가져가지 않도록 `scroll="content"`를 사용하고 실제 가용 영역 안에서 캔버스 크기를 정한다.
112
+ Web는 pointer capture와 `touch-action: none`을 사용한다. 첫 터치/포인터만 획을 소유한다.
113
+ `cropPreview="circle"`은 잘릴 모서리를 표시하지만 출력 이미지에는 포함하지 않는다. 기본값 `none`은 사각 그림이다.
114
+ 펜과 지우개의 실제 굵기는 슬라이더 값과 같다. 배경 변경 후 지우개 자국도 새 배경을 따른다.
115
+
116
+ ## 확인 범위와 소비 이관
117
+
118
+ 다에리 Native·Web, 스핀트 Native가 이 컴포넌트를 소비한다. 제품의 중복 캔버스·도구·이력 구현과 그 전용 계약 테스트는 제거하고
119
+ 공통 계약/renderer 테스트로 옮긴다. 제품 테스트는 계정·업로드 실패·탐색 연결을 계속 확인한다.
120
+ 소스 연결 개발에서만 제공되는 후보이며 npm 게시·스토어 반영·실기기 검증과 구분한다.
121
+
122
+ ## 플랫폼 차이
123
+
124
+ | 항목 | Web | Native |
125
+ | --- | --- | --- |
126
+ | 획 입력 | pointer capture | 단일 responder 소유 |
127
+ | 이미지 | Canvas JPEG | SVG PNG |
128
+ | 주변 스크롤 | touch-action none | 비스크롤 본문 |
129
+
130
+ ## 근거
131
+
132
+ - 공통 소유권: 2026-10-27 사용자 지시로 다에리·스핀트의 복제 편집기를 HJM으로 옮겼다.
133
+ - 획 소유권과 스크롤: 기존 다에리의 두 번째 손가락·세로 획 결함 대응을 보존한다.
@@ -1,12 +1,12 @@
1
1
  # FollowListScreen
2
2
 
3
3
  - 단계: 컴포넌트
4
- - 상태: 실험
4
+ - 상태: 배포
5
5
  - 지원: Web · Native
6
- - 적용: 미게시(1.46.1 이후)
6
+ - 적용: 1.47.0
7
7
  - 검토일: 2026-10-25
8
8
  - 근거: 사용자 Instagram 팔로워 화면 요청, 양 renderer src/follow-list.tsx
9
- - 스토리북: `실험/화면/소통/팔로워와 팔로잉`
9
+ - 스토리북: `배포/화면/소통/팔로워와 팔로잉`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
@@ -69,7 +69,7 @@ import { ImageChoiceGroup } from "@hjmds/react-native/image-choice";
69
69
  | 크기 | 그림 72 정사각, 바깥 고리 2 + 간격 2(터치 영역 80), radius `radius.md` 12, 체크 원 20 | `imageChoiceRecipe` |
70
70
  | 간격 | 타일 사이 `spacing.xs` 8(가로·세로), 라벨–타일 8, 그림–항목 라벨 `spacing.xxs` 4. 이름과 설명 사이 0 | `imageChoiceRecipe.gap`, `itemLabelGap` |
71
71
  | 글자 칸 | 이름 줄(`itemLabelLines`) + 설명 줄(묶음에 설명이 있으면 1)을 caption 행간(16 × 글자 배율)으로 모든 타일이 같은 높이로 비워 둔다. 타일 높이 = 80 + 4 + 줄 수 × 행간(글자 칸이 없으면 80) | `resolveImageChoiceCaptionLines`, `resolveImageChoiceTileHeight` |
72
- | 순서·정렬 | `items` 순서대로 시작 쪽부터 줄바꿈 배치, 체크는 타일 끝 쪽 위 모서리 | 두 렌더러 |
72
+ | 순서·정렬 | `items` 순서대로 시작 쪽부터 줄바꿈 배치. 항목이 적어도 화면 폭에 맞는 빈 칸을 유지해 다음 항목이 중간 칸부터 채워진다(2026-10-27 Diairy 양끝 벌어짐 수정). 체크는 타일 끝 쪽 위 모서리 | 두 렌더러 |
73
73
  | 고정·스크롤 | 기본은 자체 스크롤 없음(바깥 목록·화면이 스크롤). `virtualized`면 묶음이 `height` 높이로 스스로 스크롤한다. Web은 포커스 테두리가 잘리지 않게 안쪽 여백 4(`virtualViewportInset`), 열 수 = ⌊(폭 + 8) / 88⌋(최소 1) | 두 렌더러, `resolveImageChoiceColumns` |
74
74
  | 좁은 폭·큰 글자 | 남은 폭에서 줄바꿈, 타일 크기는 유지 | flex wrap |
75
75
 
@@ -0,0 +1,139 @@
1
+ # ShareSheet
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 실험
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.47.0 이후)
7
+ - 검토일: 2026-10-26
8
+ - 근거: `src/share-sheet.ts`의 `shareSheetRecipe`·`resolveShareSheetSendOutcome`·`filterShareSheetRecipients`; Web `src/share-sheet.tsx`, Native `src/share-sheet.tsx`
9
+ - 스토리북: `실험/컴포넌트/오버레이/공유 시트`
10
+
11
+ ## 언제 쓰나
12
+
13
+ 링크 하나를 **앱 안의 받는 사람에게 보내거나(선택), 링크를 복사하거나, 기기 공유 시트로 내보내는** 공유 시트에 쓴다. 단톡방에 보내기·팔로잉에게 DM 보내기처럼 앱 안 대상이 있으면 `recipients`를 주고, 외부 공유만 있으면 생략한다. Sheet·ImageChoiceGroup·버튼을 제품에서 다시 조립하지 않는다.
14
+
15
+ ## 쓰지 않을 때
16
+
17
+ | 상황 | 대신 쓸 것 |
18
+ | --- | --- |
19
+ | 링크 없이 값 하나만 복사 | `ClipboardButton`·`ClipboardIconButton`(Web) |
20
+ | 받는 사람을 고른 뒤 대화방을 새로 만들거나 초대하는 흐름 | `ChatRoomCreate`·`ChatRoomInvite` |
21
+ | 사진·파일 원본을 보내기 | 제품 업로드 흐름 + 기기 공유 |
22
+ | 공유 외 여러 행동이 섞인 메뉴 | `Menu` |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `ShareSheet` | 기본. 공유 시트 | `@hjmds/react/share-sheet` | `@hjmds/react-native/share-sheet` |
29
+ | `ShareSheetProps`, `ShareSheetRecipients`, `ShareSheetRecipient`, `ShareSheetRecipientSearch` | 보조. 타입 | `@hjmds/react/share-sheet` | `@hjmds/react-native/share-sheet` |
30
+ | `shareSheetRecipe`, `resolveShareSheetSendOutcome`, `filterShareSheetRecipients`, `resolveShareSheetPickerValue`, `validateShareSheet` | 보조. 배치 값·보내기 결과 규칙 | `@hjmds/design-contracts/share-sheet` | 같음 |
31
+
32
+ ## 최소 사용 예
33
+
34
+ ```tsx
35
+ // Web
36
+ import { ShareSheet } from "@hjmds/react/share-sheet";
37
+
38
+ <ShareSheet open={open} onOpenChange={setOpen} title={t("share.title")} link={url} shareTitle={room.name}
39
+ labels={{ close: t("common.close"), copyLink: t("share.copyLink"), systemShare: t("share.shareTo"), linkCopied: t("share.linkCopied"), actionFailed: t("share.failed") }}
40
+ onLinkCopied={() => track("share", { method: "copy_link" })}
41
+ recipients={{
42
+ items: chats.map(chat => ({ id: chat.id, label: chat.name, image: <Avatar name={chat.name} decorative /> })),
43
+ labels: recipientLabels, status: chatsQuery.isError ? "error" : chatsQuery.isLoading ? "loading" : "ready", onRetry: () => void chatsQuery.refetch(),
44
+ onSend: async ids => sendToChats(ids, url), // { sentIds, error? }
45
+ }} />
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import * as Clipboard from "expo-clipboard";
51
+ import { ShareSheet } from "@hjmds/react-native/share-sheet";
52
+
53
+ <ShareSheet open={open} onOpenChange={setOpen} title={t("share.title")} link={url} shareTitle={drop.title} labels={labels}
54
+ copyLink={async link => { if (!(await Clipboard.setStringAsync(link))) throw new Error("clipboard"); }}
55
+ icons={{ copyLink: <Link size={24} color={colors.text} />, systemShare: <Share size={24} color={colors.text} /> }}
56
+ recipients={{ items: following.map(user => ({ id: user.id, label: user.nickname, image: <UserAvatar user={user} /> })), labels: recipientLabels,
57
+ onSend: async ids => sendDirectMessages(ids) }} />
58
+ ```
59
+
60
+ ## 축과 기본값
61
+
62
+ | prop | 값 | 기본값 | 설명 |
63
+ | --- | --- | --- | --- |
64
+ | `open`·`onOpenChange` | `boolean` · `(open: boolean, detail: { reason }) => void` | 필수 | 제어형. 닫히면 선택·보낸 표시·검색어·안내가 처음으로 돌아간다 |
65
+ | `title` | `string` | 필수 | 시트 제목. Native Android 기기 공유 선택창 제목으로도 쓴다 |
66
+ | `labels` | `{ close, copyLink, systemShare, linkCopied, actionFailed }` | 필수 | 지역화 문구. 빈 문자열이면 `TypeError` |
67
+ | `link` | `string` | 필수 | 복사·공유할 URL. 바뀌면 이전 "복사함" 안내를 지운다 |
68
+ | `shareTitle`·`shareText` | `string` | — | 기기 공유 시트에 링크와 함께 넘기는 제목·문구 |
69
+ | `copyLink` | Web `((link: string) => Promise<void>) \| false` · Native `((link: string) => Promise<void>) \| null` | Web `navigator.clipboard.writeText` · Native 필수 | 링크 복사. Web `false`·Native `null`이면 복사 행동을 숨긴다. 거부되면 실패 안내를 보인다 |
70
+ | `systemShare` | Web `((content) => Promise<void>) \| false` · Native `((content) => Promise<"completed" \| "dismissed">) \| false` | Web `navigator.share`(없는 브라우저는 숨김) · Native `Share.share` | 기기 공유. `false`면 숨긴다. iOS 미리보기용 제품 모듈 등으로 바꿀 수 있다. `content`는 `{ url, title?, text? }` |
71
+ | `onLinkCopied` | `() => void` | — | 복사가 끝난 뒤. 분석 기록용 |
72
+ | `onSystemShare` | `(outcome: "completed" \| "dismissed") => void` | — | 기기 공유 시트가 닫힌 뒤. `completed`도 전달 확인이 아니다 |
73
+ | `icons` | Web `{ copyLink?, systemShare? }` · Native `{ copyLink, systemShare }` | Web HJM `copy`·`share` 아이콘 · Native 필수 | 두 원형 행동의 그림 |
74
+ | `recipients` | 아래 표 | — | 앱 안 받는 사람. 없으면 외부 공유만 |
75
+ | `notice` | `ReactNode` | — | 본문 맨 위 제품 안내(보내기 보상 등) |
76
+
77
+ | `recipients` 필드 | 값 | 기본값 | 설명 |
78
+ | --- | --- | --- | --- |
79
+ | `items` | `{ id, label, image, description?, disabled? }[]` | 필수 | 받는 사람. id 중복·빈 이름이면 `TypeError` |
80
+ | `labels` | `{ group, search, clearSearch, loading, loadFailed, retry, empty, noResults, sent, sendFailed, send(count) }` | 필수 | 지역화 문구. `send`는 개수를 받아 버튼 문구를 만든다 |
81
+ | `status` | `"ready"` · `"loading"` · `"error"` | `"ready"` | 목록 상태. `error`면 `onRetry`가 있을 때 다시 시도 버튼을 붙인다 |
82
+ | `onRetry`·`retrying` | `() => void` · `boolean` | — | 불러오기 다시 시도와 그 진행 표시 |
83
+ | `search` | `{ query?, onQueryChange?, searching? }` | — | 생략하면 이름으로 이 안에서 거른다. `onQueryChange`를 주면 제품이 검색어와 결과 `items`를 맡는다(서버 검색) |
84
+ | `sentIds` | `readonly string[]` | `[]` | 시트를 열기 전에 이미 보낸 받는 사람. 체크된 채 잠긴다 |
85
+ | `onSend` | `(ids: readonly string[]) => Promise<{ sentIds; error? }>` | 필수 | 고른 사람에게 보낸다. `sentIds`는 잠기고 나머지는 선택된 채 남는다. `error`가 없으면 `labels.sendFailed`를 보인다. 요청하지 않은 id를 돌려주면 `TypeError` |
86
+
87
+ ## 배치
88
+
89
+ | 항목 | 값 | 근거 |
90
+ | --- | --- | --- |
91
+ | 크기 | 받는 사람이 있으면 Sheet `size="large"`(Native는 본문 스크롤), 없으면 `auto`. 원형 행동 IconButton `large`(지름 52) `secondary`, 보내기 Button `large` `primary` 가득 폭 | `shareSheetRecipe.size`·`actionButton`·`sendButton` |
92
+ | 간격 | 본문 영역 사이 `spacing.md` 16, footer의 보내기–행동 줄 `spacing.xs` 8, 두 원형 행동 사이 `spacing.xxxl` 40, 버튼–글자 `spacing.xs` 8 | `shareSheetRecipe.bodyGap`·`footerGap`·`actionGap`·`actionCaptionGap` |
93
+ | 순서·정렬 | 본문: 제품 `notice` → 보내기 오류 → 복사·공유 안내 → 검색 → 목록(또는 불러오는 중·오류·빈 상태). footer: 보내기 버튼(고른 사람이 있을 때만) → 가운데 정렬 [링크 복사][공유하기], 각 버튼 아래 caption 글자 | 두 렌더러 |
94
+ | 고정·스크롤 | footer는 Sheet가 고정하고 본문만 스크롤한다. 받는 사람은 ImageChoiceGroup 타일(이름 2줄) | Sheet footer, `shareSheetRecipe.picker` |
95
+ | 좁은 폭·큰 글자 | 타일은 폭에 맞춰 줄바꿈하고 크기는 유지한다. 두 원형 행동은 한 줄 가운데에 둔다 | ImageChoiceGroup |
96
+
97
+ ```text
98
+ ┌──────────────────────────────┐
99
+ │ ─── 공유 │ ← Sheet 머리(끌어서 닫기)
100
+ ├──────────────────────────────┤ ← 본문(스크롤)
101
+ │ [제품 안내] │
102
+ │ [보내기 오류 / 링크 복사 안내] │
103
+ │ [🔍 이름 검색 ×] │
104
+ │ [얼굴][얼굴][얼굴][얼굴] │
105
+ │ 서연 민준 지우 하린 │
106
+ │ 보냄 │ ← 보낸 사람: 체크·잠금·설명 줄
107
+ ├──────────────────────────────┤ ← footer(고정)
108
+ │ [ 2명에게 보내기 ] │ ← 고른 사람이 있을 때만
109
+ │ (🔗) (↗) │
110
+ │ 링크 복사 공유하기 │
111
+ └──────────────────────────────┘ ← 안전 영역
112
+ ```
113
+
114
+ ## 꼭 지킬 것
115
+
116
+ - 보이는 문구·접근성 이름은 모두 `labels`로 준다. 컴포넌트가 만드는 문구는 없다.
117
+ - 기기 공유 시트는 열렸다 닫힌 것만 안다. 전달됐다고 안내하거나 "보냄"으로 표시하지 않는다. 앱 안 보내기만 `onSend` 결과로 "보냄"을 표시한다.
118
+ - `onSend`는 실제로 전달한 id만 `sentIds`로 돌려준다. 실패한 사람은 선택된 채 남아 다시 누르면 그 사람만 보낸다(이미 보낸 사람에게 두 번 보내지 않는다).
119
+ - 보내는 동안 시트는 사용자가 닫을 수 없고(`busy`) 목록은 잠긴다. 연속 탭은 요청 하나로 묶인다. 로그인 만료처럼 시트를 닫아야 하는 실패는 제품이 `onOpenChange`의 제어 상태를 `false`로 바꾼다.
120
+ - 검색칸에 자동 포커스를 주지 않는다. 하단 시트는 열릴 때 입력에 포커스하지 않으므로 키보드가 목록과 공유 버튼을 가리지 않는다.
121
+ - Native 복사는 제품이 `copyLink`로 준다(`expo-clipboard` 등). HJM Native 핵심은 클립보드 모듈을 설치하지 않는다.
122
+
123
+ ## 플랫폼 차이
124
+
125
+ | 항목 | Web | Native |
126
+ | --- | --- | --- |
127
+ | 복사 기본값 | `navigator.clipboard.writeText` | 없음. `copyLink` 필수(`null`이면 숨김) |
128
+ | 기기 공유 기본값 | `navigator.share({ url, title?, text? })`. 없는 브라우저와 서버 렌더에서는 숨김 | `Share.share`. iOS는 링크를 `url`로, Android는 `url`을 버리므로 제목·문구 다음 줄에 링크를 붙인 `message`로 보낸다 |
129
+ | 공유 취소 | `AbortError` 거부 → `onSystemShare("dismissed")`, 실패 안내 없음 | iOS `dismissedAction` → `dismissed`. Android는 항상 `completed` |
130
+ | 교체 함수의 결과 | `Promise<void>`, 취소는 `AbortError`로 거부 | `Promise<"completed" \| "dismissed">` |
131
+ | 아이콘 | 생략하면 HJM `copy`·`share` | `icons` 필수(핵심이 그림을 그리지 않는다) |
132
+ | 안내 알림 | 복사 성공 `status`, 실패 `alert` | 복사 성공 polite, 실패 assertive 알림 |
133
+
134
+ ## 근거
135
+
136
+ - 도입: 2026-10-26 사용자 지시 "공유에 대해서 디자인 시스템에 넣고 꺼내서 쓰자 번뚝 공유 그대로 공통화 해". 번뚝 Web·Native 공유 시트(받는 사람 검색·다중 선택·보낸 사람 잠금·부분 실패 재시도·두 원형 외부 행동)를 제품 중립 API로 옮겼다. 오늘갈게 단톡방 보내기처럼 다른 제품도 같은 시트를 쓴다.
137
+ - 기기 공유는 전달을 주장하지 않음: 번뚝 공유가 공유 시트를 연 것만 관측할 수 있어 시도만 기록했다. `onSystemShare`의 `completed`도 같은 뜻이다.
138
+ - 실패한 사람만 선택 유지: 번뚝 부분 실패 재시도에서 이미 보낸 DM이 중복되지 않게 했다. 요청하지 않은 id 보고는 보내지 않은 사람을 "보냄"으로 표시하므로 오류로 둔다.
139
+ - 별도 보조 기능(supplemental): Sheet에 새 상태 축을 더하지 않고 Sheet·ImageChoiceGroup·IconButton·SearchField를 조합한 공유 전용 구성이라 catalog canonical 행이나 Sheet companion으로 두지 않고 자기 지침과 스토리를 가진다(`ChoiceRow`와 같은 방식, `scripts/sync-public-component-map.mjs`).
@@ -5,7 +5,7 @@
5
5
  - 지원: Native
6
6
  - 적용: 1.12.1
7
7
  - 검토일: 2026-10-06
8
- - 근거: [Optional interaction adapters · Shared screen transition](../../../../../docs/interaction-adapters.md#shared-screen-transition)(Native 전용, experimental, supplemental, 카탈로그 계약 없음), `packages/react-native/src/screen-transition.tsx`, 시연은 구성 스토리 `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환` › 카드 확대와 화면 전환. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
8
+ - 근거: [Optional interaction adapters · Shared screen transition](../../../../../docs/interaction-adapters.md#shared-screen-transition)(Native 전용, 문서화한 host에서 stable, supplemental, 카탈로그 계약 없음), `packages/react-native/src/screen-transition.tsx`, 시연은 구성 스토리 `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환` › 카드 확대와 화면 전환. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
9
  - 스토리북: `배포/구성/직접 조작과 모션/끌기·밀기·화면 전환`
10
10
 
11
11
  ## 언제 쓰나
@@ -112,6 +112,18 @@ import { Sheet } from "@hjmds/react-native/overlays";
112
112
 
113
113
  개발 소스·미게시: `size="small"`은 가용 높이의 40%로, `medium`(60%) 높이의 2/3이다. 짧은 안내에 내용 높이만 쓰면 지나치게 작고 medium은 빈 공간이 큰 경우 선택한다. Web·Native 공통 recipe가 크기를 소유한다.
114
114
 
115
+ ### 크기 고르기 (2026-10-27)
116
+
117
+ 2026-10-27 포트폴리오 전 앱 하단 시트 조사에서 고정 크기 16곳 중 6곳의 높이가 내용보다 과했다.
118
+ 연도 선택기 하나뿐인 필터가 85%로 열렸고, 문단 하나짜리 안내가 60%였다. 사유 6개짜리 신고·몇 줄짜리 알림 설정·몇 개 안 되는 기록 목록도 85%로 열렸다.
119
+ 그래서 크기는 아래 순서로 고른다.
120
+
121
+ - **기본은 `auto`다.** 내용이 정해진 보조 작업은 내용 높이로 연다. 신고, 행 몇 개짜리 설정, 확인, 조건 몇 개짜리 필터, 길이가 바뀌는 선택 목록이 여기에 든다. 길어지면 90%에서 멈추고 본문이 스크롤한다.
122
+ - **고정 크기(`medium`·`large`·`full`)는 본문이 남은 높이를 채워야 할 때만 쓴다.** 자기 스크롤과 상태 영역을 가진 검색·목록 화면(SearchScreen 등), 그리기처럼 화면을 쓰는 작업이 여기에 든다. 고르는 조건에 따라 구획이 나타나고 사라져 편집 중 높이가 흔들리는 폼도 고정 크기를 쓴다.
123
+ - **짧은 안내 문단에는 `medium` 대신 `small`(또는 `auto`)을 쓴다.**
124
+ - **화면 틀을 시트에 넣고 고정 크기로 채우지 않는다.** 시트 안 신고는 `ModerationScreen` 대신 [ModerationForm](moderation-screen.md)을 쓰고 `auto`로 연다. 화면 틀은 높이를 채워야 해서 시트를 large로 키우게 된다.
125
+ - **제품의 공통 시트 wrapper는 모든 사용처에 같은 고정 크기를 강제하지 않는다.** 기본은 `auto`로 두고, 위 고정 크기 조건에 맞는 사용처만 크기를 넘긴다.
126
+
115
127
  Native 스크롤 본문은 viewport 높이만큼 content를 채운다(미게시). 고정 크기 시트의 빈/오류/로딩 상태는 `CompositionLayout layoutStyle={{ flex: 1 }}`로 연결해 손잡이·머리·footer를 제외한 본문 중앙에 둔다. 목록이 길면 같은 본문에서 스크롤한다.
116
128
 
117
129
  선택형 Native `GestureSheetInput`의 실제 keyboard-tracking host에도 제품 `tokens.fontFamily.ui`가 이어진다. optional library·폰트 설치와 실제 기기 검증은 제품이 맡는다.
@@ -122,7 +134,7 @@ Native 스크롤 본문은 viewport 높이만큼 content를 채운다(미게시)
122
134
  | --- | --- | --- |
123
135
  | 크기 | 하단 시트 높이: `auto` 내용 높이(최대 화면 높이 × `maxHeightRatio` 0.9), `medium` 60%, `large` 85%, `full` 위 safe area 안 전체 높이(0.9 상한 없음). Web 하단 시트 최대 폭 640. 위 두 모서리만 `radius.xl` 24. 옆 시트(`start`·`end`): Web 폭 `min(28rem, 100%)`·높이 화면 − 32, Native 폭 88%(최대 420)·화면 높이 | `sheetRecipe.sizes`·`content`·`web`, `.hjm-sheet`, Native `Sheet` |
124
136
  | 간격 | 두 플랫폼 같은 값: 좌우 `content.paddingHorizontal`(`spacing.lg` 20), 위·아래 `content.paddingTop/Bottom`(`spacing.sm` 12, 하단 시트는 아래 safe area를 더함), 머리·본문·footer 사이 `body.gap`(`spacing.md` 16), footer 위 `footer.paddingTop`(`spacing.sm` 12), 제목–닫기 `header.gap`(`spacing.sm` 12), footer 버튼 사이 `footer.gap`(`spacing.xs` 8) | `.hjm-sheet__*`, `sheetRecipe.content`·`body`·`footer` |
125
- | 순서·정렬 | 손잡이(하단) → 머리(설명 + 닫기, 둘 다 없으면 그리지 않음) → 본문 → `footer`. 보이는 제목 줄은 없다(미게시). 머리 최소 높이 44(`control.minTouchTarget`), ×는 설명이 없어도 끝 쪽 같은 자리다. 기본 하단 시트는 머리가 없어 본문이 손잡이 영역 바로 아래 `body.gap` 16에서 시작한다. Web footer는 오른쪽 정렬 가로 줄 [보조][주]. Native footer는 세로 열이라 꽉 찬 폭 버튼을 주 행동 먼저 둔다. 하단 시트는 항상 위 가운데 36×4 손잡이(터치 최소 폭·높이44)를 둔다. `detents`가 있으면 높이 전환 버튼, 없고 ×가 없으면 `closeLabel` 이름의 닫기 버튼이다. `showClose: true`로 ×를 표시하면 손잡이는 장식으로 남겨 같은 닫기 버튼이 중복되지 않는다(미게시). 손잡이·머리 영역을 아래로 끌어 `sheetDragRecipe.dismissDistanceRatio`(시트 높이의 25%) 또는 `dismissVelocity` 0.5px/ms를 넘기면 `swipe`로 닫고, 아니면 제자리로 돌아간다(미게시, Web·Native 동일) | `sheetRecipe.header`·`handle`, `.hjm-sheet__footer`, `.hjm-sheet__handle` |
137
+ | 순서·정렬 | 손잡이(하단) → 머리(설명 + 닫기, 둘 다 없으면 그리지 않음) → 본문 → `footer`. 보이는 제목 줄은 없다(미게시). 머리 최소 높이 44(`control.minTouchTarget`), ×는 설명이 없어도 끝 쪽 같은 자리다. 기본 하단 시트는 머리가 없어 본문이 손잡이 영역 바로 아래 `body.gap` 16에서 시작한다. Web footer는 오른쪽 정렬 가로 줄 [보조][주]. Native footer는 세로 열이라 꽉 찬 폭 버튼을 주 행동 먼저 둔다. 하단 시트는 항상 위 가운데 36×4 손잡이(터치 최소 폭·높이44)를 둔다. `detents`가 있으면 높이 전환 버튼, 없고 ×가 없으면 `closeLabel` 이름의 닫기 버튼이다. `showClose: true`로 ×를 표시하면 손잡이는 장식으로 남겨 같은 닫기 버튼이 중복되지 않는다(미게시). 손잡이·머리 영역을 아래로 끌어 `sheetDragRecipe.dismissDistanceRatio`(시트 높이의 25%) 또는 `dismissVelocity` 0.5px/ms를 넘기면 `swipe`로 닫고, 아니면 제자리로 돌아간다(미게시, Web·Native 동일). 본문도 같은 끌기로 닫는다(2026-10-27 사용자 결정, 미게시). 스크롤 본문은 이미 맨 위일 때 시작한 아래 끌기만 시트가 가져가고, 스크롤된 상태에서는 같은 동작이 먼저 내용을 위로 되돌린다. iOS는 UIScrollView가 JS 응답자 아래서도 스스로 끌기를 가져가므로 본문 맨 위의 당김(overscroll)으로 시트를 움직이고 UIKit 탄성 거리로 환산한 같은 임계값을 쓴다. Web 본문 끌기는 터치만 받고 마우스는 글자 선택을 위해 손잡이를 쓴다. 입력칸에서 시작한 끌기와 가로 끌기는 본문 안의 행동으로 남긴다 | `sheetRecipe.header`·`handle`, `.hjm-sheet__footer`, `.hjm-sheet__handle` |
126
138
  | 고정·스크롤 | 본문만 스크롤, 머리·`footer` 고정(Native는 `scrollable`일 때 ScrollView). 스크롤 영역은 시트 좌우 여백(`sheetRecipe.content.paddingHorizontal` 20) 끝까지 넓고 여백은 스크롤 내용 안쪽에 둔다(Web `.hjm-sheet__body` padding, Native ScrollView `marginHorizontal: -20` + 내용 `paddingHorizontal: 20`). 그래서 본문 ListRow의 8 표면이 잘리지 않고 내용 위치는 그대로다. 고정 높이(`medium`·`large`·`full`, 옆 시트)에서는 본문이 머리·footer를 뺀 남은 높이를 차지하고 `footer`는 시트 아래에 붙는다. 그래서 본문의 `flex: 1` 자식(SearchScreen, 목록 화면)이 그 높이를 채운다(Web `.hjm-sheet__body` `flex: 1 1 auto`, Native 본문 `flexGrow: 1`·`minHeight: 0`, 1.13.1 변경). `auto`는 내용 높이 그대로다. 저장·확인은 `footer`에. 아래 안전 영역은 시트가 자기 여백에 더한다(`spacing.sm` 12 + 아래 inset, Web은 `env(safe-area-inset-bottom)`). 위 inset은 시트가 올라갈 높이를 줄인다. 제품이 inset을 다시 더하지 않는다 | `.hjm-sheet__body`, `sheetRecipe.safeArea`, Native `Sheet` |
127
139
  | 좁은 폭·큰 글자 | 폭 < 640이면 하단 시트가 화면 폭을 채운다. Web footer는 줄을 바꾼다. 큰 글자로 내용이 길어지면 `auto`는 최대 90%에서 멈추고 본문이 스크롤된다(`full`은 위 safe area까지) | `.hjm-sheet[data-placement="bottom"]`, `maxHeightRatio` |
128
140