@hjmds/design-contracts 2.0.0 → 2.2.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 (40) hide show
  1. package/dist/catalog.d.ts +27 -1
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/catalog.js +1 -1
  4. package/dist/catalog.js.map +1 -1
  5. package/dist/component-recipes.d.ts +12 -0
  6. package/dist/component-recipes.d.ts.map +1 -1
  7. package/dist/component-recipes.js +2 -0
  8. package/dist/component-recipes.js.map +1 -1
  9. package/dist/image.d.ts +36 -0
  10. package/dist/image.d.ts.map +1 -1
  11. package/dist/image.js +44 -0
  12. package/dist/image.js.map +1 -1
  13. package/dist/layout.d.ts +10 -0
  14. package/dist/layout.d.ts.map +1 -1
  15. package/dist/layout.js +11 -1
  16. package/dist/layout.js.map +1 -1
  17. package/dist/tag.d.ts +20 -7
  18. package/dist/tag.d.ts.map +1 -1
  19. package/dist/tag.js +20 -9
  20. package/dist/tag.js.map +1 -1
  21. package/dist/version.d.ts +1 -1
  22. package/dist/version.js +1 -1
  23. package/dist/version.js.map +1 -1
  24. package/docs/generated/component-maturity.md +1 -1
  25. package/docs/generated/renderer-evidence.json +3 -3
  26. package/docs/generated/renderer-evidence.md +1 -1
  27. package/docs/generated/showcase-manifest.json +3 -2
  28. package/docs/layout.md +3 -1
  29. package/docs/tag.md +14 -5
  30. package/docs/usage/components/avatar.md +9 -0
  31. package/docs/usage/components/chart.md +1 -0
  32. package/docs/usage/components/comment-thread-row.md +4 -1
  33. package/docs/usage/components/image.md +7 -0
  34. package/docs/usage/components/layout.md +4 -3
  35. package/docs/usage/components/profile-summary.md +25 -0
  36. package/docs/usage/components/radio-group.md +10 -0
  37. package/docs/usage/components/sheet.md +1 -1
  38. package/docs/usage/components/sidebar.md +2 -0
  39. package/docs/usage/components/tag.md +8 -5
  40. package/package.json +1 -1
