@hjmds/design-contracts 1.35.0 → 1.36.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 (53) hide show
  1. package/dist/catalog.d.ts +32 -1
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/component-recipes.d.ts +36 -5
  4. package/dist/component-recipes.d.ts.map +1 -1
  5. package/dist/component-recipes.js +12 -4
  6. package/dist/component-recipes.js.map +1 -1
  7. package/dist/icon.d.ts +1 -1
  8. package/dist/icon.d.ts.map +1 -1
  9. package/dist/icon.js +2 -1
  10. package/dist/icon.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/password-field.d.ts +1 -1
  15. package/dist/password-field.d.ts.map +1 -1
  16. package/dist/password-field.js +1 -1
  17. package/dist/password-field.js.map +1 -1
  18. package/dist/recipes.d.ts +1 -1
  19. package/dist/recipes.d.ts.map +1 -1
  20. package/dist/recipes.js.map +1 -1
  21. package/dist/screen-patterns.d.ts.map +1 -1
  22. package/dist/screen-patterns.js +2 -1
  23. package/dist/screen-patterns.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/dist/version.js.map +1 -1
  27. package/docs/generated/component-maturity.md +1 -1
  28. package/docs/generated/renderer-evidence.json +3 -3
  29. package/docs/generated/renderer-evidence.md +1 -1
  30. package/docs/generated/showcase-manifest.json +1 -1
  31. package/docs/password-field.md +4 -2
  32. package/docs/screen-patterns.md +2 -2
  33. package/docs/usage/README.md +1 -1
  34. package/docs/usage/SELECTION.md +16 -1
  35. package/docs/usage/components/button.md +1 -1
  36. package/docs/usage/components/card.md +1 -1
  37. package/docs/usage/components/chat-message.md +1 -1
  38. package/docs/usage/components/comment-thread-row.md +6 -0
  39. package/docs/usage/components/comment-thread-screen.md +6 -0
  40. package/docs/usage/components/icon.md +4 -0
  41. package/docs/usage/components/list-detail-screen.md +2 -0
  42. package/docs/usage/components/load-more.md +2 -0
  43. package/docs/usage/components/menu.md +86 -21
  44. package/docs/usage/components/password-field.md +3 -1
  45. package/docs/usage/components/photo-source-sheet.md +14 -8
  46. package/docs/usage/components/repeatable-field-group.md +1 -0
  47. package/docs/usage/components/segmented-control.md +1 -1
  48. package/docs/usage/compositions/family-drawer.md +1 -1
  49. package/docs/usage/compositions/live-list.md +1 -1
  50. package/docs/usage/compositions/photo-source.md +2 -2
  51. package/docs/usage/compositions/shortcut-rail.md +1 -1
  52. package/docs/usage/compositions/task-controls.md +3 -1
  53. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-20
8
8
  - 근거: [Dropdown 판정](../../dropdown.md), [Popover 경계](../../popover.md), `src/component-recipes.ts`(`menuRecipe`)
9
9
  - 스토리북: `배포/컴포넌트/탐색/메뉴`
10
10
 
@@ -14,6 +14,48 @@
14
14
  보기 옵션 켜고 끄기처럼 안정적인 id를 가진 action·단일 선택·다중 선택 목록이 여기에 속한다.
15
15
  트리거·떠 있는 표면·항목 목록을 Menu 하나가 소유하므로 별도 Dropdown은 없다.
16
16
 
