@hjmds/design-contracts 1.13.0 → 1.14.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 (126) hide show
  1. package/dist/agreement.d.ts +7 -1
  2. package/dist/agreement.d.ts.map +1 -1
  3. package/dist/agreement.js +14 -3
  4. package/dist/agreement.js.map +1 -1
  5. package/dist/behaviors.d.ts +1 -1
  6. package/dist/catalog.d.ts +5 -0
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/component-recipes.d.ts +14 -0
  9. package/dist/component-recipes.d.ts.map +1 -1
  10. package/dist/component-recipes.js +16 -1
  11. package/dist/component-recipes.js.map +1 -1
  12. package/dist/content-transition.d.ts +17 -0
  13. package/dist/content-transition.d.ts.map +1 -1
  14. package/dist/content-transition.js +21 -0
  15. package/dist/content-transition.js.map +1 -1
  16. package/dist/date-entry.d.ts +71 -0
  17. package/dist/date-entry.d.ts.map +1 -0
  18. package/dist/date-entry.js +78 -0
  19. package/dist/date-entry.js.map +1 -0
  20. package/dist/document-resource.d.ts +104 -0
  21. package/dist/document-resource.d.ts.map +1 -0
  22. package/dist/document-resource.js +77 -0
  23. package/dist/document-resource.js.map +1 -0
  24. package/dist/effect-surface.d.ts +6 -1
  25. package/dist/effect-surface.d.ts.map +1 -1
  26. package/dist/effect-surface.js +5 -2
  27. package/dist/effect-surface.js.map +1 -1
  28. package/dist/field-group.d.ts +47 -0
  29. package/dist/field-group.d.ts.map +1 -0
  30. package/dist/field-group.js +92 -0
  31. package/dist/field-group.js.map +1 -0
  32. package/dist/image.d.ts +17 -0
  33. package/dist/image.d.ts.map +1 -1
  34. package/dist/image.js +19 -0
  35. package/dist/image.js.map +1 -1
  36. package/dist/internal/effect-noise.d.ts +2 -0
  37. package/dist/internal/effect-noise.d.ts.map +1 -0
  38. package/dist/internal/effect-noise.js +4 -0
  39. package/dist/internal/effect-noise.js.map +1 -0
  40. package/dist/progressive-blur.d.ts +32 -0
  41. package/dist/progressive-blur.d.ts.map +1 -0
  42. package/dist/progressive-blur.js +28 -0
  43. package/dist/progressive-blur.js.map +1 -0
  44. package/dist/reference-controls.d.ts +32 -0
  45. package/dist/reference-controls.d.ts.map +1 -0
  46. package/dist/reference-controls.js +28 -0
  47. package/dist/reference-controls.js.map +1 -0
  48. package/dist/scroll-progress.d.ts +7 -1
  49. package/dist/scroll-progress.d.ts.map +1 -1
  50. package/dist/scroll-progress.js +21 -2
  51. package/dist/scroll-progress.js.map +1 -1
  52. package/dist/text-annotation.d.ts +43 -0
  53. package/dist/text-annotation.d.ts.map +1 -0
  54. package/dist/text-annotation.js +137 -0
  55. package/dist/text-annotation.js.map +1 -0
  56. package/dist/version.d.ts +1 -1
  57. package/dist/version.js +1 -1
  58. package/dist/version.js.map +1 -1
  59. package/docs/agreement.md +14 -0
  60. package/docs/dialog.md +16 -1
  61. package/docs/effect-surface.md +19 -3
  62. package/docs/generated/component-maturity.md +1 -1
  63. package/docs/generated/renderer-evidence.json +3 -3
  64. package/docs/generated/renderer-evidence.md +1 -1
  65. package/docs/generated/showcase-manifest.json +1 -1
  66. package/docs/image.md +17 -0
  67. package/docs/optional-adapters.md +25 -0
  68. package/docs/popover.md +15 -0
  69. package/docs/rating.md +6 -1
  70. package/docs/reference-controls.md +40 -0
  71. package/docs/screen-patterns.md +2 -0
  72. package/docs/sheet.md +9 -0
  73. package/docs/task-list.md +15 -1
  74. package/docs/text-annotation.md +85 -0
  75. package/docs/toggle-group.md +6 -0
  76. package/docs/usage/README.md +18 -1
  77. package/docs/usage/components/agreement.md +13 -3
  78. package/docs/usage/components/alert-dialog.md +7 -1
  79. package/docs/usage/components/avatar.md +33 -1
  80. package/docs/usage/components/card.md +7 -1
  81. package/docs/usage/components/carousel.md +5 -1
  82. package/docs/usage/components/chat-message.md +11 -1
  83. package/docs/usage/components/chip.md +10 -2
  84. package/docs/usage/components/content-transition.md +28 -4
  85. package/docs/usage/components/dialog.md +18 -1
  86. package/docs/usage/components/effect-surface.md +4 -2
  87. package/docs/usage/components/empty-state.md +10 -2
  88. package/docs/usage/components/field.md +23 -0
  89. package/docs/usage/components/form.md +6 -1
  90. package/docs/usage/components/image-comparison.md +82 -0
  91. package/docs/usage/components/image.md +81 -2
  92. package/docs/usage/components/keyboard-avoiding.md +6 -1
  93. package/docs/usage/components/link.md +3 -1
  94. package/docs/usage/components/list-row.md +3 -1
  95. package/docs/usage/components/list.md +6 -1
  96. package/docs/usage/components/message-composer.md +5 -0
  97. package/docs/usage/components/popover.md +7 -1
  98. package/docs/usage/components/progress.md +28 -0
  99. package/docs/usage/components/progressive-blur.md +113 -0
  100. package/docs/usage/components/rating.md +74 -0
  101. package/docs/usage/components/search-field.md +10 -0
  102. package/docs/usage/components/search-screen.md +16 -5
  103. package/docs/usage/components/segmented-control.md +29 -4
  104. package/docs/usage/components/sheet.md +7 -1
  105. package/docs/usage/components/statistic.md +11 -0
  106. package/docs/usage/components/tags-input.md +5 -0
  107. package/docs/usage/components/toast.md +5 -0
  108. package/docs/usage/components/upload-item.md +3 -1
  109. package/docs/usage/compositions/action-feedback.md +77 -0
  110. package/docs/usage/compositions/adaptive-content.md +81 -0
  111. package/docs/usage/compositions/context-toolbar.md +85 -0
  112. package/docs/usage/compositions/date-entry.md +108 -0
  113. package/docs/usage/compositions/document-resource.md +124 -0
  114. package/docs/usage/compositions/field-group.md +104 -0
  115. package/docs/usage/compositions/illustrated-outcome.md +91 -0
  116. package/docs/usage/compositions/live-list.md +104 -0
  117. package/docs/usage/compositions/optional-adapters.md +1 -1
  118. package/docs/usage/compositions/origin-dialog.md +108 -0
  119. package/docs/usage/compositions/selection-motion.md +73 -0
  120. package/docs/usage/compositions/texture-comparison.md +86 -0
  121. package/docs/usage/compositions/upload-recovery.md +80 -0
  122. package/docs/usage/compositions/video-dialog.md +100 -0
  123. package/docs/usage/screens/common-search.md +7 -2
  124. package/docs/usage/screens/flow-onboarding.md +5 -3
  125. package/docs/usage/screens/product-bento.md +117 -0
  126. package/package.json +37 -1