@@ -180,6 +180,10 @@ Avatar를 쓰지 못한 이유(이름 필수, Web 이미지 호스트 없음, 4
180
180
  일반 목록은 `ListRow.leadingAction`, 카드 작성자는 `Card.leadingAction`, 대화 목록은 `ConversationRow.avatarAction` + `renderBody`, 참여자는 `ChatRoomMembers.memberProfile`이다.
181
181
  같은 사진에 `Avatar.action`과 상위 `avatarAction`을 함께 넣지 않는다. 본문 링크/버튼과 독립 프로필 버튼은 형제다.
182
182
 
183
+ 사진 옆에 따로 보이는 회원 닉네임도 같은 프로필로 이동한다(2026-10-29 사용자 지시: Spint 댓글에서 닉네임을 눌러도 반응이 없었다).
184
+ 댓글 `CommentThreadRow`는 `avatarAction`이 있으면 작성자 이름도 같은 행동·접근성 이름의 글자 버튼으로 그린다(Web `hjm-identity-name-action`,
185
+ Native 중첩 `Text` press). 제품은 추가 prop 없이 `avatarAction`만 공급한다. 행 전체가 대화방·알림 목적지인 글자 안의 이름은 버튼을 중첩하지 않는다.
186
+
183
187
  회원 식별자가 없는 장식·방 사진을 회원 프로필로 연결하지 않는다. 명시적인 사진 편집·공유 대상 선택은 원래 작업을 유지한다.
184
188
 
185
189
  내 프로필 화면의 내 사진은 예외로 그 자체가 사진 변경 버튼이다(2026-10-25 사용자 지시). `action={{label: t("photo.change"), onPress: openPhotoPicker}}`를 연결하고 사진 고르기·자르기·업로드는 제품이 소유한다. 같은 화면에 중복 "사진 바꾸기" 버튼을 두지 않는다.
@@ -196,3 +200,8 @@ Avatar를 쓰지 못한 이유(이름 필수, Web 이미지 호스트 없음, 4
196
200
  ### 그림 전체 표시 (2026-10-28)
197
201
 
198
202
  스핀트에서 직접 그린 정사각형 그림의 모서리가 원형 프로필에서 잘린 사례로 `imageFit="contain"`을 추가했다. 기본 `cover`는 기존 사진 채움을 유지한다. `contain`은 사진·그림 전체를 프레임 안에 넣고 여백을 남긴다. 공통 recipe의 `1 / √2` 비율로 정사각형 대각선까지 원 안에 들어오며 이미지의 모서리를 다시 둥글게 자르지 않는다. Web·Native와 기존 저장 이미지에 동일하게 적용한다. Native `renderImage`의 `size`에는 실제 이미지 영역 크기가 전달된다.
203
+
204
+ **회원 프로필 사진은 원을 꽉 채운다(`cover`, 기본값) — 2026-10-29 사용자 지시.** `contain`은 작은 원 안에 네모난 사진이 작게 들어가고
205
+ 빈 테두리가 남아 사진처럼 보이지 않았다(Spint). 사진 전체는 프로필의 "사진 크게 보기"(`ProfileSummary.avatarView` + `ImageViewer`)에서
206
+ 본다. 회원 아바타·댓글·채팅·알림·목록의 사진에 `contain`을 쓰지 않는다. `contain`은 잘리면 뜻이 바뀌는 비사진 그림(로고·스티커·도감
207
+ 일러스트)에만 쓰고 그 이유를 사용처 주석에 남긴다.
@@ -116,6 +116,7 @@ import { BarChart, Sparkline } from "@hjmds/react-native/chart";
116
116
  | `focusIndex` | `number \| null` | — | 제어 포커스 |
117
117
  | `defaultFocusIndex` | `number \| null` | 마지막 관측 index | 비제어 시작 포커스 |
118
118
  | `onFocusChange` | `(index: number \| null, detail: { scrubbing: boolean }) => void` | — | 스크럽 중 `scrubbing: true`, 손을 떼면 `false` |
119
+ | `showFocusLabel` | `boolean` | `false` | LineChart·BarChart에서 누름을 놓은 뒤에도 선택한 점의 말풍선 유지 (2026-10-29 기록 차트 요청) |
119
120
  | `cursorLabel` | `(index: number) => string` | `categories[index]` | 스크럽 중 커서 위 말풍선 |
120
121
 
121
122
  ### BarChart
@@ -65,10 +65,12 @@ import { CommentThreadRow } from "@hjmds/react-native/screen-flows";
65
65
  | `id`·`parentId`·`author`·`body`·`timeLabel`·`likeCountLabel`·`likeIcon`·`likeLabel` | 필수 | `parentId`가 `null`이면 최상위 댓글 |
66
66
  | `bodyText` | 없음 | 주면 `author`와 본문을 한 문장으로, 시각은 아래 행동 줄로 옮긴다 |
67
67
  | `timePlacement` | `"auto"` | 미게시. `"auto"`는 `bodyText`가 없으면 시각을 작성자 옆에 둔다. `"actions"`는 글이 아닌 본문(꽃 도장·카드)에서도 시각을 본문 아래 반응 요약·답글과 같은 줄 맨 앞으로 옮긴다 |
68
+ | `textActions` | 없음 | 미게시. 답글 줄의 답글 뒤에 붙는 제품 문구 행동 `{ id, label, onPress, disabled? }[]`. 답글과 같은 muted caption 문구 버튼이며 순서대로 놓인다(예: 번역 댓글의 "원본 보기"/"번역 보기"). 문구·상태는 제품이 소유한다 |
68
69
  | `panel` | 없음 | 미게시. 댓글별 인라인 패널(수정·신고·차단 양식). 답글 줄 아래, 길게 누르기 대상 밖에 `panelGap` 8 간격으로 놓인다. 열림 상태·초점·제출·닫기는 제품이 소유한다 |
69
70
  | `mediaAlbum` | 없음 | 미게시. 한 장씩 넘기는 사진 묶음 `{ count, renderPage(index), pageLabel(index, count), previousLabel, nextLabel, aspectRatio?, onPress?(index), index?·onIndexChange?·defaultIndex? }`. `renderPage`는 화면에 보이는 장에만 호출되어 제품이 그 사진만 조회·디코딩한다. 불러오는 중·실패 장은 `MediaAlbumPageStatus`(`@hjmds/react(-native)/screens`, [ChatMessage](chat-message.md#축과-기본값))로 그려 안쪽 내용이 낭독기에 닿게 한다. `media`와 함께 주면 `TypeError`. 본문 아래, 길게 누르기 대상 밖에 놓인다 |
70
71
  | `authorBadge` | 없음 | 미게시. 닉네임 바로 뒤, 같은 줄에 놓는 제품 배지. 간격 `spacing.xxs` 4, 좁은 폭에서는 줄바꿈한다. `bodyText` 모드에서도 작성자·배지·본문 순서로 배치한다. 배지 문구·노출 조건은 제품이 소유한다 |
71
72
  | `avatar` | 없음 | 제품 아바타. 최상위 32·답글 24 폭 칸에 둔다 |
73
+ | `avatarAction` | 없음 | 회원 프로필 이동 `{ label, onPress, disabled? }`. 사진과 **작성자 이름**이 같은 행동을 쓴다(이름은 2026-10-29부터, [Avatar 지침](avatar.md#회원-프로필-이동-2026-10-25)) |
72
74
  | `canReply` | `parentId === null` | 답글 행동 표시 여부 |
73
75
  | `replyDisabled` | false | 답글 행동 비활성 |
74
76
  | `likeAction`·`actions` | 없음 | 기본 좋아요 버튼 대체·옆에 추가 |
@@ -92,7 +94,7 @@ import { CommentThreadRow } from "@hjmds/react-native/screen-flows";
92
94
 
93
95
  - 가상화 host에서 행 사이 간격(`commentThreadRecipe.rowGap`)과 답글 들여쓰기(`replyIndent`)를 직접 줄 때도 recipe 값을 쓴다. 제품 숫자로 다시 정하지 않는다.
94
96
  - 답글 펼치기·정렬·권한·좋아요 저장은 제품이 소유한다. 행은 콜백만 호출한다.
95
- - 행 안에 두 번째 레이아웃을 만들지 말고 `actions`·`likeAction`·`media`·`mediaAlbum`·`panel` 슬롯으로 추가한다. 시각·답글 줄을 본문 노드 안에 다시 그리지 말고 `timePlacement: "actions"`와 `canReply`를 쓴다.
97
+ - 행 안에 두 번째 레이아웃을 만들지 말고 `actions`·`likeAction`·`media`·`mediaAlbum`·`panel` 슬롯으로 추가한다. 시각·답글 줄을 본문 노드 안에 다시 그리지 말고 `timePlacement: "actions"`와 `canReply`를 쓴다. 답글 옆 문구 행동도 본문에 버튼을 두지 말고 `textActions`로 준다.
96
98
  - `mediaAlbum`의 사진 조회·권한 확인·캐시 수명은 제품이 `renderPage` 안에서 소유한다. HJM은 보이는 장만 마운트하고 넘김 상태를 행 안에 둔다(제어형은 `index`+`onIndexChange`).
97
99
 
98
100
  ## 상태와 상호작용
@@ -106,6 +108,7 @@ import { CommentThreadRow } from "@hjmds/react-native/screen-flows";
106
108
  - 2026-10-20 Spint의 “나” 배지가 본문 첫 줄을 차지하던 문제를 해결하기 위해 `authorBadge` 슬롯을 추가했다. 작성자에 붙는 표시는 HJM이 닉네임 옆에 배치하고 제품은 배지 내용만 공급한다. 개발 소스 변경이며 미게시다.
107
109
 
108
110
  - 2026-10-20 Spint 댓글이 촘촘하다는 사용자 피드백에 따라 공통 행 사이 간격을 12→20, 내부 줄 사이 간격을 2→4로 조정했다. Web·Native가 같은 recipe를 사용한다. 개발 소스 변경이며 미게시다.
111
+ - `textActions`: 2026-10-29 Spint 댓글 번역. 사용자가 Instagram처럼 "답글 달기" 옆에 "원본 보기"/"번역 보기"를 두라고 요청했는데, 답글 줄에 제품 행동 자리가 없어 본문 아래 별도 줄로 그리고 있었다.
109
112
  - `timePlacement`·`panel`·`mediaAlbum`: 2026-10-18 HJM 1.30.0 채택 감사. Spint는 꽃 도장 본문에서 시각을 답글 줄에 두려고 시각·답글 행을 본문 안에 다시 그렸고, Utilverse는 수정·신고·차단 양식을 본문 노드 안(길게 누르기 대상 안)에 넣었으며, 사진 묶음은 선택한 장만 조회하는 제품 앨범을 따로 만들었다.
110
113
 
111
114
  ## 플랫폼 차이
@@ -66,6 +66,13 @@ import { ImageViewer } from "@hjmds/react-native/image-viewer";
66
66
  errorLabel={t("viewer.loadFailed")} retryLabel={t("common.retry")} />
67
67
  ```
68
68
 
69
+ ### 아래로 끌어 닫기 (기본 동작, 2026-10-30)
70
+
71
+ 다른 사진 뷰어처럼 확대하지 않은 사진을 아래로 끌면 `onClose`가 불린다(사용자 요청). 사진이 손가락을 따라
72
+ 내려오고 위아래 조작부가 흐려지며, 120pt 이상 끌거나 빠르게 내리면 닫힌다. 짧게 끌거나 위로 끌면 제자리로
73
+ 돌아온다. 확대한 사진은 끌면 이동하므로 닫히지 않는다. 제품은 따로 연결할 것이 없고, 닫기 버튼은 접근성 경로로 남는다.
74
+ Web도 같은 기준으로 터치·펜으로 끌면 닫히고, 마우스는 닫기 버튼과 Esc를 쓴다. 사용자가 모든 앱·웹의 기본 지침으로 정했으므로 제품이 이 동작을 끄거나 자체 뷰어로 대체하지 않는다.
75
+
69
76
  ### 사진을 눌러 크게 보기 (`ImageViewerTrigger`, 2026-10-23)
70
77
 
71
78
  사진을 원시 `button`·`Pressable`로 감싸 뷰어를 열지 않는다(포트폴리오 사이트 `App.tsx`, Spint `MarkPhoto`·`ComposeScreen`이던 우회).
@@ -65,18 +65,19 @@ import { Layout } from "@hjmds/react-native/primitives";
65
65
  | `sidebar.renderOverlay` | Web `(sidebarLandmark: ReactElement) => ReactNode` · Native `(sidebar: ReactNode) => ReactNode` | — | `mode: "overlay"`에서만, 그리고 필수다. 받은 landmark(사이드바 영역)를 SidePanel 등 안에 넣어 돌려준다. 열림·닫힘은 그 SidePanel이 소유한다 |
66
66
  | `sidebar` 모양 | `{ role, mode, label, children, renderOverlay? }` + Web `landmarkProps`·`landmarkRef`, Native `containerProps` | — | `label`은 현지화 문자열 |
67
67
  | `skipLinkLabel`(Web) | 현지화 문자열 | — | Web skip link는 키보드 포커스가 닿을 때만 보인다 |
68
+ | `skipLink`(Web) | `false` | — | skip link를 만들지 않고 랜드마크(`nav`·`main`)로 건너뛰기를 제공한다(WCAG ARIA11). 제품이 기록된 결정으로 링크를 없앤 경우에만 쓴다(다에리 2026-10-02 결정, 2026-10-30 추가). `skipLinkLabel`과 함께 쓰면 `TypeError` |
68
69
  | `mainId`(Web) | 공백 없는 문자열 | 자동 생성 | 비거나 공백이 있으면 `TypeError` |
69
70
  | `layoutStyle`(Web) | 배치 key만 | — | 루트 배치. Native는 `layoutStyle`이 없고 `style`(layout primitive, 1.13 deprecated 제외 대상)만 받는다 |
70
71
 
71
72
  - 이벤트 콜백은 없다. 사이드바 열림 상태는 `renderOverlay` 안의 SidePanel `open`·`onOpenChange`가 소유한다.
72
73
 
73
- - Web persistent 사이드바 폭은 recipe 280. main 최대 폭·좌우 여백은 foundations의 `contentMaxWidth`·`pagePadding.regular`.
74
+ - Web persistent 사이드바 폭은 안에 넣은 Sidebar의 폭 단계를 따른다. main 최대 폭·좌우 여백은 foundations의 `contentMaxWidth`·`pagePadding.regular`.
74
75
 
75
76
  ## 배치
76
77
 
77
78
  | 항목 | 값 | 근거 |
78
79
  | --- | --- | --- |
79
- | 크기 | Web: persistent 사이드바 칸 폭 280(`layoutRecipe.sidebar.width`), main 최대 폭 1200(`layout.contentMaxWidth`). Native: 루트·main은 `flex: 1`이고 여백·폭 제한은 없다 | `design-contracts/src/layout.ts`(`layoutRecipe`), `react/src/layout.tsx`, `react-native/src/primitives.tsx`(Layout) |
80
+ | 크기 | Web: persistent 사이드바 칸은 내용 폭을 따른다 — [Sidebar](sidebar.md)의 폭 단계(260·220·접힘 72)가 정한다(2026-10-30, 예전 280 고정은 Sidebar 옆에 빈 띠를 남겼다. `layoutRecipe.sidebar.width`는 2.x 호환으로만 남는다). main 최대 폭 1200(`layout.contentMaxWidth`). Native: 루트·main은 `flex: 1`이고 여백·폭 제한은 없다 | `design-contracts/src/layout.ts`(`layoutRecipe`), `react/src/layout.tsx`, `react-native/src/primitives.tsx`(Layout) |
80
81
  | 간격 | Web: main 좌우 여백 `spacing.lg` 20(`layout.pagePadding.regular`). skip link는 화면 왼쪽 위에서 `spacing.sm` 12 떨어진다 | `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-layout__skip-link`) |
81
82
  | 순서·정렬 | Web: 루트는 grid다. header·footer는 전체 폭, persistent 사이드바는 왼쪽(RTL에서는 오른쪽) 칸, main은 나머지 칸에서 가운데 정렬된다. Native: header → (persistent 사이드바) → main → footer 순서로 세로로 쌓인다 | `react/src/styles.css`(`.hjm-layout`), `react-native/src/primitives.tsx`(Layout) |
82
83
  | 고정·스크롤 | 스크롤은 main 안의 내용이 소유한다. header·footer는 main 바깥이라 스크롤되지 않는다. Web skip link는 화면에 고정되고 키보드 포커스 때만 내려온다. overlay 사이드바는 그리드 칸을 차지하지 않고 `renderOverlay`의 SidePanel이 위를 덮는다 | `react/src/styles.css`(`.hjm-layout`), `react/src/layout.tsx` |
@@ -97,7 +98,7 @@ Web (persistent sidebar) Native
97
98
 
98
99
  ## 꼭 지킬 것
99
100
 
100
- - Web에서 `header`나 `sidebar`가 있으면 `skipLinkLabel`(현지화)이 필수다. 없으면 `TypeError`.
101
+ - Web에서 `header`나 `sidebar`가 있으면 `skipLinkLabel`(현지화)이 필수다. 없으면 `TypeError`. 명시적 `skipLink={false}`만 예외다.
101
102
  - 화면마다 Layout을 다시 세우지 않는다. `main` landmark는 하나여야 한다.
102
103
  - 어느 화면 폭에서 persistent↔overlay를 바꿀지는 제품이 정한다. Layout은 전환 규칙을 갖지 않는다.
103
104
  - 사이드바 내용·헤더 내용·푸터 내용은 각 컴포넌트가 소유한다. Layout에 상태를 다시 만들지 않는다.
@@ -103,6 +103,31 @@ Web의 텍스트/이모지 artwork는 원형72 프레임 안에서 glyph.xxl48
103
103
  - 남의 프로필에는 `nameEdit`를 넣지 않고, 사진은 회원 아바타 기준(확대·신고)을 따른다.
104
104
  - 제품이 ProfileSummary 대신 고유 머리를 그리는 경우에도 사진 = 사진 변경, 이름 = 닉네임 변경을 지킨다. 이름이 그림(예: Spint 내 정원의 나무 팻말)이면 연필을 따로 두지 않고 그 이름 그림 자체를 닉네임 변경 버튼으로 한다(2026-10-25 사용자 결정).
105
105
 
106
+ ### 프로필 사진 올리기: 원으로 맞추기 (2026-10-29)
107
+
108
+ 사용자 지시로 모든 앱·웹에서 프로필 사진을 올릴 때 **인스타그램처럼 원 안에서 위치와 크기를 맞춘다.** 시스템 정사각 자르기
109
+ (`allowsEditing`, `aspect: [1, 1]`)는 원형 아바타와 모양이 달라 결과를 예측할 수 없었다.
110
+
111
+ - 사진을 고른 뒤(시스템 편집 끔) `image-viewer` 하위 경로의 `CircularPhotoCropper`를 연다. 끌어서 옮기고 두 손가락(웹은 휠·두 손가락)으로
112
+ 확대한다. 원 밖은 어둡게 가리고, 놓으면 원 안에 빈 곳이 생기지 않게 되돌아간다(`clampCircularPhotoCrop`, 최대 `maxZoom` 4배).
113
+ - `onConfirm(crop)`은 원본 픽셀 정사각형 `{ originX, originY, width, height }`이다. 제품이 그 영역을 자르고(Native `expo-image-manipulator`
114
+ `crop`, Web canvas) 기존 크기 제한·압축으로 올린다. 자르기·업로드 중에는 `busy`로 두 버튼을 잠근다.
115
+ - 정사각형을 저장하고 원 모양은 표시(`cover`)가 만든다. 원형 투명 PNG를 만들지 않는다.
116
+ - 글 첨부 사진처럼 원형이 아닌 사진에는 쓰지 않는다.
117
+
118
+ ### 남의 프로필: 사진을 누르면 크게 보기 (2026-10-29)
119
+
120
+ 사용자 지시로 모든 앱·웹의 회원 프로필 화면에서 **그 회원의 프로필 사진을 누르면 사진이 크게 열린다.** Spint 회원 프로필에서
121
+ 사진을 눌러도 반응이 없었고, Utilverse만 제품 코드로 확대를 따로 만들어 앱마다 동작이 달랐다.
122
+
123
+ - `avatarView={{ label: t("photo.view"), onPress: () => setViewerOpen(true) }}`를 준다. HJM이 아바타 틀 전체를 이미지 버튼
124
+ (`ImageViewerTrigger`, Native `imagebutton` 역할)으로 만들고 크기·원형·누름 상태를 소유한다.
125
+ - 제품은 `image-viewer` 하위 경로의 `ImageViewer`를 연다(Native는 gesture-handler·zoom-toolkit 선택 의존성이 그 경로에만 있다).
126
+ 사진 조회·권한·캐시는 `items`/`renderImage`로 공급한다. 사진 신고는 제품의 기존 경로를 유지하며 확대 화면에 둘 때는 `actions`를 쓴다.
127
+ - 사진이 없어 이니셜·기본 얼굴만 있으면 `avatarView`를 넣지 않는다(크게 볼 사진이 없다).
128
+ - 내 프로필은 사진 = 사진 변경(`Avatar.action`)이므로 `avatarView`를 넣지 않는다. 두 행동을 한 사진에 같이 넣지 않는다.
129
+ - 제품 고유 머리(ProfileSummary 대신 그린 카드)도 같은 규칙을 `ImageViewerTrigger` + `ImageViewer`로 지킨다.
130
+
106
131
  ### 프로필 카드 틀 (2026-10-25)
107
132
 
108
133
  같은 날 사용자 지적: Utilverse 프로필 카드(평평한 `default`, radius `lg`)가 BurnTok 카드(`raised` 그림자, 테두리, radius `xl`)와
@@ -171,3 +171,13 @@ Chip/직접 wrap으로 조립하고 있었다. `presentation="chips"`는 기존
171
171
  공통 레일이 가로 스크롤과 선택 항목 노출을 소유한다. 제품은 `renderLabel`로 요일·일자 등
172
172
  콘텐츠만 공급한다. 비활성인 선택 날짜도 표시할 수 있다. 조회 실패를 비활성 날짜로 바꾸지 않는다.
173
173
  이 API는 아직 게시하지 않은 개발 소스 변경이다.
174
+
175
+ ### 선택 면의 브랜드 강조 (2026-10-29 개발 소스 · 미게시)
176
+
177
+ `selectionTone="neutral" | "brand" | "brand-outline"`를 Web/Native에서 지정한다. 기본값은 `neutral`이다.
178
+ 오늘야구 날짜 카드의 선택일을 파란 배경·흰 글자로 강조해 달라는 요청이 계기다.
179
+ `brand`는 테마의 `primary`/`onPrimary` 쌍을 사용하고 미선택 면은 기존 canvas를 유지한다(light는 흰색).
180
+ 라벨·설명과 Native `renderLabel` 안의 중립 Text는 선택 면의 글자색을 상속한다.
181
+ 브랜드색·상태색을 명시한 미리보기는 제품이 의미에 맞게 공급한다.
182
+
183
+ 같은 날 사용자 후속 요청으로 `brand-outline`은 선택 배경도 canvas(light 흰색)로 유지하고 테두리만 대표색 primary로 강조한다. 일반 글자색과 비활성 계약은 유지한다.
@@ -134,7 +134,7 @@ Native 스크롤 본문은 viewport 높이만큼 content를 채운다(미게시)
134
134
  | --- | --- | --- |
135
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` |
136
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` |
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` |
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 사용자 결정, 미게시). 스크롤 본문은 이미 맨 위일 때 시작한 아래 끌기만 시트가 가져가고, 스크롤된 상태에서는 같은 동작이 먼저 내용을 위로 되돌린다. 스크롤하지 않는 본문 안에 들어간 목록 스크롤(시트 안 `SearchScreen embedded`)도 같은 규칙을 따른다. Native는 그 목록이 시트의 끌기 연결을 넘겨받아 목록 위치로 판단한다(2026-10-30 Spint 도시 시트에서 스크롤된 목록을 되돌리면 시트도 함께 내려가던 결함, 미게시). iOS는 UIScrollView가 JS 응답자 아래서도 스스로 끌기를 가져가므로 본문 맨 위의 당김(overscroll)으로 시트를 움직이고 UIKit 탄성 거리로 환산한 같은 임계값을 쓴다. Web 본문 끌기는 터치만 받고 마우스는 글자 선택을 위해 손잡이를 쓴다. 입력칸에서 시작한 끌기와 가로 끌기는 본문 안의 행동으로 남긴다 | `sheetRecipe.header`·`handle`, `.hjm-sheet__footer`, `.hjm-sheet__handle` |
138
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` |
139
139
  | 좁은 폭·큰 글자 | 폭 < 640이면 하단 시트가 화면 폭을 채운다. Web footer는 줄을 바꾼다. 큰 글자로 내용이 길어지면 `auto`는 최대 90%에서 멈추고 본문이 스크롤된다(`full`은 위 safe area까지) | `.hjm-sheet[data-placement="bottom"]`, `maxHeightRatio` |
140
140
 
@@ -94,6 +94,8 @@ Native: 없음. 폰은 BottomNavigation, 태블릿 split view는 navigator가
94
94
  | `renderLink` | `(props: SidebarLinkRenderProps) => ReactElement` | 일반 `<a>` | client router 어댑터. props(`href`·`className`·`aria-current`·접힘 `aria-label`·`onClick`·`children` 등)를 모두 자기 링크에 펼친다. 비활성·`destination` 없는 항목은 어댑터를 거치지 않는다. `BottomNavigation.renderLink`와 같은 계약. 개발 소스·미게시 |
95
95
  | `renderIcon` | `(item) => ReactNode` | — | 접힌 레일에 필수 |
96
96
  | `renderBadge` | `(count: number, item) => ReactNode` | 숫자 배지 | — |
97
+ | `header` | `ReactNode` | — | 그룹 위 제품 칸(브랜드 표식·주 행동). 2026-10-30 다에리·번뚝 데스크탑 셸이 HJM 밖에 따로 그리던 자리 |
98
+ | `footer` | `ReactNode` | — | 맨 아래에 붙는 제품 칸(설정·언어). 그룹이 짧아도 바닥에 선다(위 경계선 1px) |
97
99
  | `className` · `layoutStyle` | 문자열 · 배치 전용 style 객체 | — | 루트 배치만. 폭·색은 recipe 소유 |
98
100
 
99
101
  ## 배치
@@ -42,7 +42,7 @@ import { Tag } from "@hjmds/react/display";
42
42
  // Native
43
43
  import { Tag } from "@hjmds/react-native/data-display";
44
44
 
45
- <Tag layoutStyle={{ marginTop: 4 }}>{t("player.position.leftField")}</Tag>
45
+ <Tag variant="outline" layoutStyle={{ marginTop: 4 }}>{t("player.position.leftField")}</Tag>
46
46
  ```
47
47
 
48
48
  ## 축과 기본값
@@ -51,20 +51,23 @@ import { Tag } from "@hjmds/react-native/data-display";
51
51
  | --- | --- | --- | --- |
52
52
  | `children` | 문자열 | — (필수) | 문자열만 받는다. 비어 있으면 `TypeError` |
53
53
  | `tone` | `neutral` · `info` · `success` · `attention` · `brand` | `neutral` | `warning`·`danger`는 없다(이유는 계약 문서) |
54
+ | `variant` | `default` · `outline` | `outline` | outline은 배경 없는 테두리형 캡슐 |
54
55
  | `layoutStyle` | `HjmCompositionStyleProp` | — | 바깥 여백·`alignSelf` 같은 배치. Web·Native 모두 |
55
56
  | `accessibilityLabel`(Native) | `string` | 라벨 | — |
56
57
  | `style`·`labelStyle`(Native) | 지원하지 않음 | — | 제거된 속성 — `layoutStyle` 또는 `tone`. 현재 공개 props에서 제거되었다 |
57
58
 
58
- 모양은 radius `sm` 사각형, 최소 높이 20(큰 글자에서 늘어남), caption 크기 medium(500) 글자다.
59
+ 작은 사각형은 명시적인 `variant="default"`, 최소 높이 20, caption medium(500)이다.
60
+ 기본 `variant="outline"`은 투명 배경, radius `full`, 최소 높이 32, 좌우 10·상하 6, caption regular(400)이다.
61
+ 프로필의 긴 사실 정보처럼 배경 채움 없이 나열할 때 쓴다. neutral 글자는 `content.primary`를 따른다.
59
62
 
60
63
  ## 배치
61
64
 
62
- Native의 모서리는 Provider의 `tokens.radius.sm`이다. metadata 의미와 tone은 변하지 않는다.
65
+ Native 기본 모서리는 `tokens.radius.full`, 명시적인 default 변형은 `tokens.radius.sm`이다. metadata 의미와 tone은 변하지 않는다.
63
66
 
64
67
  | 항목 | 값 | 근거 |
65
68
  | --- | --- | --- |
66
- | 크기 | 최소 높이 20, 테두리 1, radius `sm` 8. 누르는 대상이 아니므로 44 터치 영역을 두지 않는다 | `tagRecipe.size`, `styles.css` `.hjm-tag`, `react-native/src/data-display.tsx` |
67
- | 간격 | 좌우 여백 `spacing.xxs` 4. 제목 옆이면 간격 `spacing.xs` 8, 여러 개는 가로로 `spacing.xxs` 4~`xs` 8 | `tagRecipe.size` |
69
+ | 크기 | 기본 최소 높이 32, 테두리 1, radius `full` 999. 누르는 대상이 아니므로 44 터치 영역을 두지 않는다 | `tagRecipe.size`, `styles.css` `.hjm-tag`, `react-native/src/data-display.tsx` |
70
+ | 간격 | 기본 안쪽 여백 좌우 10·상하 6. 제목 옆이면 간격 `spacing.xs` 8, 여러 개는 가로로 `spacing.xxs` 4~`xs` 8 | `tagRecipe.size` |
68
71
  | 순서·정렬 | 제목·행 옆에 붙는 작은 메타 표시다. 제목 옆이면 제목과 세로 가운데 정렬한다 | — |
69
72
  | 고정·스크롤 | — | — |
70
73
  | 좁은 폭·큰 글자 | 여러 개는 줄바꿈(`wrap`)한다. 큰 글자에서는 높이가 글자에 맞춰 늘어난다(고정 높이 금지) | `tagRecipe.size.minHeight` 주석 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hjmds/design-contracts",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Renderer-neutral design contracts, tokens, recipes, and behaviors shared by HJM products.",
5
5
  "license": "MIT",
6
6
  "repository": {