17
+ ## 세 가지 표시 방식 — 개발 소스, 미게시
18
+
19
+ 2026-10-20 유틸버스 더보기에서 제목·목록·닫기가 하나의 넓은 빈 표면에 떠 있어 항목 구분이 흐렸고,
20
+ 그룹 목록·앵커 메뉴·하단 행동 목록을 상황별로 함께 쓰자는 사용자 결정으로 `presentation`을 추가했다.
21
+ 별도 메뉴 컴포넌트 세 개가 아니라 **하나의 Menu에서 항목·권한·선택·닫힘 후 실행을 공유하는 표현 축**이다.
22
+ 새 prop은 설치된 npm 버전에 있다고 가정하지 않는다. 로컬 소스 개발 또는 이 변경을 포함한 게시 버전에서 사용한다.
23
+
24
+ | `presentation` | 선택하는 상황 | 배치와 닫기 |
25
+ | --- | --- | --- |
26
+ | `grouped` | 화면 전체의 보조 행동·명확히 구분된 목록 | 중앙 패널, 연한 목록 면·행 사이 선, 분리된 닫기 버튼. 폭 상한 288, 안쪽 12, 목록–닫기 간격 8, 기본 행 높이 52 |
27
+ | `anchored` | 댓글 ⋮·항목 관리처럼 트리거 대상과의 연결이 중요 | 트리거의 논리적 끝에 맞추고 아래 공간이 부족하면 위로 배치. Native 폭 상한 280, 안쪽 8, 기본 행 높이 48. 별도 닫기 행 없이 바깥 누름·Escape/back으로 닫기 |
28
+ | `bottom` | 사진·파일·공유 등 모바일에서 아래쪽으로 접근하는 행동 목록 | 하단에서 목록 면과 닫기 버튼을 별도 표면으로 분리. 폭 상한 420, 바깥 패널 padding 0·행 좌우 12, 목록–닫기 간격 8, 기본 행 높이 56. 하단 safe area 포함 |
29
+
30
+ 값은 `menuRecipe.presentations`가 소유한다(Native pt, Web CSS px/rem 대응). `density="compact"`는 기존 최소 터치 높이 44를 유지한다.
31
+ Web의 기존 앵커 메뉴 폭·충돌 해소 계약은 유지한다. Native 기본은 `grouped`, Web 기본은 `anchored`다.
32
+ 일반적인 ‘더 보기’는 제목 줄을 반복하지 않는다. Native `title`은 기본 생략하며 명시한 대상 제목만 표시하고 접근성 이름은 `triggerLabel`로 유지한다.
33
+ Web의 `grouped`/`bottom`에는 지역화한 `dismissLabel`을 반드시 전달한다.
34
+
35
+ 공통 화면이 상황에 맞는 기본값을 정한다. 댓글의 `commentMenu.presentation`은 양 renderer에서 `anchored`가 기본이며
36
+ 제품 고유 상호작용 이유가 있을 때만 바꾼다. 유틸버스 홈은 `grouped`를 명시한다.
37
+ 항목 수·로딩 상태·창 너비만으로 세 방식 사이를 자동 전환하지 않는다. 앵커형의 위치·가용 높이만 화면에 맞춰 계산한다.
38
+ 폭·여백·구분선·가림막·키보드/포커스·다음 모달 실행 순서를 앱에서 재조립하지 않는다.
39
+
40
+ ```tsx
41
+ // Native: 홈 보조 행동
42
+ <Menu presentation="grouped" triggerLabel={t("home.more")} dismissLabel={t("common.close")}
43
+ items={items} onActionAfterDismiss={handleAction} />
44
+ // Native: 사진 관련 행동 목록 (사진 선택 자체의 전용 구성은 PhotoSourceSheet 우선)
45
+ <Menu presentation="bottom" triggerLabel={t("photo.actions")} dismissLabel={t("common.close")}
46
+ items={photoActions} onActionAfterDismiss={handlePhotoAction} />
47
+ // Web: 같은 표현 축; modal 표현에는 dismissLabel이 필요하다.
48
+ <Menu presentation="grouped" label={t("home.more")} dismissLabel={t("common.close")}
49
+ trigger={<IconButton label={t("home.more")}>⋯</IconButton>} items={items} />
50
+ // 공통 댓글 화면: 목록과 권한은 제품, 트리거와 Menu는 HJM
51
+ commentMenu={{ presentation: "anchored", label: commentMenuLabel,
52
+ dismissLabel: t("common.close"), items: allowedItems, onAction: handleCommentAction }}
53
+ ```
54
+
55
+ `bottom`도 안정적인 id의 행동/선택 목록이면 Menu다. 입력 본문이 생기면 Dialog·Sheet,
56
+ 확인 문구와 버튼만 필요하면 AlertDialog를 사용한다. Menu가 닫힌 후 여는 원칙은 세 표현 모두 동일하다.
57
+ 실행 예제는 양 Showcase의 Menu `Grouped`·`Anchored`·`Bottom`이다.
58
+
17
59
  ## 쓰지 않을 때
18
60
 
19
61
  | 상황 | 대신 쓸 것 |
@@ -69,12 +111,13 @@ import { Menu } from "@hjmds/react-native/navigation";
69
111
 
70
112
  | prop | 값 | 기본값 | 설명 |
71
113
  | --- | --- | --- | --- |
114
+ | `presentation` | `grouped` · `anchored` · `bottom` | Web `anchored` · Native `grouped` | 위의 표현 선택 표 참조 |
72
115
  | `items` · `sections` | 둘 중 정확히 하나(Native는 `source`도 가능) | 필수 | 섹션마다 `label` 또는 `accessibilityLabel`이 필요하다. 활성 항목이 하나도 없으면 `TypeError` |
73
116
  | 항목 모양 | Web `{ id, label: ReactNode, textValue?, description?, leading?, trailing?, tone?, disabled? }` · Native `{ id, label: string, textValue?, description?, shortcut?, tone?, disabled? }` | — | Web `label`이 글이 아니면 `textValue` 필수(typeahead) |
74
117
  | 항목 `tone` | `neutral` · `danger` | `neutral` | |
75
118
  | `onAction` | Web `(id: string) => void` · Native `(value) => void \| Promise<void>` | — | 고르는 즉시. 메뉴가 닫히기 전이다 |
76
119
  | `onActionAfterDismiss` | Web `(id: string) => void` · Native `(value) => void \| Promise<void>` | — | 메뉴가 실제로 닫힌 뒤 한 번. Dialog·Sheet 열기·화면 이동은 여기서 한다 |
77
- | `open` · `defaultOpen` · `onOpenChange` | `onOpenChange: (open: boolean, detail) => void` — Web `detail = { reason }`, Native 두 번째 인자가 `reason` | 비제어 `false` | reason: Web `trigger` · `selection` · `escape` · `outside` · `tab`, Native `trigger` · `selection` · `escape` · `outside` · `programmatic`. 제어하면 `onOpenChange` 필수(Web) |
120
+ | `open` · `defaultOpen` · `onOpenChange` | `onOpenChange: (open: boolean, detail) => void` — Web `detail = { reason }`, Native 두 번째 인자가 `reason` | 비제어 `false` | reason: Web `trigger` · `selection` · `escape` · `outside` · `tab`, Native `trigger` · `selection` · `escape` · `outside` · `programmatic`. 개발 소스의 Web 모달 닫기 버튼도 `programmatic`. 제어하면 `onOpenChange` 필수(Web) |
78
121
  | 선택(Web) | `selectionMode`: `action` · `single`(`value: string \| null`, `onValueChange(value: string)`) · `multiple`(`value: ReadonlySet<string>`, `onValueChange(value: ReadonlySet<string>)`) | `action` | `defaultValue`로 비제어 |