@@ -0,0 +1,82 @@
1
+ # ImageComparison
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: [계약](../../reference-controls.md)
9
+ - 스토리북: `배포/컴포넌트/데이터 표시/이미지 전후 비교`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 같은 좌표와 비율의 두 이미지를 겹쳐 변화량을 비교할 때 쓴다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 서로 다른 장면의 목록 | [Image](image.md)와 [Grid](grid.md) |
22
+
23
+ ## 공개 이름과 import
24
+
25
+ | 이름 | 역할 | Web | Native |
26
+ | --- | --- | --- | --- |
27
+ | `ImageComparison` | 독립 supplemental | `@hjmds/react/image-comparison` | `@hjmds/react-native/image-comparison` |
28
+
29
+ ## 최소 사용 예
30
+
31
+ ```tsx
32
+ // Web
33
+ import { ImageComparison } from "@hjmds/react/image-comparison";
34
+ <ImageComparison label={label} before={before} after={after} value={percentage} onValueChange={setPercentage} getValueText={formatPercentage} />
35
+ ```
36
+
37
+ ```tsx
38
+ // Native
39
+ import { ImageComparison } from "@hjmds/react-native/image-comparison";
40
+ <ImageComparison label={label} before={before} after={after} value={percentage} onValueChange={setPercentage} getValueText={formatPercentage} decrementLabel={decrementLabel} incrementLabel={incrementLabel} />
41
+ ```
42
+
43
+ Native는 import를 `@hjmds/react-native/image-comparison`로 바꾼다.
44
+
45
+ ## 축과 기본값
46
+
47
+ | prop | 값 | 기본값 | 설명 |
48
+ | --- | --- | --- | --- |
49
+ | `value` | number | 필수 | value는 0~100. before/after는 src·width·height·label을 가지며 두 비율이 같아야 한다. |
50
+ | Web `layoutStyle` | 배치 전용 style | 없음 | 바깥 틀의 폭·margin·flex 배치 |
51
+ | `disabled` | boolean | false | 변경을 막고 현재 값을 유지 |
52
+
53
+ ## 배치
54
+
55
+ | 항목 | 값 | 근거 |
56
+ | --- | --- | --- |
57
+ | 크기 | 바깥 폭을 채우고 원본 aspect ratio로 높이를 정한다. 두 이미지는 같은 전체 크기이며 before만 잘린다. | renderer `image-comparison.tsx` |
58
+ | 간격 | 위 라벨 사이 space.sm=12px. 실제 조작 Slider는 이미지 바로 아래에 둔다. | foundations spacing |
59
+ | 순서·정렬 | 전후 라벨→이미지→비율 조절 | renderer 순서 |
60
+ | 고정·스크롤 | 바깥 화면이 스크롤을 소유한다 | 별도 스크롤 없음 |
61
+ | 좁은 폭·큰 글자 | 폭을 강제 고정하지 않고 본문/행의 줄바꿈을 허용한다 | 위 배치 |
62
+
63
+ ## 꼭 지킬 것
64
+
65
+ - 이미지 경계는 별도 조작 엔진이 아니다. before 비율은 물리적 왼쪽 기준이며 RTL에서도 이미지 의미를 뒤집지 않는다. Native에는 decrementLabel/incrementLabel도 제품 언어로 전달한다.
66
+ - 브랜드는 HJM provider의 semantic palette로 연결한다. 점수 집계·이미지 변환·저장은 제품 소유다.
67
+
68
+ ## 플랫폼 차이
69
+
70
+ | 항목 | Web | Native |
71
+ | --- | --- | --- |
72
+ | 조작 | Web은 Slider의 range/키보드, Native는 Slider의 adjustable 및 증감 버튼을 그대로 사용한다. | 같은 의미를 플랫폼 host로 번역 |
73
+
74
+ ### 이미지 host와 fixture
75
+
76
+ 2026-10-07 iOS Expo Go에서 SVG data URI가 오류 fallback을 표시했지만 슬라이더는 정상
77
+ 동작했다. 예제는 `scripts/generate-comparison-fixtures.py`로 같은 좌표의 PNG를 생성해
78
+ 양쪽 기본 이미지 host에서 표시한다. 값 변경·AX 이미지 이름만으로 이미지 로딩 성공을
79
+ 판정하지 않는다. 제품은 지원되는 실제 자산 형식과 로딩/실패 화면을 확인한다.
80
+
81
+ RTL에서도 보정 전 라벨은 왼쪽, 보정 후는 오른쪽이다. 슬라이더와 문구 쓰기 방향은
82
+ 제품 언어를 따르며 이미지 경계의 물리적 의미와 구분한다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-07
8
8
  - 근거: [Image](../../image.md), [GridReveal](../../grid-reveal.md), [ImageViewer](../../optional-adapters.md#behavior-boundaries), recipe `imageRecipe`(`src/image.ts`)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/이미지` · `배포/컴포넌트/시각 효과/격자 등장 효과`
10
10
 
@@ -82,7 +82,10 @@ import { ImageViewer } from "@hjmds/react-native/image-viewer";
82
82
  | ImageViewer `items` | `id`·`uri`·`label` | — | 라벨 6종, `safeAreaInsets`가 필수 |
83
83
  | ImageViewer `initialIndex` | 0 이상 정수 | `0` | — |
84
84
  | ImageViewer `onClose` · `onIndexChange` | `() => void` · `(index: number) => void` | `onClose` 필수 | — |
85
- | ImageViewer `safeAreaInsets` | `{ top: number; bottom: number }` | 필수 | — |
85
+ | ImageViewer `safeAreaInsets` | `{ top: number; bottom: number; left?: number; right?: number }` | 필수, 좌우 0 | 버튼·caption·상태 안내를 물리적 좌우 안전 영역 안에 배치. 사진은 전체 갤러리 폭 사용 |
86
+ | ImageViewer `supportedOrientations` | RN Modal의 orientation 배열 | RN 기본값 | 제품 manifest·기기 회전 잠금 범위 안에서 허용. Modal은 fullScreen |
87
+ | ImageViewer `renderImage` | `(props: ImageViewerImageRenderProps) => ReactNode` | RN Image | Native 전용, 미게시. `item`, 측정된 `width`·`height`, `onReady`·`onError`를 전달. 제품 이미지 host의 캐시·표시 이벤트를 연결 |
88
+ | ImageViewer `onImageStatusChange` | `({ item, status }) => void` | 없음 | `loading`·`ready`·`error`. 마운트된 각 이미지 기준이며 비선택 페이지도 포함할 수 있음 |
86
89
 
87
90
  - 실패하면 중립 배경 위에 오류 기호를 그리고, 정보 이미지의 이름은 그대로 유지한다. `fallback`은 시각만 바꾼다.
88
91
 
@@ -105,6 +108,82 @@ import { ImageViewer } from "@hjmds/react-native/image-viewer";
105
108
  - ImageViewer는 불러오는 중(`loadingLabel`)과 실패(`errorLabel`)를 모두 알린다. 실패는 Android assertive live region, iOS는 `announceForAccessibility`다(미게시(1.12.1 이후). 1.12.1은 실패를 알리지 않았다).
106
109
  - 원격 이미지 권한·URL 수명·캐시는 제품 소유다. HJM은 메타데이터를 가져오지 않는다.
107
110
 
111
+ ### Native ImageViewer의 제품 이미지 호스트
112
+
113
+ Utilverse의 사진 결과 확인은 Expo `onDisplay`에 의존한다(ADR-0020). RN Image `onLoad`를
114
+ 그대로 표시 완료로 간주하지 않도록 기존 optional ImageViewer에 host 슬롯을 추가했다.
115
+ HJM에 Expo 의존성을 넣거나 별도 갤러리를 복제하지 않는다. 아래는 Expo를 이미 쓰는 제품의 연결 예다.
116
+
117
+ ```tsx
118
+ import { Image as ExpoImage } from "expo-image";
119
+ import { ImageViewer } from "@hjmds/react-native/image-viewer";
120
+
121
+ <ImageViewer {...viewerProps}
122
+ renderImage={({ item, width, height, onReady, onError }) => (
123
+ <ExpoImage source={{ uri: item.uri }} cachePolicy="none" contentFit="contain"
124
+ accessibilityLabel={item.label} style={{ width, height }}
125
+ onDisplay={onReady} onError={onError} />
126
+ )}
127
+ onImageStatusChange={({ item, status }) => recordImageStatus(item.id, status)}
128
+ />
129
+ ```
130
+
131
+ `viewerProps`는 위의 open/items/라벨/inset/닫기 props다. host는 이미지의 접근성 이름과
132
+ 크기를 연결하고 상태 문구·재시도 버튼을 다시 만들지 않는다. 재시도는 host를 새로 마운트한다.
133
+ 이전 시도의 이벤트와 닫힌 세션의 이벤트는 무시하며 오류는 재시도 전까지 유지한다.
134
+ `ready`는 연결한 host 이벤트의 의미일 뿐이다. 기본 경로는 계속 RN onLoad이므로 실제 표시나
135
+ 사용자의 검토 완료를 뜻하지 않는다. 상태 통지는 렌더링된 페이지마다 발생하므로 현재 선택·
136
+ 열림·결과 URI·보기 모드·사용자 확인 조건은 제품이 결합해야 한다. 닫을 때 별도의 상태 이벤트를
137
+ 보내지 않는다. 제품은 닫기/교체에서 검토를 무효화한다. host 변경만으로 세션이 새로 열리지 않는다.
138
+ 회전을 허용하는 제품은 `supportedOrientations={["portrait", "landscape"]}`와 갱신되는
139
+ safeAreaInsets 네 방향을 제공한다. 앱 manifest가 portrait 고정이면 이 prop만으로 회전이 보장되지 않는다.
140
+ 화면 크기가 바뀌면 버튼을 제외한 남은 갤러리 영역을 다시 측정해 host에 전달한다.
141
+ 아래 inspection 확장은 미게시이며, Expo 표시 확인·결과 승인 회귀와 Utilverse 채택은 별도 검증한다.
142
+
143
+ ### 결과 검사 크기 계산
144
+
145
+ `@hjmds/design-contracts/components/image`의 `resolveImageInspectionGeometry`는 원본과
146
+ 실측 viewport 크기, `fit | double | pixels`를 받아 scale·width·height·panBounds를 반환한다.
147
+ fit은 전체가 들어가는 크기, double은 fit의 2배, pixels는 원본 수치와 같은 layout 단위다.
148
+ 기기의 물리 pixel 배율을 추정하지 않는다. 0 크기는 측정 전 상태이므로 호출을 미룬다.
149
+
150
+ ```ts
151
+ import { resolveImageInspectionGeometry } from "@hjmds/design-contracts/components/image";
152
+
153
+ const geometry = resolveImageInspectionGeometry(
154
+ { width: 600, height: 600 }, { width: 402, height: 454 }, "double",
155
+ ); // width/height 804, panBounds x=201 y=175 (중앙에서 양방향)
156
+ ```
157
+
158
+ ### Native 결과 검사 보기 (미게시)
159
+
160
+ `ImageViewer`의 선택적 `inspection`은 위 계산을 사용한다. 모든 `items`에 실제 출력의
161
+ 양수 `width`·`height`를 제공한다. 일반 Gallery의 pinch/paging과 별개로 고정 배율의 결과를
162
+ 검사하는 용도다. `inspection`을 생략하면 기존 Gallery 동작을 유지한다.
163
+
164
+ ```tsx
165
+ <ImageViewer {...viewerProps}
166
+ items={[{ id: "result", uri: outputUri, label: resultLabel, width: outputWidth, height: outputHeight }]}
167
+ inspection={{
168
+ mode, onModeChange: next => { invalidateReview(); setMode(next); },
169
+ labels: { mode: "결과 보기", fit: "맞춤", double: "2배", pixels: "출력 크기",
170
+ left: "왼쪽", right: "오른쪽", up: "위", down: "아래", center: "중앙" },
171
+ getPositionText: ({ x, y }) => `위치 ${Math.round(x)}, ${Math.round(y)}`,
172
+ }} />
173
+ ```
174
+
175
+ 문구는 예시이며 제품 i18n에서 공급한다. 닫기 아래 SegmentedControl, 남은 이미지 viewport,
176
+ 방향/중앙 버튼과 위치 안내, 이미지 설명 순으로 배치한다. 큰 글자에서는 선택·방향 버튼이
177
+ 줄바꿈되며 그 아래 실제 남은 viewport로 배율을 다시 계산한다. `renderImage`의 width/height는
178
+ 이 모드에서 **이미지의 표시 크기**이며 viewport보다 클 수 있다.
179
+
180
+ - fit은 전체 맞춤, double은 fit의 2배, pixels는 출력 수치와 같은 layout 단위다. 물리 기기 pixel의 1:1 decode를 보장하지 않는다.
181
+ - 드래그 또는 방향 버튼으로 양축 끝까지 이동한다. 버튼은 viewport의 80%씩 이동해 20% 문맥을 유지하며 물리 방향은 RTL에서도 뒤집지 않는다. 넘침이 없는 축은 비활성이다.
182
+ - 확대/축소 pinch·double tap과 swipe 페이지 전환은 사용하지 않는다. 여러 이미지는 이전/다음 버튼으로 전환하고 한 장이면 두 버튼을 숨긴다.
183
+ - 모드·viewport·이미지·재시도가 바뀌면 중앙에서 새 host를 열고 loading부터 시작한다. 폐기된 host/제스처 콜백은 새 상태를 바꾸거나 위치를 알리지 않는다.
184
+ - 위치는 이미지 중앙에서 본 viewport의 x/y 오프셋과 maxX/maxY 한계다. `getPositionText`는 비어 있지 않은 지역화 문자열을 반환한다. 화면에서는 한 줄로 제한해 위치 문구 변화가 viewport를 바꾸지 않게 하고 접근성 이름은 전체를 제공한다.
185
+ - 위치 알림은 Android live region, iOS 완료된 이동의 announce API를 사용한다. 실제 스크린리더 검증은 남아 있다. 제품의 표시 완료/수동 확인/내보내기 승인 계약은 기존 host 절대로 유지한다.
186
+
108
187
  ## 플랫폼 차이
109
188
 
110
189
  | 항목 | Web | Native |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-07
8
8
  - 근거: [Native platform](../../native-platform.md#키보드-회피), 판정 `resolveKeyboardInset`(`src/native-platform.ts`)
9
9
  - 스토리북: 없음
10
10
 
@@ -14,6 +14,11 @@
14
14
  키보드가 뜨면 측정한 높이만큼 아래 여백을 늘리고, 내려가면 safe area 여백만 남긴다.
15
15
  `react-native-keyboard-controller`를 설치하지 않은 앱의 기본 선택이다(Native 전용, 별도 보조 기능).
16
16
 
17
+ 이 컴포넌트는 keyboard event의 높이를 사용하며 wrapper의 window 위치와 실제 겹침은
18
+ 측정하지 않는다. 2026-10-07 Utilverse의 측정 기반 host 대조에서 이 차이를 확인했다.
19
+ 이미 adjustResize나 제품 host가 겹침을 처리하면 중첩하지 말고, safe area·하단 독·회전에서
20
+ 같은 여백이 두 번 적용되지 않는지 확인한 뒤 교체한다.
21
+
17
22
  ## 쓰지 않을 때
18
23
 
19
24
  | 상황 | 대신 쓸 것 |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-07
8
8
  - 근거: [Link](../../link.md), recipe `linkRecipe`(`src/component-recipes.ts`), 목적지 검증 `src/link.ts`
9
9
  - 스토리북: `배포/컴포넌트/동작/링크`
10
10
 
@@ -95,6 +95,8 @@ const renderGlyph = createLucideGlyph({ chevronEnd: ChevronRight }); // 제품
95
95
 
96
96
  ## 꼭 지킬 것
97
97
 
98
+ - Web 파일 다운로드는 실제 `href`와 anchor `download` 속성을 사용한다. 파일 설명·형식·크기는 제품이 정확한 현지화 문구로 제공한다. Native Link는 탐색 계약이므로 파일 저장·공유 작업의 완료나 실패를 자동 처리하지 않는다. 2026-10-07 Nucleus/TFWM 파일 사례 대조에서 탐색·다운로드·업로드를 같은 상태로 분류하지 않기 위해 이 경계를 명시했다.
99
+
98
100
  - 라벨은 i18n 키로 넣는다. 접근성 이름을 따로 줄 때도 보이는 문구를 포함한다.
99
101
  - navigation을 `onClick`/`onPress` callback으로 대신하지 않는다. Web은 실제 `href`를, Native는 `destination`을 둔다.
100
102
  - 앞뒤 아이콘은 장식이다. 이름은 링크 문구가 소유한다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-07
8
8
  - 근거: [ListRow](../../list-row.md), recipe `listRowRecipe`(`src/component-recipes.ts`)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/목록 행`
10
10
 
@@ -102,6 +102,8 @@ import { Avatar, ListRow } from "@hjmds/react-native/data-display";
102
102
 
103
103
  ## 꼭 지킬 것
104
104
 
105
+ - Web `href`는 탐색 링크이며 `download` prop은 없다. 다운로드 속성이 필요한 파일은 [Link](link.md)를 사용한다. 큰 미리보기와 다운로드·메뉴 등 독립 행동은 Card로 구성하며 클릭 가능한 행 안에 링크를 중첩하지 않는다. 2026-10-07 파일 사례 대조에서 파일 행의 외형만으로 다운로드 지원을 추론한 대응표를 바로잡았다.
106
+
105
107
  - 제목·설명은 i18n 키로 넣는다. leading의 사진·아이콘은 장식으로 두고 의미는 제목이 말한다.
106
108
  - 행 안에 다른 버튼을 넣지 않는다. Native는 별도 target을 `trailingAction`에 둔다(행 명령 옆에 따로 그린다).
107
109
  - 배치는 `layoutStyle`로 한다. 높이·여백·배경을 덮지 않는다. 밀도는 `density`로 바꾼다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-07
8
8
  - 근거: recipe `listRecipe`(`src/component-recipes.ts`), TaskList 계약: [Task list](../../task-list.md)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/목록` · `배포/컴포넌트/입력/할 일 목록`
10
10
 
@@ -77,6 +77,7 @@ import { Text } from "@hjmds/react-native/primitives";
77
77
  | TaskList `items` | `readonly TaskItem[]` — `{ id, label, completed, description?, disabled? }` | — | `id`·`label`이 비거나 `id`가 중복되거나 `completed`가 boolean이 아니면 `TypeError` |
78
78
  | TaskList `onCompletedChange` | `(id: string, completed: boolean) => void` | — | 필수. 체크 하나마다 한 번. 목록 순서·저장은 제품이 정한다 |
79
79
  | TaskList `disabled` | `true` · `false` | `false` | 전체 체크박스를 끈다 |
80
+ | TaskList `renderItemAction` | `({ item, disabled }) => ReactNode` | — | 체크 아래 별도 행동. disabled는 전체/항목 잠금의 합이며 제품 버튼에 전달한다. 간격 spacing.sm 12, 체크 안에 중첩하지 않음 |
80
81
  | TaskList `emptyContent` | ReactNode | — | `items`가 비면 `List` 안에 그린다 |
81
82
  | TaskList `renderCollection` | `(context: { items, renderItem: (item: TaskItem) => ReactNode }) => ReactNode` | — | SortableCollection을 합성할 수 있다. 이때 목록 루트는 제품이 그린다 |
82
83
  | TaskList `layoutStyle`(Web) | 배치 key만 | — | List 루트 배치. `renderCollection`을 쓰면 무시되고 제품 루트를 배치한다. Native TaskList는 `layoutStyle`이 없다 |
@@ -117,3 +118,7 @@ import { Text } from "@hjmds/react-native/primitives";
117
118
  | 구조 | `role="list"` + 자식마다 `role="listitem"` | `accessibilityRole="list"` |
118
119
  | 구분선 | CSS(`data-separator`) | 행 사이 1px `View` |
119
120
  | 배치 | `layoutStyle`(+ `className`) | `layoutStyle`(`style`은 deprecated) |
121
+
122
+ 2026-10-07 Utilverse 항목 삭제 채택을 위해 `renderItemAction`을 추가했다. 체크와 삭제의 초점·누름을 분리하고 큰 글자 라벨 폭을 보존하도록 행동을 다음 줄에 둔다. `renderCollection`의 `renderItem`에도 포함된다. 삭제 저장·실패·되돌리기는 제품이 처리한다.
123
+
124
+ 항목 삭제 뒤에는 제품이 다음 항목의 독립 행동(없으면 이전 항목, 목록이 비면 후속 행동)으로 초점을 복구한다. Web Showcase는 ref와 커밋 후 focus 예시를 제공한다. Native 접근성 초점 검증은 아직 남아 있다.
@@ -122,3 +122,8 @@ import { Image } from "react-native";
122
122
  | 첨부 목록 | CSS 클래스 `hjm-message-composer__attachments` | 가로 ScrollView |
123
123
  | 이벤트 이름 | `attachmentAction.onPress`(내부에서 onClick으로 연결) | `onPress` |
124
124
  | 배치 prop | `layoutStyle`(루트) | 없음 |
125
+
126
+
127
+ ### 고정 아이콘과 큰 글자
128
+
129
+ 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-07
8
8
  - 근거: [Popover](../../popover.md), [ConfirmPopover 조합](../../confirm-popover.md), `src/popover.ts`(`popoverRecipe`)
9
9
  - 스토리북: `배포/컴포넌트/오버레이/팝오버`
10
10
 
@@ -63,6 +63,7 @@ Native 예는 없다(renderer 없음).
63
63
  | `dismissPolicy`(부분 지정) | `{ dismissible?, outsideDismiss?, escapeDismiss?, focusOutDismiss? }`(`boolean`) | 모두 `true` | |
64
64
  | `open` · `defaultOpen` | `boolean` | 비제어 `false` | 제어하면 `onOpenChange` 필수 |
65
65
  | `onOpenChange` | `(open: boolean, details: { reason }) => void` | — | `reason`: `trigger` · `close-action` · `outside-pointer` · `outside-focus` · `escape` · `programmatic` |
66
+ | `motionOrigin` | `TransitionRect` | — | 미게시: 열기 직전 viewport 좌표. 위치 확정 뒤 공간 전환하며 기존 비모달 초점·닫기 정책 유지 |
66
67
  | `initialFocusRef` | `RefObject<HTMLElement \| null>` | 첫 포커스 가능 요소 | 열릴 때 처음 포커스 |
67
68
  | `portalContainer` | `HTMLElement` | `document.body` | 표면을 붙일 곳 |
68
69
  | `className` | 문자열 | — | `layoutStyle`은 받지 않는다(아래 함정) |
@@ -106,3 +107,8 @@ Native 예는 없다(renderer 없음).
106
107
  - Popover는 `layoutStyle`을 받지 않는다(Web `layoutStyle` 제외 15개 중 하나). 렌더하는 것이 제품 trigger와 떠 있는 portal뿐이라
107
108
  배치는 trigger 쪽(또는 감싼 요소)에서 한다.
108
109
  - 부모 Popover가 닫히면 안에 중첩된 Popover도 닫히고 `onOpenChange(false, { reason: "programmatic" })`가 온다. 제어형이면 이 reason도 처리한다.
110
+
111
+ - `motionOrigin`은 출발 버튼의 `getBoundingClientRect()` 값이다. 모션 감소·잘못된 좌표·WAAPI 미지원이면
112
+ 공간 전환을 생략하고 기존 표현을 쓴다. 닫혔을 때는 즉시 inert/aria-hidden으로 입력에서 제외한다.
113
+ - 초안은 Popover 위의 제품 상태에 둔다. 종료 후 portal이 제거되므로 내부 비제어 입력에만 두면 사라진다.
114
+ - 공간 전환 실험은 [버튼에서 이어지는 편집](../compositions/origin-dialog.md)의 Web 전용 변형이다.
@@ -120,3 +120,31 @@ linear circular (Web) circular (Native)
120
120
  ## 함정
121
121
 
122
122
  - 1.12.0 전 Native `max` 기본값은 1이었다. 분수 값을 넘기던 옛 코드는 `max={1}`을 명시해야 한다([이관표](../../migration-native-legacy-removal.md)).
123
+
124
+ ### 가장자리 힌트의 표시 여부
125
+
126
+ 2026-10-07 레퍼런스 조사에서 끝까지 스크롤한 마지막 행도 블러에 가려지는 문제를 확인했다.
127
+ 새 `resolveScrollEdges`는 같은 `ScrollMetrics`에서 논리적 이전/다음 콘텐츠 존재 여부를 계산한다.
128
+ 아직 미게시이며 `/scroll-progress` subpath에서 제공한다. 블러 renderer 자체를 제공한다는 뜻은 아니다.
129
+
130
+ ```ts
131
+ import { resolveScrollEdges } from "@hjmds/design-contracts/scroll-progress";
132
+ const { before, after } = resolveScrollEdges(metrics);
133
+ ```
134
+
135
+ - viewportSize=0(미측정), 콘텐츠가 들어맞는 경우에는 둘 다 false다.
136
+ - 소수 offset과 정수 콘텐츠 크기의 오차 때문에 1 logical pixel 이내는 경계로 취급한다.
137
+ - Native 탄성 overscroll은 범위 안으로 제한한다. 가로 RTL offset은 제품 host가 논리적 전진 값으로 정규화한다.
138
+ - 콘텐츠 크기가 바뀌면 새 metrics로 다시 계산한다. focus나 읽기 도구로 접근한 콘텐츠를 가리는지는
139
+ 이 순수 계산만으로 판단할 수 없다. 시각 효과 renderer가 해당 조작 상태에서 가림을 제거해야 한다.
140
+
141
+ ### 읽기 영역의 길이가 바뀔 때
142
+
143
+ 2026-10-07 외부 읽기 진행 효과 대조에서 window 장식과 실제 본문 영역의 진행률을
144
+ 구분했다. ScrollProgress는 독서 완료 증명이 아니라 지정한 host의 위치다. 제품의 동의·
145
+ 학습 완료를 스크롤 100%만으로 확정하지 않는다.
146
+
147
+ 양쪽 읽기 진행 표시 예제의 요약만 보기/전체 내용 보기로 내용 축소와 복원을 확인한다.
148
+ Web은 useScrollMetrics의 resize/mutation 관찰, Native는 ScrollView의 onLayout·
149
+ onContentSizeChange·onScroll을 연결한다. 길이를 줄인 뒤 예전 offset을 그대로 제품 상태로
150
+ 저장하지 않는다. 측정 전 viewport=0은 0, 측정 후 화면 안에 들어오는 내용은 1이다.
@@ -0,0 +1,113 @@
1
+ # ProgressiveBlur
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: `src/progressive-blur.ts`, [도입 검토](../../../../../docs/plans/progressive-blur-adoption-2026-10-07.md)
9
+ - 스토리북: `배포/컴포넌트/시각 효과/가장자리 흐림`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 스크롤 영역의 바깥에 더 내용이 있음을 알리거나 장식 이미지 가장자리를 흐릴 때 쓴다.
16
+ 내용과 형제인 장식 레이어이며 텍스트·버튼을 children으로 받지 않는다. 원본 목록의 마지막
17
+ 행이 끝에서도 가려져 HJM은 실제 ScrollMetrics 경계와 입력 초점에 따라 효과를 제거한다.
18
+
19
+ ## 쓰지 않을 때
20
+
21
+ | 상황 | 대신 쓸 것 |
22
+ | --- | --- |
23
+ | 배경 mesh/glow/grain | [EffectSurface](effect-surface.md) |
24
+ | 읽기 완료율 | [Progress](progress.md)의 ScrollProgress |
25
+ | 가려야 하는 개인정보 | 권한·데이터 정책으로 제거. 블러는 보안 경계가 아님 |
26
+ | 중요한 본문·고정 버튼 위 | 효과를 사용하지 않음 |
27
+
28
+ ## 공개 이름과 import
29
+
30
+ | 이름 | 역할 | Web | Native |
31
+ | --- | --- | --- | --- |
32
+ | `ProgressiveBlur` | 추가 장식 API | `@hjmds/react/progressive-blur` | `@hjmds/react-native/progressive-blur` |
33
+ | ProgressiveBlurDescriptor | 공통 입력 | `@hjmds/design-contracts/progressive-blur` | 같음 |
34
+ | ProgressiveBlurHostLayer | Native 호스트 입력 | 해당 없음 | `@hjmds/react-native/progressive-blur` |
35
+
36
+ 루트 export에 없다. 기존 EffectSurface는 내용을 흐리지 않는 배경 장식이므로 별도 API를 둔다.
37
+ Native renderer에 Expo/마스크 peer를 추가하지 않는다. 실제 블러·마스크 엔진은 제품이 연결한다.
38
+
39
+ ## 최소 사용 예
40
+
41
+ ```tsx
42
+ // Web
43
+ import { ProgressiveBlur } from "@hjmds/react/progressive-blur";
44
+ <ProgressiveBlur descriptor={{ edge: "bottom", extent: 64, strength: 0.5,
45
+ content: { kind: "scroll", metrics, focused: contentHasFocus } }} />
46
+ ```
47
+
48
+ ```tsx
49
+ // Native
50
+ import { ProgressiveBlur } from "@hjmds/react-native/progressive-blur";
51
+ <ProgressiveBlur descriptor={{ edge: "bottom", extent: 64, strength: 0.5,
52
+ content: { kind: "scroll", metrics, focused: contentHasFocus } }}
53
+ renderLayer={layer => renderProductBackdropLayer(layer)} />
54
+ ```
55
+
56
+ 내용과 효과를 같은 positioned/clipped 부모의 형제로 둔다. 위쪽과 아래쪽이 필요하면 두 개를
57
+ 놓는다. Web useScrollMetrics를 재사용하고 Native는 실제 onScroll/onLayout/onContentSizeChange를
58
+ 연결한다. 내용에 초점이 있는 동안 focused=true를 전달한다. 읽기 도구·외부 키보드 포커스도
59
+ 제품에서 전달하거나 그 환경에서는 효과를 끈다. Showcase를 소비 앱에서 import하지 않는다.
60
+
61
+ ## 축과 기본값
62
+
63
+ | prop | 값 | 기본값 | 설명 |
64
+ | --- | --- | --- | --- |
65
+ | descriptor.edge | top · bottom · start · end | 필수 | start/end는 provider direction으로 물리 위치 결정 |
66
+ | descriptor.extent | 0 초과~160 logical px | 필수 | 실제 host에서 비교 후 명시적으로 선택 |
67
+ | descriptor.strength | 0~1 | 필수 | 0은 효과 없음, 플랫폼 간 같은 px를 뜻하지 않음 |
68
+ | descriptor.layers | 정수 2~8 | 4 | 실험 시작값, 기기 성능 보증이 아님 |
69
+ | descriptor.content | decoration 또는 scroll | 필수 | scroll은 metrics와 focused 필수 |
70
+ | renderLayer(Native) | layer → ReactNode | 필수 | 실제 플랫폼 블러와 alpha mask. 호스트 없음은 null로 표현 가능 |
71
+
72
+ Native layer에는 side(물리 방향), strength, start/end(0~1 alpha ramp 위치)가 전달된다.
73
+ inner edge에서 투명, 지정 side 쪽에서 불투명인 마스크를 만든다. Web은 strength×16px blur와
74
+ CSS mask를 쓴다. Native intensity와 CSS radius는 같은 단위가 아니다.
75
+
76
+ ## 배치
77
+
78
+ | 항목 | 값 | 근거 |
79
+ | --- | --- | --- |
80
+ | 크기 | 상하: 부모 폭 + extent 높이, 좌우: 부모 높이 + extent 폭 | resolveProgressiveBlur와 renderer |
81
+ | 간격 | 자체 여백 없음, 지정 side에 0으로 부착 | renderer absolute frame |
82
+ | 순서·정렬 | 내용 뒤의 형제 레이어, 포인터 통과 | pointerEvents none |
83
+ | 고정·스크롤 | 스크롤 내용 밖 같은 부모 안에 둠 | 예제 positioned host |
84
+ | 좁은 폭·큰 글자 | 부모 크기와 새 ScrollMetrics를 사용, 입력 초점 때 제거 | content.kind scroll |
85
+
86
+ ## 꼭 지킬 것
87
+
88
+ - 장식 내용을 별도 accessible subtree로 만들지 않는다. 컴포넌트는 장식 호스트 전체를 접근성 트리에서 숨긴다.
89
+ - scroll 끝에서는 해당 효과가 사라지고 맞춤/미측정이면 양쪽 모두 사라진다. content.kind decoration으로 이를 우회하지 않는다.
90
+ - Native renderLayer는 절대 위치로 부모를 채우고 터치 대상·텍스트를 넣지 않는다.
91
+ - Native 호스트가 실패하면 장식만 사라진다. 복구를 원할 때 명시적으로 remount한다.
92
+ - 정적 효과여서 자체 자동 애니메이션이 없다. 주변 콘텐츠의 모션 감소·일시 정지는 별도로 유지한다.
93
+ - 원본 8층을 무조건 복사하지 않고 2~8층과 효과 없음을 같은 기기에서 비교한다. 기본값 4는 실험용이다.
94
+
95
+ ## 플랫폼 차이
96
+
97
+ | 항목 | Web | Native |
98
+ | --- | --- | --- |
99
+ | 실제 효과 | CSS backdrop-filter + mask-image | 제품 blur/mask host |
100
+ | 추가 peer | 없음 | HJM 없음, 제품 호스트에 따라 필요 |
101
+ | 지원 누락 | CSS 미지원이면 내용은 그대로 보임 | null 또는 호스트 오류 경계로 장식만 제거 |
102
+ | 스크롤 | useScrollMetrics | 실제 Native 이벤트 연결 |
103
+ | 데모 엔진 | 브라우저 | ExpoBlur + MaskedView + SVG alpha mask |
104
+
105
+ Expo SDK57 Android는 BlurTargetView/blurTarget/blurMethod가 있어야 실제 블러가 된다.
106
+ 동적 목록 뒤에 효과를 렌더하며 Android 12 이전 비용은 별도로 측정한다. 모듈 누락 안내는
107
+ 효과 지원·기기 검증 통과가 아니다. 실험 승격 전 양 플랫폼의 실제 합성과 기기 성능을 확인한다.
108
+
109
+ ## 함정
110
+
111
+ 전체 화면에 고정하면 다른 영역을 흐릴 수 있다. 부모를 반드시 클리핑한다. extent가 작은
112
+ viewport보다 크지 않게 선택한다. 공개 HJM API는 기본 subtree의 배치를 바꾸지 않으며,
113
+ 제품 호스트의 alpha mask가 틀리면 단위 테스트가 통과해도 실제 블러는 보이지 않을 수 있다.
@@ -0,0 +1,74 @@
1
+ # Rating
2
+
3
+ - 단계: 컴포넌트
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: [계약](../../reference-controls.md)
9
+ - 스토리북: `배포/컴포넌트/입력/별점 선택`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 정수 점수를 선택하거나 계산된 소수 평균을 읽기 전용으로 보여 줄 때 쓴다.
16
+
17
+ ## 쓰지 않을 때
18
+
19
+ | 상황 | 대신 쓸 것 |
20
+ | --- | --- |
21
+ | 반점 간격의 연속 점수 입력 | [Slider](slider.md)의 기존 별점 예제 |
22
+ | 일반 선택지 | [RadioGroup](radio-group.md) |
23
+
24
+ ## 공개 이름과 import
25
+
26
+ | 이름 | 역할 | Web | Native |
27
+ | --- | --- | --- | --- |
28
+ | `Rating` | 독립 supplemental | `@hjmds/react/rating` | `@hjmds/react-native/rating` |
29
+
30
+ ## 최소 사용 예
31
+
32
+ ```tsx
33
+ // Web
34
+ import { Rating } from "@hjmds/react/rating";
35
+ <Rating label={label} value={score} onValueChange={setScore} getValueLabel={formatScore} clearLabel={clearLabel} />
36
+ ```
37
+
38
+ ```tsx
39
+ // Native
40
+ import { Rating } from "@hjmds/react-native/rating";
41
+ <Rating label={label} value={score} onValueChange={setScore} getValueLabel={formatScore} clearLabel={clearLabel} />
42
+ ```
43
+
44
+ Native는 import를 `@hjmds/react-native/rating`로 바꾼다.
45
+
46
+ ## 축과 기본값
47
+
48
+ | prop | 값 | 기본값 | 설명 |
49
+ | --- | --- | --- | --- |
50
+ | `value` | number 또는 null | 필수 | 5개 기본, max 1~10. 미평가는 null이며 입력 점수는 1~max의 정수, 평균은 readOnly의 0~max 소수다. |
51
+ | Web `layoutStyle` | 배치 전용 style | 없음 | 바깥 틀의 폭·margin·flex 배치 |
52
+ | `disabled` | boolean | false | 변경을 막고 현재 값을 유지 |
53
+
54
+ ## 배치
55
+
56
+ | 항목 | 값 | 근거 |
57
+ | --- | --- | --- |
58
+ | 크기 | Web은 44px 최소 radio 영역, Native는 control.minTouchTarget 44. 별 그림은 글자 크기에 따라 커지고 행이 줄바꿈된다. | renderer `rating.tsx` |
59
+ | 간격 | space.xs=8px의 별 사이 간격; 초기화는 점수 문구 뒤에 온다. | foundations spacing |
60
+ | 순서·정렬 | 질문→선택→점수→선택적 초기화 | renderer 순서 |
61
+ | 고정·스크롤 | 바깥 화면이 스크롤을 소유한다 | 별도 스크롤 없음 |
62
+ | 좁은 폭·큰 글자 | 폭을 강제 고정하지 않고 본문/행의 줄바꿈을 허용한다 | 위 배치 |
63
+
64
+ ## 꼭 지킬 것
65
+
66
+ - Web 초기화 후 첫 별점으로 초점이 돌아온다. Space로 다시 선택할 수 있다.
67
+ - label, value, getValueLabel을 제품 언어로 공급한다. 입력형은 onValueChange가 필수이고 평균형에는 전달하지 않는다.
68
+ - 브랜드는 HJM provider의 semantic palette로 연결한다. 점수 집계·이미지 변환·저장은 제품 소유다.
69
+
70
+ ## 플랫폼 차이
71
+
72
+ | 항목 | Web | Native |
73
+ | --- | --- | --- |
74
+ | 조작 | Web은 실제 radio와 name으로 폼에 연결한다. Native는 radio 접근성 상태와 onPress를 쓴다. | 같은 의미를 플랫폼 host로 번역 |
@@ -118,3 +118,13 @@ import { SearchField } from "@hjmds/react-native/inputs";
118
118
  | 진행 중 입력 | 입력 가능(`aria-busy`) | 입력 가능(`accessibilityState.busy`). 2026-10-06까지 Native는 `busy`인 동안 입력을 무시했다(1.12.1 이후 미게시) |
119
119
  | 입력 요소 | `<input type="search">`, 원시 `onChange`도 전달 | `TextInput` |
120
120
  | 배치 | `layoutStyle`·`fieldClassName`(필드 틀), `style`은 안쪽 input | `layoutStyle` |
121
+
122
+
123
+ ### 고정 아이콘과 큰 글자
124
+
125
+ 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.
126
+
127
+ ### 브라우저 지우기 중복 방지
128
+
129
+ Web은 type=search를 유지하지만 브라우저 기본 cancel 장식은 숨긴다. 2026-10-07 빈 상태 복구
130
+ 실측에서 ×가 두 개 보였기 때문이다. 실제 지우기는 HJM 버튼의 clearLabel·onClear·초점 복귀로 한다.
@@ -5,7 +5,7 @@
5
5
  - 지원: Web · Native
6
6
  - 적용: 미게시(1.12.1 이후)
7
7
  - 검토일: 2026-10-06
8
- - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. `onSubmit`·`filtersOverflow`는 2026-10-06 사용자 요청("검색과 필터 있는 앱 서비스 따라해")의 검색 화면 개편에서 추가. 같은 날 사용자 위임 결정(권장안)으로 미리보기에만 있던 두 단계 검색 로직(`committedQuery`·`recentQueries`·`suggestedQueries`·`suggestions`·`resultSummary`·`appliedFilters`·`filterSheet`)과 `queryLabelVisibility`를, utilverse 적용 조사로 `searching`·`searchingLabel`을 추가([계약 결정 표](../../screen-patterns.md#searchscreen-두-단계-검색)). 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screens.tsx`·`src/screen-flows.tsx`; 기존 개별 지침을 새 규격으로 통합. `onSubmit`·`filtersOverflow`는 2026-10-06 사용자 요청("검색과 필터 있는 앱 서비스 따라해")의 검색 화면 개편에서 추가. 같은 날 사용자 위임 결정(권장안)으로 미리보기에만 있던 두 단계 검색 로직(`committedQuery`·`recentQueries`·`suggestedQueries`·`suggestions`·`resultSummary`·`appliedFilters`·`filterSheet`)과 `queryLabelVisibility`를, utilverse 적용 조사로 `searching`·`searchingLabel`을 추가([계약 결정 표](../../screen-patterns.md#searchscreen-두-단계-검색)). 1.13.0 utilverse 적용에서 드러난 결함으로 `hostGutter`와 확정 시 키보드 닫기(Web 결과 영역 포커스)를 추가(1.13.1 patch). 예제 스토리는 2026-10-06 사용자 승인으로 스토리북 배포([승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정)). 스토리북 배포는 API 게시가 아니다(`적용` 참고)
9
9
  - 스토리북: `배포/화면/검색/검색 결과와 필터`
10
10
 
11
11
  ## 언제 쓰나
@@ -146,7 +146,7 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
146
146
  | `queryField` | `ReactNode` | 없음 | 기본 `SearchField` 대신 그릴 입력. 지우기 동작을 직접 제공한다 |
147
147
  | `debounceMs` | `number` | `300` | 0 미만은 0 |
148
148
  | `filters` | `ReactNode` | 없음 | 검색 입력 아래, 상단에 고정. 두 단계 검색에서는 결과 단계에만, 검색어 때문인 0건에는 숨긴다 |
149
- | `onSubmit` | `(query: string) => void` | 없음 | 확정 신호. 기본 입력의 Web Enter(`enterKeyHint="search"`, IME 조합 중 Enter는 무시)·Native 키보드 검색 키(`returnKeyType="search"`, `onSubmitEditing`)와 SearchScreen이 그린 확정 행·제안·최근·추천 검색어가 모두 여기로 온다. 앞뒤 공백을 지운 값이고 공백만이면 부르지 않는다. 고른 값이 입력과 다르면 먼저 `onQueryChange`. 대기 중인 `onSearch` debounce는 그대로 둔다. `queryField`를 주면 그 입력의 키는 연결하지 않는다 |
149
+ | `onSubmit` | `(query: string) => void` | 없음 | 확정 신호. 모든 확정에서 키보드를 닫는다(Native `Keyboard.dismiss()`, Web은 고른 항목이면 결과 영역으로 포커스, 미게시(1.13.1 이후)) — 아래 플랫폼 차이. 기본 입력의 Web Enter(`enterKeyHint="search"`, IME 조합 중 Enter는 무시)·Native 키보드 검색 키(`returnKeyType="search"`, `onSubmitEditing`)와 SearchScreen이 그린 확정 행·제안·최근·추천 검색어가 모두 여기로 온다. 앞뒤 공백을 지운 값이고 공백만이면 부르지 않는다. 고른 값이 입력과 다르면 먼저 `onQueryChange`. 대기 중인 `onSearch` debounce는 그대로 둔다. `queryField`를 주면 그 입력의 키는 연결하지 않는다 |
150
150
  | `committedQuery` | `string` | 없음 | 결과를 보여 줄 확정 검색어. 주면 두 단계 검색(`onSubmit` 필수, 타입). 단계: 검색어 공백 = 검색 전, 확정값과 다름 = 입력 중, 같음 = 결과(앞뒤 공백 무시). `children`은 결과 단계에만 그린다 |
151
151
  | `queryLabelVisibility` | `visible` · `hidden` | `visible` | `hidden`이면 보이는 label 없이 `queryLabel`을 접근성 이름(Web `aria-label`, Native `accessibilityLabel`)과 placeholder로 쓴다 |
152
152
  | `searching` · `searchingLabel` | `boolean` · `string` | 없음 | 기본 입력의 진행 표시(Web `loading`, Native `busy`+`busyLabel`). 함께 주거나 함께 생략(타입). 켜져 있으면 `searchingLabel`을 낭독한다 |
@@ -157,7 +157,8 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
157
157
  | `resultSummary.sort` | `{ label, triggerLabel, value, options: { id, label }[], onChange(id), icon? }` (Native는 `dismissLabel` 추가) | 없음 | 결과 머리 끝 Menu(single). 바꾸면 본문을 맨 위로 |
158
158
  | `appliedFilters` | `{ items: { key, label }[], removeLabel(label), onRemove(key), clearAllLabel, onClearAll(), removeIcon? }` | 없음 | 결과 머리 아래 × 칩 + `모두 해제`. 개수는 trigger 이름과 0건 원인 판정에도 쓴다 |
159
159
  | `filterSheet` | `{ open, onOpenChange(open), title, value, onApply(next), count(draft): number \| null, reset(draft), isDefault(draft), renderContent(draft, setDraft), labels: { close, reset, apply(count) }, size?, trigger? }` | 없음 | 열 때 `value`를 초안으로 복사, 닫히면 초안을 버림, 주 행동에서만 `onApply`. `count = 0`이면 주 행동 비활성, `isDefault`면 초기화 비활성. `trigger`를 주면 칩 줄 맨 앞에 `label(적용 개수)` Chip |
160
- | `filtersOverflow` | `wrap` · `scroll` | `wrap` | `scroll`이면 `filters`를 한 줄 가로 스크롤 영역에 넣고 화면 좌우 여백(`spacing.md` 16)만큼 가장자리까지 넓힌다. 첫 칩은 검색 입력과 같은 시작선에 놓인다 |
160
+ | `filtersOverflow` | `wrap` · `scroll` | `wrap` | `scroll`이면 `filters`를 한 줄 가로 스크롤 영역에 넣고 화면 좌우 여백(`spacing.md` 16, `contentInset="none"`이면 0)에 `hostGutter`를 더한 만큼 가장자리까지 넓힌다. 첫 칩은 검색 입력과 같은 시작선에 놓인다 |
161
+ | `hostGutter` | `none` · `compact` · `regular` · `spacious` | `none` | 미게시(1.13.1 이후). 이 화면을 감싼 host가 이미 준 좌우 여백 이름(`containerRecipe.gutters`: 0 · `spacing.md` 16 · `spacing.lg` 20 · `spacing.xl` 24). Container 안이면 그 `gutter`, Sheet 본문 안이면 `regular`(`sheetRecipe.content.paddingHorizontal`과 같은 `spacing.lg`). `scroll` 줄만 이 값을 쓴다 |
161
162
  | `recentSearches` | `ReactNode` | 없음 | `query`가 공백일 때만 본문 맨 위에 보인다 |
162
163
  | `children` | `ReactNode` | 필수 | 결과 |
163
164
  | 나머지 | `ScreenLayout`과 같음(`children`·`footer` 제외) | — | `notice`는 필터 아래에 붙는다 |
@@ -185,6 +186,7 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
185
186
  - 두 단계 검색에서 `children`에는 결과 목록만 넣는다. 제안·로딩 행·0건 EmptyState·개수·정렬을 다시 그리면 SearchScreen이 그린 것과 겹친다.
186
187
  - 적용 조건은 `appliedFilters`로 넘긴다. 0건 원인(필터면 칩 줄 유지 + `모두 해제`, 검색어면 칩 줄 숨김 + 추천 검색어)을 이 개수로 판정한다.
187
188
  - 시트 `count`와 결과 개수는 같은 계산에서 나와야 한다. 다르면 "N개 결과 보기"와 적용 뒤 개수가 어긋난다.
189
+ - `contentInset="none"`으로 host가 여백을 주는 화면은 `hostGutter`에 그 여백 이름을 넘긴다. 없으면 `scroll` 줄이 host 여백 안에서 잘린다.
188
190
  - 칩이 여러 개인 필터 줄은 `filtersOverflow="scroll"`로 한 줄에 둔다. 제품이 CSS·ScrollView로 가로 스크롤을 따로 만들지 않는다
189
191
  (가장자리 여백·포커스 링 잘림·RTL이 제품마다 갈린다).
190
192
  - 검색 로딩·0건·오류는 `state`로 본문을 바꾸지 않고 `children` 안에서 그린다(입력·필터·이전 결과를 지키기 위해서다. 2026-10-06 개편 전
@@ -196,7 +198,8 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
196
198
  | --- | --- | --- |
197
199
  | `queryField` slot | 넘기면 기본 SearchField 대신 그린다 | 넘기면 기본 SearchField 대신 그린다 |
198
200
  | 기본 입력 | `SearchField` + `queryLabel`·`queryClearLabel` | `SearchField` + `queryLabel`·`queryClearLabel`, `busyLabel`도 `queryLabel` |
199
- | `onSubmit` | Enter keydown. `isComposing`·keyCode 229(IME 조합)는 무시 | `onSubmitEditing`. 한 줄 입력의 기본 blur-on-submit으로 키보드가 닫힌다 |
201
+ | `onSubmit` | Enter keydown. `isComposing`·keyCode 229(IME 조합)는 무시. Enter로 확정하면 포커스는 입력에 남는다 | `onSubmitEditing`. 한 줄 입력의 기본 blur-on-submit으로 키보드가 닫힌다 |
202
+ | 제안·최근·추천을 골라 확정 | 고른 행·칩은 단계가 바뀌며 사라지므로 포커스를 결과 영역(`.hjm-search-screen__results`, `tabIndex=-1`, 테두리 없음)으로 옮긴다. 입력을 떠나므로 모바일 브라우저 키보드도 닫힌다. 개수는 기존 `role="status"`가 읽는다 | `Keyboard.dismiss()`(one-step·two-step 모두). 포커스는 옮기지 않는다(낭독 커서 유지) |
200
203
  | `queryLabelVisibility="hidden"` | `aria-label` + `placeholder` | `accessibilityLabel` + `placeholder` |
201
204
  | `searching` | `loading`: 입력 가능, `aria-busy`, 숨긴 `role="status"`로 `searchingLabel` | `busy`+`busyLabel`: 입력 가능, `announceForAccessibilityWithOptions(queue)` |
202
205
  | 개수 낭독 | 숨긴 `role="status"` 하나 | `announceForAccessibilityWithOptions(…, { queue: true })`, 문구가 바뀔 때만 |
@@ -204,7 +207,7 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
204
207
  | 제안 강조 | `match` 범위 굵게 | ListRow 제목이 문자열이라 강조 없음 |
205
208
  | 정렬 | Menu 목록, 바꾸면 `.hjm-screen__body` 맨 위 | Menu에 `sort.dismissLabel` 필요, 바꾸면 본문 ScrollView 맨 위(`scrollRef`는 그대로 전달) |
206
209
  | 필터 시트 | 창 폭 ≥ 960(expanded)이면 옆 시트(`placement="end"`), footer [초기화][주] | 아래 시트 `scrollable`, footer 세로 fullWidth 주 먼저 |
207
- | `filtersOverflow="scroll"` | `div.hjm-search-screen__filters`(overflow-x auto, 스크롤바 숨김, 위아래로 포커스 링 두께만큼 여백). Tab 이동 시 브라우저가 칩을 보이게 스크롤한다 | 가로 `ScrollView`(`keyboardShouldPersistTaps="handled"`, 표시기 숨김), `contentInset="none"`이면 넓히지 않는다 |
210
+ | `filtersOverflow="scroll"` | `div.hjm-search-screen__filters`(overflow-x auto, 스크롤바 숨김, 위아래로 포커스 링 두께만큼 여백, 넓히는 폭은 inline `--hjm-search-filters-bleed`). Tab 이동 시 브라우저가 칩을 보이게 스크롤한다 | 가로 `ScrollView`(`keyboardShouldPersistTaps="handled"`, 표시기 숨김, `marginHorizontal`·`paddingHorizontal`에 같은 폭) |
208
211
 
209
212
  ## 함정
210
213
 
@@ -212,4 +215,12 @@ import { SearchScreen } from "@hjmds/react-native/screen-flows";
212
215
  - 1.12.1 이하 Native에서는 `searching`인 동안 SearchField `busy`가 입력을 무시해 그동안 친 글자가 사라졌다. 다음 릴리스부터 두 플랫폼 모두 입력을 받으므로 입력 중 제안 조회에도 켤 수 있다. 1.12.1 이하에 머무는 Native 제품은 확정 결과 요청에만 켠다.
213
216
  - Native 필터 시트는 닫힘 애니메이션이 끝난 뒤에 다시 열린다(Sheet 계약). 닫자마자 `open`을 켜도 바로 보이지 않을 수 있다.
214
217
  - Web `filters` 안에 `Stack wrap`을 넣어도 `scroll`에서는 한 줄이다(자식 폭을 `max-content`로 잡는다).
218
+ - 1.13.0 이하에서는 제안·최근·추천을 골라 확정해도 Native 키보드가 남았고(검색 키만 닫혔다), Web은 고른 행이 사라지며 포커스가 `<body>`로
219
+ 떨어졌다. utilverse는 `onSubmit`에서 `Keyboard.dismiss()`를 불렀다(2026-10-06). 1.13.1부터 SearchScreen이 닫으므로 그 호출을 지운다.
220
+ - 1.13.0 이하에서는 `contentInset="none"`이면 `scroll` 줄이 전혀 넓어지지 않아 host 여백(Container·Sheet)에서 잘렸다. 1.13.1부터 `hostGutter`로 맞춘다.
221
+ - 시트 안 SearchScreen은 Sheet `size`를 `auto` 밖으로 주고 `scrollable` 없이 넣는다. 1.13.0 이하 Native는 이때 화면이 0pt가 됐다([Sheet 함정](sheet.md#함정)).
215
222
 
223
+
224
+ ### 고정 아이콘과 큰 글자
225
+
226
+ 2026-10-06 최근 검색 삭제 기호가 큰 글자에서 잘린 재현에 따라 Native 내장 삭제·메뉴 기호는 고정 아이콘 틀의 크기를 유지한다. 주변 제목·라벨은 계속 확대한다. Chip의 체크와 Toast 닫기는 기존 비확대 경로를 유지하며 회귀 검사에 포함한다. 제품이 전달한 아이콘 슬롯은 제품이 같은 조건을 검증한다.