79
122
  | 선택(Native) | `selection`: `{ mode: "none" }` · `{ mode: "single", selectedKey, onSelectionChange(key \| null) }` · `{ mode: "multiple", selectedKeys: ReadonlySet, onSelectionChange(keys) }` | `{ mode: "none" }` | `defaultSelectedKey(s)`로 비제어 |
80
123
  | `density` | `comfortable` · `compact` | `comfortable`(recipe) | Web은 생략하면 Provider density를 따른다 |
@@ -88,27 +131,25 @@ Native popover와 항목 모서리는 Provider token에서 recipe 역할을 해
88
131
 
89
132
  | 항목 | 값 | 근거 |
90
133
  | --- | --- | --- |
91
- | 크기 | 항목 높이 `comfortable` 56(`layout.rowHeight.singleLine`) · `compact` 44(`control.minTouchTarget`). Web 표면 폭 `13.75rem`~`min(24rem, 90vw)`, 높이 최대 `100dvh − 2 × spacing.md`. Native 표면 폭 100%·최대 320(`menuRecipe.maxWidth`), 높이 최대 75% | `menuRecipe.density`, `.hjm-menu__content`, `react-native/src/navigation.tsx` |
92
- | 간격 | 항목 좌우 `spacing.sm` 12, leading·문구·단축키 사이 12. 섹션 제목 좌우 12 · 위아래 `spacing.xs` 8. Web: 트리거와 8(`menuRecipe.sideOffset`), 화면 가장자리 8(`collisionPadding`), 안쪽 `spacing.xs` 8(`menuRecipe.surface.padding`; 2026-10-06까지 4, 1.13.0 변경), 표면·항목 radius `radius.md` 12, 구분선 위아래 4 · 좌우 8. Native: 바깥 여백·안쪽 `spacing.md` 16, 제목·목록·닫기 사이 12, radius `radius.lg` 16 | `collectionItemContract`, `menuRecipe.sectionLabel`·`surface`, `.hjm-menu__*`, `useAnchoredPopup` |
93
- | 순서·정렬 | 일반 행동을 위에, `danger` 행동은 맨 아래. Web `align="start"`(기본)는 트리거 시작 끝, `end`는 끝 끝에 맞춘다(행 끝 ⋯은 `end`). Native는 위→아래 가운데 정렬한 보조 제목(`label`, muted) → 항목 → 닫기 버튼(`ghost`) | `menuRecipe`, `src/overlays.tsx` |
94
- | 고정·스크롤 | Web: 트리거에 붙는 portal(z-index `layer.dropdown` 400), 아래가 모자라면 위로 뒤집고 가로는 화면 안으로 민다. 넘치면 표면 안 스크롤. Native: `Modal`로 화면 **가운데**, scrim을 누르면 닫히고 항목 목록만 스크롤 | `useAnchoredPopup`, `react-native/src/navigation.tsx`(`Menu`) |
134
+ | 크기 | 항목 높이 `comfortable` 56(`layout.rowHeight.singleLine`) · `compact` 44(`control.minTouchTarget`). Web 표면 폭 `13.75rem`~`min(24rem, 90vw)`, 높이 최대 `100dvh − 2 × spacing.md`. Native 폭은 `menuRecipe.presentations`의 상한과 가용 폭 중 작은 값, 그룹/하단 높이 최대 75%, 앵커 높이는 위·아래 가용 공간 | `menuRecipe.density`, `.hjm-menu__content`, `react-native/src/navigation.tsx` |
135
+ | 간격 | 항목 좌우 `spacing.sm` 12, leading·문구·단축키 사이 12. 섹션 제목 좌우 12 · 위아래 `spacing.xs` 8. Web: 트리거와 8(`menuRecipe.sideOffset`), 화면 가장자리 8(`collisionPadding`), 안쪽 `spacing.xs` 8(`menuRecipe.surface.padding`; 2026-10-06까지 4, 1.13.0 변경), 표면·항목 radius `radius.md` 12, 구분선 위아래 4 · 좌우 8. Native: safe area 바깥 여백 `spacing.md` 16, 표현별 안쪽 여백은 `presentations.padding`, 영역 사이 `spacing.xs` 8, radius `radius.lg` 16 | `collectionItemContract`, `menuRecipe.sectionLabel`·`surface`, `.hjm-menu__*`, `useAnchoredPopup` |
136
+ | 순서·정렬 | 일반 행동을 위에, `danger` 행동은 맨 아래. Web `align="start"`(기본)는 트리거 시작 끝, `end`는 끝 끝에 맞춘다(행 끝 ⋯은 `end`). Native는 선택한 presentation에 따라 항목 → 분리된 닫기(앵커형은 생략). 명시한 대상 제목만 앞에 표시 | `menuRecipe`, `src/overlays.tsx` |
137
+ | 고정·스크롤 | Web: 트리거에 붙는 portal(z-index `layer.dropdown` 400), 아래가 모자라면 위로 뒤집고 가로는 화면 안으로 민다. 넘치면 표면 안 스크롤. Native: `Modal` 안의 중앙/트리거 옆/하단 배치. 가림막을 누르면 닫히며 항목 목록만 스크롤. Web 모달 표현도 HJM 공통 포커스 격리·배경 스크롤 잠금을 사용 | `useAnchoredPopup`, `react-native/src/navigation.tsx`(`Menu`) |
95
138
  | 좁은 폭·큰 글자 | Web 폭 상한 `90vw`, 항목 문구는 줄바꿈된다. Native 높이 상한 75%를 넘으면 목록이 스크롤된다. 짧은 목록은 내용 높이만 사용한다(`flexGrow: 0`) | `.hjm-menu__content`, `maxHeight: "75%"` |
96
139
 
97
140
  ```text
98
- Web (트리거 아래, 앵커) Native (Modal, 화면 가운데)
99
- ┌ 행 ───────────────── [⋯] ┐ ┌──────────── 화면 ─────────────┐
100
- └──────────────────────────┘ │░░░░░░░░ scrim(누르면 닫힘) ░░░│
101
- ↓ 8 │░ ┌─────────────────────────┐ ░│
102
- ┌─────────────────┐ │░ │ 제목 │ ░│
103
- │ 편집 │ ← 56 │░ │ 편집 │ ░│ ← 항목 56
104
- │ 공유 │ │░ │ 공유 │ ░│ (스크롤)
105
- │─────────────────│ │░ │ 삭제 (danger) │ ░│
106
- │ 삭제 (danger) │ │░ │ [ 닫기 ] │ ░│ ← ghost
107
- └─────────────────┘ │░ └─────────────────────────┘ ░│
108
- 13.75rem ~ 24rem │░ 폭 ≤320 · 높이 ≤75% · 여백 16│
109
- └───────────────────────────────┘
141
+ 그룹형 (중앙) 앵커형 (트리거 옆) 하단형 (하단)
142
+ ╭─────────────────╮ 행 ⋮ ╭─────────────────╮
143
+ │ ╭─────────────╮ │ ╭────────────╮ │ 저장 │
144
+ │ │ 최근 사용 │ │ │ 신고 │ │─────────────────│
145
+ │ │─────────────│ │ │────────────│ │ 공유 │
146
+ │ │ 설정 │ │ │ 차단 │ ╰─────────────────╯
147
+ │ ╰─────────────╯ │ ╰────────────╯ ╭─────────────────╮
148
+ │ [ 닫기 ] │ │ 닫기 │
149
+ ╰─────────────────╯ ╰─────────────────╯
110
150
  ```
111
151
 
152
+
112
153
  ## 꼭 지킬 것
113
154
 
114
155
  - 항목 id는 비어 있지 않고 유일해야 하며, 활성 항목이 하나 이상 있어야 한다. 어기면 렌더 중 던진다.
@@ -123,12 +164,36 @@ Web (트리거 아래, 앵커) Native (Modal, 화면 가운데)
123
164
  | 항목 | Web | Native |
124
165
  | --- | --- | --- |
125
166
  | 트리거 | `trigger`(element 필수), `label`은 메뉴 이름 | `triggerLabel` 필수, `trigger`/`renderTrigger` 선택 |
126
- | 닫기 버튼 이름 | 없음 | `dismissLabel` 필수 |
167
+ | 닫기 버튼 이름 | 앵커형은 없음, 그룹/하단형은 `dismissLabel` 필수 | `dismissLabel` 필수 (앵커형은 바깥 닫기의 접근성 이름) |
127
168
  | 선택 | `selectionMode="single"\|"multiple"` + `value`/`onValueChange` | `selection={{ mode, selectedKey(s), onSelectionChange }}` |
128
169
  | 선택 후 실행 | 없음 | `onSelectionAfterDismiss` |
129
170
  | 상태 | `disabled` | `disabled`, `readOnly`(+`readOnlyLabel`), `busy`, `onRetry`/`retryLabel` |
130
- | 표면 | 트리거에 붙는 portal(`portalContainer`) | `Modal` |
171
+ | 표면 | 앵커형 `AnchoredPortal`, 그룹/하단형 `HjmPortal` (`portalContainer`) | 세 표현 모두 하나의 `Modal` |
131
172
  | 항목 label | `ReactNode`(+`textValue`) | `string` |
132
173
  | 배치 | `className`, `layoutStyle` | `layoutStyle`(`style`은 제거된 속성) |
133
174
 
134
- 2026-10-18 스핀트 댓글 메뉴 사진에서 단일 행동에도 넓은 폭과 빈 공간이 생겼다. Native 메뉴 폭을 기존 recipe 상한에 맞추고 목록의 기본 확장을 없앴으며, 닫기는 조용한 행동으로 분리했다. 사용자 후속 요청으로 닫기 위 구분선은 제거했다.
175
+ 과거 근거(2026-10-20의 세 표현 도입 전): 2026-10-18 스핀트 댓글 메뉴 사진에서 단일 행동에도 넓은 폭과 빈 공간이 생겼다. Native 메뉴 폭을 기존 recipe 상한에 맞추고 목록의 기본 확장을 없앴으며, 닫기는 조용한 행동으로 분리했다. 사용자 후속 요청으로 닫기 위 구분선은 제거했다.
176
+
177
+ ## 전사 개발 적용 선택 — 2026-10-20
178
+
179
+ 같은 날 사용자가 모든 앱의 메뉴를 상황별 세 표현으로 명시해 달라고 요청했다. 화면/구성은 기본값에
180
+ 우연히 기대지 않고 사용 의도를 정한다. 제품 adapter는 `presentation`을 전달할 수 있어야 하며,
181
+ 번뚝 `AppMenu`는 사용처마다 명시하도록 요구한다.
182
+
183
+ | 사용 의도 | 선택 | 적용 |
184
+ | --- | --- | --- |
185
+ | 화면 전체의 보조 행동 | `grouped` | 유틸버스 홈, 번뚝 실행 화면의 방문자 행동(앱·웹) |
186
+ | 모바일에서 작품을 관리하는 묶음 작업 | `bottom` | 번뚝 실행 화면의 소유자 관리. 웹은 같은 작업을 중앙 `grouped`로 표시 |
187
+ | 특정 콘텐츠·작성자·댓글·메모에 붙는 행동 | `anchored` | 번뚝 피드/이야기/프로필/아이디어 메시지/댓글, 유틸버스 메모/댓글, 스핀트 댓글 |
188
+ | 옆의 제어를 바꾸는 선택 | `anchored` | HJM SplitAction·ModeAction의 ChoiceMenu, SearchScreen 결과 정렬 |
189
+
190
+ 명시적인 제품/플랫폼 선택이며 항목 수나 화면 폭으로 표현을 자동 전환하지 않는다. 메뉴가 닫힌 뒤의
191
+ 이동·신고·차단·삭제 확인·시스템 공유 순서를 세 표현 모두 유지한다. 제품에서 표면 색을 따로 덮어
192
+ 메뉴 전용 모양을 만들지 않고 HJM Provider 테마와 공개 아이콘 슬롯을 사용한다.
193
+
194
+ **복합 구성은 보존한다.** ChatMessage의 길게 누르기는 반응 선택·원문/사진 미리보기·메시지 행동이
195
+ 한 묶음인 전용 구성이다. 일반 Menu로 치환하면 반응과 앨범 조작이 사라지므로 기존 hold 구성과 그
196
+ 앵커 배치를 유지한다. PhotoSourceSheet·미디어 선택·언어/팀 선택·입력/신고 시트·확인창·시스템 공유창도
197
+ 각각의 전용 계약을 따른다. 탭·내비게이션의 ‘메뉴’ 아이콘과 본문 ‘더 보기’ 펼침은 행동 메뉴가 아니다.
198
+ 현재 등록 앱의 경로별 조사·검증 범위는 포트폴리오 `docs/design/STANDARD_ADOPTION.md`의 메뉴 절을 따른다.
199
+ 새 `presentation` 축과 소비자 코드는 개발 소스이며 게시/버전 상승/운영 배포를 수행하지 않는다.
@@ -89,7 +89,7 @@ const passwordRef = useRef<TextInput>(null);
89
89
  | --- | --- | --- | --- |
90
90
  | `value` · `defaultValue` | 문자열 | 비제어 `""` | |
91
91
  | `onValueChange` | `(value: string) => void` | — | Web은 DOM `onChange`도 함께 받는다 |
92
- | `autofillHint` | `current`(로그인) · `new`(가입·변경) | 필수 | Web은 `current-password`/`new-password`, Native는 iOS `textContentType` `password`/`newPassword`로 번역된다 |
92
+ | `autofillHint` | `current`(로그인) · `new`(가입·변경) · `none`(비계정 비밀값) | 필수 | Web은 `current-password`/`new-password`, Native는 iOS `textContentType` `password`/`newPassword`로 번역된다 |
93
93
  | `revealLabel` · `concealLabel` | 문자열 | 필수 | 토글의 접근성 이름. 각각 "누르면 보임"·"누르면 숨김" 행동 문구 |
94
94
  | `revealed` · `defaultRevealed` | `boolean` | `false` | 가림 상태(제어·비제어). 값과 독립된 축이다 |
95
95
  | `onRevealedChange` | `(revealed: boolean) => void` | — | 토글을 누를 때 |
@@ -139,3 +139,5 @@ Native `large`는 프로필의 `typography.bodyLarge`를 읽고 그 줄 높이
139
139
 
140
140
  - Native `error`·`description`은 `string`이라 `exactOptionalPropertyTypes`에서 `undefined`를 받지 않는다(Web은 `ReactNode`라 통과).
141
141
  값이 없을 수 있으면 위 예처럼 조건부 spread로 넘긴다.
142
+
143
+ 2026-10-20 Utilverse Wi-Fi QR 채택: `autofillHint="none"`은 Web autocomplete=off, Native autoComplete=off/textContentType=none/importantForAutofill=no로 계정 자격증명 제안을 요청하지 않는다(1.36.0 추가 API). 숨김/표시 상태는 기존 계약을 유지한다.
@@ -4,13 +4,13 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.17.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-20
8
8
  - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
9
  - 스토리북: `배포/구성/선택과 필터/사진 촬영과 앨범 선택`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
13
- 사진 버튼 하나에서 **앨범에서 고르기 / 촬영하기**를 고르게 할 때 쓴다. [Sheet](sheet.md)와 Button 두 개의
13
+ 사진 버튼 하나에서 **앨범에서 고르기 / 촬영하기**를 고르게 할 때 쓴다. [Sheet](sheet.md)와 [ListRow](list-row.md) 두 개의 아이콘 목록
14
14
  조합이며 출처 선택만 한다. 권한 요청·카메라 실행·파일 선택·업로드는 `onSelect`에서 제품이 한다.
15
15
 
16
16
  ## 쓰지 않을 때
@@ -86,18 +86,22 @@ import { PhotoSourceSheet } from "@hjmds/react-native/screen-flows";
86
86
  | `onOpenChange` | `(open: boolean) => void` | 필수 | 닫기·선택 시 `false` |
87
87
  | `onSelect` | `(source: "library" \| "camera") => void` | 필수 | 고르면 시트를 닫고(`onOpenChange(false)`) 호출한다. 호출 시점은 플랫폼 차이 참고 |
88
88
  | `labels` | `{ title, library, camera, cancel }` | 필수 | `cancel`은 Sheet 닫기 버튼 이름 |
89
- | `cameraAvailable` | `boolean` | `true` | `false`면 촬영 버튼을 숨긴다. 기기 카메라 유무는 제품이 판단한다 |
90
- | `disabled` | `boolean` | `false` | 두 버튼을 막고 선택을 무시한다 |
89
+ | `cameraAvailable` | `boolean` | `true` | `false`면 촬영 행을 숨긴다. 기기 카메라 유무는 제품이 판단한다 |
90
+ | `disabled` | `boolean` | `false` | 두 선택 행을 막고 선택을 무시한다 |
91
91
 
92
92
  ## 배치
93
93
 
94
+ 2026-10-20 사용자 선택: 테두리 버튼 두 개의 표현을 아이콘 목록형으로 통일한다.
95
+ 두 출처는 동등한 행동이므로 같은 중성색 아이콘 배경을 쓰고 전체 행을 누를 수 있게 한다.
96
+ 공개 props·플랫폼 선택기 호출 시점은 유지하며 이 변경은 아직 미게시다.
97
+
94
98
  | 항목 | 값 | 근거 |
95
99
  | --- | --- | --- |
96
- | 크기 | Sheet 기본 `placement="bottom"`·`size="auto"`(내용 높이, 화면 높이 0.9 상한), Web 최대 폭 640; 선택 버튼은 `tone="secondary"` `Button` 두 개 | `sheetRecipe`, `screen-flows.tsx` |
97
- | 간격 | 시트 안 여백은 Sheet 소유(두 플랫폼 좌우 `spacing.lg` 20 · 위아래 `spacing.sm` 12 + 하단 safe area); 두 버튼 사이 `spacing.xs` 8(세로로 쌓은 버튼, `actionGroupRecipe.stackedGap`); 바깥에 padding을 더하지 않는다 | Web `.hjm-sheet__body`, Native `sheetRecipe.content`, `Stack gap="xs"` |
98
- | 순서·정렬 | Sheet 손잡이 → 앨범 → 촬영(`cameraAvailable`일 때만). `labels.title`은 보이지 않는 시트 이름(2026-10-17 사용자 결정: 시트는 보이는 제목 없이 `title`을 접근성 이름으로만 쓴다) | Web `screen-flows.tsx` 59–60행, Native 50–51행 |
100
+ | 크기 | Sheet 기본 `placement="bottom"`·`size="auto"`(내용 높이, 화면 높이 0.9 상한), Web 최대 폭 640; 선택 항목은 기본 `comfortable` ListRow 두 개. 40pt 선행 프레임 안에 24pt 앨범·카메라 아이콘, `radius.md`, 밝은 모드 배경 `#E2E8F0`·선 `#334155`, 어두운 모드 배경 `#334155`·선 `#F1F5F9` | `sheetRecipe`, `screen-flows.tsx` |
101
+ | 간격 | 시트 안 여백은 Sheet 소유(두 플랫폼 좌우 `spacing.lg` 20 · 위아래 `spacing.sm` 12 + 하단 safe area); 선택 행 사이는 `Stack gap="xxs"` (`spacing.xxs` 4); 행 내부 간격·높이는 `listRowRecipe`; 바깥에 padding을 더하지 않는다 | Web `.hjm-sheet__body`, Native `sheetRecipe.content`, `Stack gap="xxs"` |
102
+ | 순서·정렬 | Sheet 손잡이 → 앨범 → 촬영(`cameraAvailable`일 때만). `labels.title`은 보이지 않는 시트 이름(2026-10-17 사용자 결정: 시트는 보이는 제목 없이 `title`을 접근성 이름으로만 쓴다) | Web·Native `screen-flows.tsx`의 `PhotoSourceSheet` |
99
103
  | 고정·스크롤 | 화면 하단에 뜨는 오버레이(배경 막) — 선택 뒤 picker 호출 시점은 플랫폼 차이 참고 | `Sheet` |
100
- | 좁은 폭·큰 글자 | 버튼 문구는 줄바꿈되고 Sheet 높이가 늘어난다(0.9 상한을 넘으면 본문 스크롤); 카메라가 없으면 버튼 하나만 남는다 | `sheetRecipe.content.maxHeightRatio` |
104
+ | 좁은 폭·큰 글자 | 행 문구는 줄바꿈되고 Sheet 높이가 늘어난다(0.9 상한을 넘으면 본문 스크롤); 카메라가 없으면 앨범 행 하나만 남는다 | `sheetRecipe.content.maxHeightRatio` |
101
105
 
102
106
  ## 꼭 지킬 것
103
107
 
@@ -117,3 +121,5 @@ import { PhotoSourceSheet } from "@hjmds/react-native/screen-flows";
117
121
  - Web에서 `onSelect` 안의 file input 클릭을 `setTimeout`·애니메이션 뒤로 미루면 브라우저가 거부할 수 있다. 같은 콜스택에서 클릭한다.
118
122
  - Native에서 닫히는 Modal 위에 카메라·선택기를 띄우면 iOS가 표시하지 못한다. 그래서 dismiss 완료 뒤에 호출되며, 제품이 타이머로 다시 앞당기지 않는다.
119
123
  - `capture`를 지원하지 않는 브라우저·기기는 OS 파일 선택 화면으로 대체될 수 있어 실제 촬영을 보장하지 않는다. 앨범 input에는 `capture`를 붙이지 않는다.
124
+
125
+ 2026-10-20 사용자 결정: 사진 출처 아이콘은 앱 브랜드 팔레트와 관계없이 위 중성색으로 통일한다. 주요 버튼의 브랜드 색은 유지한다.
@@ -67,6 +67,7 @@ import { TextField } from "@hjmds/react-native/inputs";
67
67
  | `removeLabel` | `(id) => string` | 필수 | 행마다 다른 삭제 이름(대상 이름 포함) |
68
68
  | `minFields` / `maxFields` | `number` | `0` / `Infinity` | 이하면 삭제, 이상이면 추가 비활성 |
69
69
  | `onAdd` | `() => string \| undefined` | 필수 | 새 행 id를 돌려주면 그 행이 descriptor에 나타난 뒤 포커스 |
70
+ | `isRemoveDisabled` | `(id) => boolean` | `false` | 참조 중인 정산 참여자처럼 입력은 허용하고 삭제만 막을 때 사용. checkout 개발 API·미게시(2026-10-20 Utilverse 채택) |
70
71
  | `onRemove` | `(id) => void` | 필수 | 제품 상태에서 행 제거 |
71
72
  | `layout` · Web `className`·`layoutStyle` | FieldGroup 값 | FieldGroup 기본 | FieldGroup으로 전달 |
72
73
 
@@ -132,7 +132,7 @@ import { SegmentedControl } from "@hjmds/react-native/inputs";
132
132
 
133
133
  - `presentation="pills"`: 카테고리·작성자·기간처럼 같은 목록의 범위를 좁힐 때 쓴다.
134
134
  - 표면 높이·좌우 여백·글자는 같은 size의 Chip과 맞춘다. 터치 영역은 최소 `control.minTouchTarget` 44이며 안쪽 표면은 위아래 `spacing.xxs` 4만큼 들어간다. 2026-10-20 Utilverse 혼합 필터 행에서 작은 Chip36과 pill28의 불일치가 확인돼 공통 크기로 정렬했다(미게시). 누름 영역은 줄이지 않는다.
135
- - 좌우 여백은 Chip과 같이 small12/medium16, 항목 사이 `spacing.xs` 8, 모서리 `radius.full`. 비선택은 `surfaceAlt`, 선택은 `content.body`/`canvas` 반전이다. 색은 제품 provider에서 따라온다.
135
+ - 좌우 여백은 Chip과 같이 small12/medium16, 항목 사이 `spacing.xs` 8, 모서리 `radius.full`. 비선택은 `surface.raised`(`canvas`) 바탕과 `border.control` 1pt 테두리, 선택은 `content.body`/`canvas` 반전과 같은 짙은 테두리다. 2026-10-20 Utilverse의 파스텔 배경에서 비선택 버튼 경계가 묻힌 사용자 지적에 따라 inset 표면을 공통으로 보완했다(미게시). 색은 제품 provider에서 따라온다.
136
136
  - 항목은 내용 폭이며 좁아지면 줄바꿈한다. 큰 글자에서도 세로로 쌓지 않는다(`pills.largeTextLayout` `wrap`): 블록 안에서는 줄바꿈하고, 가로 스크롤 줄 안에서는 한 줄로 남아 스크롤된다. 라벨 높이만큼 항목도 늘어난다. 선택 상태(radio)와 포커스 이동(Web 화살표, Native 접근성 순서)은 배치와 관계없이 같다.
137
137
  - 그룹 이름은 `label`, 하나의 선택은 `value`/`onValueChange`. 복수 조건은 ToggleGroup, 화면 이동은 Tabs/Navigation을 쓴다.
138
138
  - 예제의 전체 선택값도 실제 항목으로 넣는다. 선택된 항목을 다시 눌러도 선택을 해제하지 않는다.
@@ -60,7 +60,7 @@
60
60
  | 시트 머리 | Sheet 손잡이, `title`은 보이지 않는 접근성 이름 | 시트 맨 위 고정 | 보이는 제목 줄 없음(2026-10-17 사용자 결정: 시트는 보이는 제목 없이 `title`을 접근성 이름으로만 쓴다) |
61
61
  | 단계 표시 | `Steps` | 본문 맨 위 | 항상 가로 한 줄, 아래 `spacing.xl` 24 |
62
62
  | 단계 본문 | `ContentTransition` > `Stack gap="md"` | Steps 아래, 본문 스크롤 | 제목·설명 사이 `spacing.md` 16, `slide` 가로 16 이동 |
63
- | 행동 | Sheet `footer` + Button ×2 | 시트 아래 고정(스크롤 밖) | Web 오른쪽 정렬 가로 [이전 ghost][계속 primary], 사이 `spacing.sm` 12. Native 세로 열 꽉 찬 폭 [계속]→[이전], 사이 `spacing.sm` 12, 위 `spacing.sm` 12. 높이 44 |
63
+ | 행동 | Sheet `footer` + Button ×2 | 시트 아래 고정(스크롤 밖) | Web 오른쪽 정렬 가로 [이전 ghost][계속 primary], 사이 `spacing.xs` 8. Native 세로 열 꽉 찬 폭 [계속]→[이전], 사이 `spacing.xs` 8, 위 `spacing.sm` 12. 높이 44 |
64
64
  | 하단 안전 영역 | `Sheet` | 시트 아래 | Native `spacing.sm` 12 + inset, Web footer 아래 `spacing.lg` 20 + `env(safe-area-inset-bottom)` |
65
65
 
66
66
  - 마지막 단계의 "설정 마치기"가 저장이므로 행동은 Sheet `footer`에 둔다([Sheet](../components/sheet.md) 꼭 지킬 것). 본문만 스크롤되고 행동은 큰 글자에서도 보인다.
@@ -48,7 +48,7 @@ Magic UI Animated List의 소개 페이지 순차 노출에서 새 항목의 등
48
48
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
49
49
  | --- | --- | --- | --- |
50
50
  | 바깥 틀 | Web: `Container size="reading"` → Stack, Native: KeyboardAvoiding → ScrollView → Stack | 위에서 아래. Native canvas는 자체 스크롤이 없으므로 명시적 host 사용 | gap lg=20px, Native keyboardShouldPersistTaps="handled"·하단 spacing.md |
51
- | 변경 행동 | inline Stack 하나 · Button | 목록 위, 좁으면 감김 | `gap="sm"` 12. 주 행동은 "기록 추가" primary 하나, 나머지는 secondary |
51
+ | 변경 행동 | inline Stack 하나 · Button | 목록 위, 좁으면 감김 | `gap="xs"` 8. 주 행동은 "기록 추가" primary 하나, 나머지는 secondary |
52
52
  | 예제 도구 | 미리보기 도구 캡션 + Button `size="small"` `tone="ghost"` | 변경 행동 아래 | 캡션과 버튼 사이 `spacing.xs` 8. 제품에는 복사하지 않는다 |
53
53
  | 데이터 | List | 변경 결과 아래 | separator full, 공통 ListRow/TextField 크기. 메모 TextField는 `layoutStyle` 좌우 margin `listRowRecipe.density.comfortable.paddingHorizontal`(8)로 행 글자 시작선에 맞춘다 |
54
54
  | 빈 상태 | EmptyState | 목록 자리 가운데 | EmptyState 기본 |
@@ -19,7 +19,7 @@
19
19
  | 컴포넌트 | 역할 | 지침 |
20
20
  | --- | --- | --- |
21
21
  | PhotoSourceSheet | 명시적으로 선택 후 플랫폼 picker 실행 | [공개 계약](../components/photo-source-sheet.md) |
22
- | Button | 명시 행동·재시도 | [Button](../components/button.md) |
22
+ | ListRow | 공통 중성색 아이콘과 출처 선택 행동 | [ListRow](../components/list-row.md) |
23
23
 
24
24
  ## 배치
25
25
 
@@ -32,7 +32,7 @@
32
32
  | --- | --- | --- | --- |
33
33
  | 바깥 틀 | PhotoSourceSheet 또는 포함 Surface | 부모 화면의 해당 슬롯 | [배치](../components/photo-source-sheet.md#배치)에 따른다 |
34
34
  | 내용 | PhotoSourceSheet 내부 슬롯 | 앨범 → 촬영(보이는 제목 없음(2026-10-17 사용자 결정: 시트는 보이는 제목 없이 `title`을 접근성 이름으로만 쓴다)) | spacing 토큰과 포함 컴포넌트 recipe |
35
- | 행동 | Button 또는 공개 콜백 | 내용과 가까운 명시 진입점 | 주 행동 하나, 보조 행동과 구분 |
35
+ | 행동 | 아이콘 ListRow의 공개 콜백 | 앨범·촬영 행 전체 | 동등한 두 출처, 같은 중성색 표현 |
36
36
 
37
37
  ## 흐름과 상태
38
38
 
@@ -6,7 +6,7 @@
6
6
  - 적용: 미게시(1.34.0 이후)
7
7
  - 검토일: 2026-10-19
8
8
  - 근거: `src/shortcut-rail.ts`, `chipRecipe`. Spint 최근 도시에서 열기 버튼 안에 삭제 버튼을 넣어 두 행동과 접근성 경계가 겹친 2026-10-18 전수 이관.
9
- - 스토리북: `실험/구성/콘텐츠/바로가기 목록`
9
+ - 스토리북: `배포/구성/콘텐츠/바로가기 목록`
10
10
 
11
11
  ## 언제 쓰나
12
12
 
@@ -38,7 +38,7 @@
38
38
  | 영역 | 컴포넌트 | 위치 | 크기·간격 |
39
39
  | --- | --- | --- | --- |
40
40
  | 바깥 틀 | Stack/Card | 기존 본문·패널 안 | sm 또는 md |
41
- | 분할 행동 | Button + Menu(IconButton chevronDown 트리거, label=`menuLabel`) | 같은 inline 행, 메뉴는 실행 버튼 끝 | 인라인 간격 `spacing.sm` 12(button.md) |
41
+ | 분할 행동 | Button + Menu(IconButton chevronDown 트리거, label=`menuLabel`) | 같은 inline 행, 메뉴는 실행 버튼 끝 | 인라인 간격 `spacing.xs` 8(button.md) |
42
42
  | 메뉴 트리거 | 분할 행: IconButton(secondary). 계정 전환: Button(secondary) | 분할 행 끝·계정 이름 아래 | 내용 폭. Web·Native 모두 전체 폭으로 늘이지 않음 |
43
43
  | 비밀번호 | PasswordField/Progress/Text | 위→아래 | sm |
44
44
  | 요금 선택 | RadioGroup | 카드 상단 | card presentation |
@@ -98,3 +98,5 @@ import { AccountSwitcher } from "@hjmds/react-native/task-controls";
98
98
  ## 근거
99
99
 
100
100
  - 2026-10-17 구성 리뷰: 분할 실행의 방식 메뉴가 두 번째 텍스트 버튼("실행 방식 선택")으로 보여 별개 행동처럼 읽혔고 인라인 간격이 8이었다. 그래서 메뉴를 chevron IconButton 도구로 바꾸고 간격을 12로 맞췄다.
101
+
102
+ - 2026-10-20 사용자 요청: 버튼 묶음의 가로 기본 간격도 8로 통일했다. 위 2026-10-17의 12 간격 결정은 이 기준으로 대체한다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hjmds/design-contracts",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "Renderer-neutral design contracts, tokens, recipes, and behaviors shared by HJM products.",
5
5
  "license": "MIT",
6
6
  "repository": {