@hjmds/design-contracts 1.41.2 → 1.42.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 (138) hide show
  1. package/dist/affix.d.ts +18 -0
  2. package/dist/affix.d.ts.map +1 -1
  3. package/dist/affix.js +13 -1
  4. package/dist/affix.js.map +1 -1
  5. package/dist/alert-dialog.d.ts +32 -0
  6. package/dist/alert-dialog.d.ts.map +1 -1
  7. package/dist/alert-dialog.js +52 -0
  8. package/dist/alert-dialog.js.map +1 -1
  9. package/dist/behaviors.d.ts +6 -5
  10. package/dist/behaviors.d.ts.map +1 -1
  11. package/dist/behaviors.js +1 -1
  12. package/dist/behaviors.js.map +1 -1
  13. package/dist/catalog.d.ts +165 -15
  14. package/dist/catalog.d.ts.map +1 -1
  15. package/dist/catalog.js +4 -4
  16. package/dist/catalog.js.map +1 -1
  17. package/dist/character-count.d.ts +103 -0
  18. package/dist/character-count.d.ts.map +1 -0
  19. package/dist/character-count.js +68 -0
  20. package/dist/character-count.js.map +1 -0
  21. package/dist/chart.d.ts +30 -0
  22. package/dist/chart.d.ts.map +1 -1
  23. package/dist/chart.js +18 -0
  24. package/dist/chart.js.map +1 -1
  25. package/dist/collection-rail.d.ts +13 -1
  26. package/dist/collection-rail.d.ts.map +1 -1
  27. package/dist/collection-rail.js +9 -2
  28. package/dist/collection-rail.js.map +1 -1
  29. package/dist/component-recipes.d.ts +172 -2
  30. package/dist/component-recipes.d.ts.map +1 -1
  31. package/dist/component-recipes.js +122 -8
  32. package/dist/component-recipes.js.map +1 -1
  33. package/dist/design-system-provider.d.ts +13 -2
  34. package/dist/design-system-provider.d.ts.map +1 -1
  35. package/dist/design-system-provider.js +19 -3
  36. package/dist/design-system-provider.js.map +1 -1
  37. package/dist/foundations.d.ts +15 -0
  38. package/dist/foundations.d.ts.map +1 -1
  39. package/dist/foundations.js +7 -0
  40. package/dist/foundations.js.map +1 -1
  41. package/dist/icon-button-recipe.d.ts +33 -0
  42. package/dist/icon-button-recipe.d.ts.map +1 -1
  43. package/dist/icon-button-recipe.js +36 -1
  44. package/dist/icon-button-recipe.js.map +1 -1
  45. package/dist/image.d.ts +13 -0
  46. package/dist/image.d.ts.map +1 -1
  47. package/dist/image.js +17 -0
  48. package/dist/image.js.map +1 -1
  49. package/dist/index.d.ts +2 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +2 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/recipes.d.ts +1 -1
  54. package/dist/recipes.d.ts.map +1 -1
  55. package/dist/recipes.js +1 -1
  56. package/dist/recipes.js.map +1 -1
  57. package/dist/screen-patterns.d.ts +92 -0
  58. package/dist/screen-patterns.d.ts.map +1 -1
  59. package/dist/screen-patterns.js +79 -2
  60. package/dist/screen-patterns.js.map +1 -1
  61. package/dist/selection-helpers.d.ts +6 -0
  62. package/dist/selection-helpers.d.ts.map +1 -1
  63. package/dist/selection-helpers.js +12 -0
  64. package/dist/selection-helpers.js.map +1 -1
  65. package/dist/sidebar.d.ts +17 -2
  66. package/dist/sidebar.d.ts.map +1 -1
  67. package/dist/sidebar.js +8 -2
  68. package/dist/sidebar.js.map +1 -1
  69. package/dist/steps.d.ts +19 -19
  70. package/dist/steps.d.ts.map +1 -1
  71. package/dist/steps.js +21 -9
  72. package/dist/steps.js.map +1 -1
  73. package/dist/timeline.d.ts +41 -0
  74. package/dist/timeline.d.ts.map +1 -1
  75. package/dist/timeline.js +47 -1
  76. package/dist/timeline.js.map +1 -1
  77. package/dist/top.d.ts +21 -2
  78. package/dist/top.d.ts.map +1 -1
  79. package/dist/top.js +17 -1
  80. package/dist/top.js.map +1 -1
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.js +1 -1
  83. package/dist/version.js.map +1 -1
  84. package/docs/affix.md +7 -0
  85. package/docs/avatar-fallback.md +19 -0
  86. package/docs/brand-boundary.md +24 -2
  87. package/docs/chart.md +7 -0
  88. package/docs/collection-rail.md +4 -1
  89. package/docs/generated/component-maturity.md +1 -1
  90. package/docs/generated/renderer-evidence.json +10 -6
  91. package/docs/generated/renderer-evidence.md +1 -1
  92. package/docs/generated/showcase-manifest.json +12 -5
  93. package/docs/image.md +5 -0
  94. package/docs/list-row.md +8 -0
  95. package/docs/optional-adapters.md +3 -0
  96. package/docs/overlay-stack.md +27 -0
  97. package/docs/screen-patterns.md +39 -2
  98. package/docs/sidebar.md +15 -0
  99. package/docs/steps.md +23 -19
  100. package/docs/theme-palette.md +3 -3
  101. package/docs/theming.md +1 -0
  102. package/docs/timeline.md +18 -1
  103. package/docs/top.md +7 -0
  104. package/docs/usage/README.md +2 -2
  105. package/docs/usage/components/affix.md +18 -4
  106. package/docs/usage/components/alert-dialog.md +28 -0
  107. package/docs/usage/components/avatar.md +30 -9
  108. package/docs/usage/components/badge.md +20 -5
  109. package/docs/usage/components/chart.md +19 -1
  110. package/docs/usage/components/checkbox-group.md +24 -1
  111. package/docs/usage/components/choice-row.md +19 -4
  112. package/docs/usage/components/composition-layout.md +6 -2
  113. package/docs/usage/components/counter-badge.md +2 -2
  114. package/docs/usage/components/design-system-provider.md +2 -1
  115. package/docs/usage/components/divider.md +12 -1
  116. package/docs/usage/components/field.md +51 -0
  117. package/docs/usage/components/icon-button.md +48 -4
  118. package/docs/usage/components/icon-tile.md +13 -2
  119. package/docs/usage/components/image.md +25 -0
  120. package/docs/usage/components/list-row.md +8 -4
  121. package/docs/usage/components/list.md +1 -1
  122. package/docs/usage/components/notification-item.md +5 -1
  123. package/docs/usage/components/permission-screen.md +25 -4
  124. package/docs/usage/components/screen-layout.md +27 -1
  125. package/docs/usage/components/segmented-control.md +16 -3
  126. package/docs/usage/components/select.md +1 -0
  127. package/docs/usage/components/settings-screen.md +5 -4
  128. package/docs/usage/components/sheet.md +2 -1
  129. package/docs/usage/components/sidebar.md +34 -6
  130. package/docs/usage/components/steps.md +29 -8
  131. package/docs/usage/components/surface.md +2 -1
  132. package/docs/usage/components/text-area.md +2 -1
  133. package/docs/usage/components/timeline.md +25 -4
  134. package/docs/usage/components/top.md +15 -2
  135. package/docs/usage/screens/common-settings.md +3 -2
  136. package/docs/usage/screens/flow-permission.md +3 -3
  137. package/docs/usage/tokens/elevation-opacity.md +14 -2
  138. package/package.json +7 -1
@@ -107,11 +107,11 @@ Ant의 원시 색상이나 낮은 테두리 대비까지 복제하지 않고, HJ
107
107
 
108
108
  휠의 하늘색과 카테고리 선택색을 비교한 뒤 사용자가 기본 테마의 뉴트럴 안을 승인했다.
109
109
  `surfaceSelected`는 light `#f0f1f3`, dark `#30343b`이고 `semanticColors.selection`이 배경·글자·테두리를 묶는다.
110
- TimePicker, Chip(그룹의 chips 표현 포함)과 기본 neutral SegmentedControl이 같은 선택 배경을 쓴다.
110
+ TimePicker, Chip(그룹의 chips 표현 포함)과 `selectionTone="neutral"` SegmentedControl이 같은 선택 배경을 쓴다.
111
111
  글자는 기존 `text`, Chip 테두리는 `borderControl`을 쓰며 체크·선택 테두리·포커스 표시를 보존한다.
112
112
  이 결정은 위 2026-09-29 기록의 해당 선택 배경을 대체하며 입력 필드 배경은 바꾸지 않는다.
113
113
  `surfaceAccent`는 브랜드 보조 배경, `primary`는 주요 동작으로 유지한다.
114
- 명시적인 `selectionTone="brand"`와 solid 탭은 기존 브랜드 강조를 유지한다.
114
+ SegmentedControl의 `selectionTone="brand"`와 solid 탭은 브랜드 강조를 쓴다. 같은 날 후속 시각 비교에서 사용자가 강조를 기본으로 선택했으므로 SegmentedControl은 `brand`가 기본이며 `neutral`은 옵션이다(Web·Native 개발 소스, 미게시). 다른 컴포넌트의 중립 선택 역할은 유지한다.
115
115
  제품은 `brandPalette`의 `surfaceSelected`로 선택 배경을 지정할 수 있다. 기존 팔레트 대비 검사에 `text`/`surfaceSelected` 4.5:1 쌍을 포함한다.
116
116
 
117
117
  ## 선택 상태 색상 기준 (2026-10-23)
@@ -124,7 +124,7 @@ TimePicker, Chip(그룹의 chips 표현 포함)과 기본 neutral SegmentedContr
124
124
  | --- | --- | --- |
125
125
  | 지속되는 일반 선택 면 | `selection.background`/`selection.content`, 경계가 필요하면 `selection.border` | Chip·칩 그룹, neutral SegmentedControl, TimePicker, filled Tabs, capsule BottomNavigation, ReactionPicker, ToggleGroup, selected Button·IconButton, 선택 Card·ListRow·Select·Combobox·Menu·Tree·TransferList·DataTable 행 |
126
126
  | 현재 조작 위치 | 기존 focus/hover/pressed 역할. 확정 선택과 합치지 않음 | CommandPalette·Mentions·TagsInput 제안의 active descendant, Select·Combobox의 active 항목, 포커스 링 |
127
- | 명시적인 진한 브랜드 면 | `primary`/`onPrimary` | `Tabs indicator="solid"`, `SegmentedControl selectionTone="brand"`, `BottomNavigation selection="tile"`, prominent destination |
127
+ | 진한 브랜드 면 | `primary`/`onPrimary` | `Tabs indicator="solid"`, `SegmentedControl` 기본 `selectionTone="brand"`, `BottomNavigation selection="tile"`, prominent destination |
128
128
  | 작은 값 표시·현재 위치 표시 | 기존 전용 체크·점·밑줄·테두리 계약 | Checkbox·Radio의 glyph, Switch on, Calendar 날짜 원, underline/text Tabs, 일반 BottomNavigation 아이콘·글자, Pagination·Steps·Anchor·Carousel 위치 표시, 읽기 전용 반응 요약 |
129
129
  | 브랜드·장식·업무 강조 | 기존 브랜드/상태 역할 | ChatMessage outgoing, 폴더 그림, liquid toast, 표의 `highlighted` 업무 강조, 브랜드 Surface/Badge, 오류·성공 등 상태 |
130
130
  | 미디어 위 선택 증거 | 읽을 수 있는 전용 링·스크림 유지 | ImageChoice·MediaSelection. 회색 면으로 사진을 덮지 않음 |
package/docs/theming.md CHANGED
@@ -29,6 +29,7 @@ import { HjmProvider } from "@hjmds/react/provider";
29
29
  교체되고 나머지는 기본값을 유지하므로 recipe와 대비 규칙이 그대로 적용된다.
30
30
  1.5.0부터 Provider가 `brandPalette`를 prop으로 직접 받는다. 이 경로에서 Provider는 계속
31
31
  OS theme·글자 크기·reduced motion을 관찰하고, 중첩 Provider는 가장 가까운 상위 `brandPalette`를 물려받는다.
32
+ 중첩 Provider가 일부 역할만 바꾸려면 `brandPaletteMode="extend"`를 함께 준다(기본 `replace`는 상위 브랜드를 끊는다, [브랜드 경계 §1.1](./brand-boundary.md#11-하위-범위의-일부-역할만-바꾸기--brandpalettemode-개발-소스미게시)).
32
33
 
33
34
  ```tsx
34
35
  import { HjmProvider, type HjmBrandPalette } from "@hjmds/react/provider";
package/docs/timeline.md CHANGED
@@ -32,7 +32,7 @@
32
32
  각 항목은 stable string `id`와 보이는 `label`을 가진다(Collection 기본 계약의 최소
33
33
  부분집합, [[collection]] 참고). `textValue`, `none|single|multiple` selection mode,
34
34
  `idle|loading|loadingMore|empty|error` async 상태는 Steps와 같은 이유로 가져오지 않는다 —
35
- Timeline은 검색/타이핑 탐색 대상이 아니고, 사용자가 고르는 대상도 아니다. 비동기 로딩은
35
+ Timeline은 검색/타이핑 탐색 대상이 아니고, 기본적으로 사용자가 고르는 대상도 아니다(선택형은 아래 「기록 선택」). 비동기 로딩은
36
36
  이 계약이 직접 다루지 않는다 — 항목이 뒤로(과거로) 더 늘어나야 하면 기존 항목을 그대로
37
37
  둔 채 다음 페이지만 요청하는 문제이므로, List가 그렇듯 `LoadMore`([[load-more]])와
38
38
  합성한다. Timeline 자체가 `idle|loading|empty` 축을 새로 만들지 않는다 — 항목이 하나도
@@ -143,3 +143,20 @@ Ant Design Timeline의 `mode="alternate"`(항목이 좌우로 번갈아 배치)
143
143
  connector 시각이 아니라 평평한 `AppListRow` 목록으로 그려져 있어, 시각 recipe 쪽
144
144
  vertical slice는 이 조사에서 확인하지 못했다. 구단 상세의 영입·유출 이력도 미확인 후보였다.
145
145
  2026-09-29부터 제품 채택은 관측으로 분리하며 [현재 승격 기준](stable-promotion.md)을 따른다.
146
+
147
+ ## 기록 선택 (2026-10-23, 개발 소스·미게시)
148
+
149
+ `selectedId`/`onSelectedIdChange`는 기록 하나를 골라 제품의 다른 표시(지도·상세)를 바꾸는 제어형 단일 선택이다.
150
+ 위의 "사용자가 고르는 대상이 아니다"는 수동 기록(`selectedId` 없음)의 기본값으로 남는다. 선택은 진행 커서가 아니다 —
151
+ `tone`·connector 색·순서는 선택과 무관하고, 선택된 기록 앞뒤를 지난/남은 단계로 나누지 않는다(그건 Steps).
152
+
153
+ - 계약: `TimelineDescriptor.selectedId`(`Id | null`), 해석 결과의 `selected`, 목록 밖 id는 `RangeError`.
154
+ `validateTimelineSelection`이 `selectedId`와 콜백의 짝, 선택형에서 `renderActions` 금지를 검사한다.
155
+ 선택형 기록은 하나의 대상(Web option, Native 선택 버튼)이라 안에 별도 명령을 둘 수 없다.
156
+ - 키보드: `getTimelineSelectionTarget`이 공용 collection 이동 계산(`getCollectionNavigationTarget`, 순환 없음)을 재사용한다.
157
+ 기록에는 처음과 끝이 있어 순환하지 않는다. 선택이 포커스를 따르는 이유는 선택이 옆 표시만 바꾸는 가벼운 동작이기 때문이다.
158
+ - 표현: `timelineRecipe.item.selected` — `selection.background` 면 + `selection.border` 1 테두리. ListRow 선택과 같은
159
+ 중립 선택 면이며 브랜드 면이 아니다(theme-palette 선택 상태 기준).
160
+ - 기존 API로 표현할 수 없었던 이유: `renderActions`의 Button은 기록과 별도 대상이라 선택 의미가 기록에 붙지 않고,
161
+ [선택 지침](usage/SELECTION.md#조립-규칙필수)이 `selected` Button 나열을 금지한다. RadioGroup·ListRow로 바꾸면
162
+ 레일·시각·기록 의미를 제품이 다시 조립해야 한다(다에리 여정 일지 감사).
package/docs/top.md CHANGED
@@ -37,3 +37,10 @@
37
37
  2026-10-07 소비 감사에서 두 renderer의 제목 크기·행간·굵기가 foundation으로 확정된 recipe를 직접 읽어 프로필을 우회했다. `large`는 가까운 프로필의 `heading.level3`, `medium`은 `typography.titleLarge`를 읽고 프로필이 없으면 기존 recipe를 유지한다. 시각 역할은 `headingLevel`과 독립이며 display 서체·제목 의미·보조 행동·초점은 바꾸지 않는다.
38
38
 
39
39
  프로필 연결은 1.15.0 이후 미게시 변경이며, 기본 배포 계약과 실제 Native 기기 검증은 구분한다.
40
+
41
+ ## 제목 줄바꿈 (2026-10-23, 개발 소스·미게시)
42
+
43
+ `titleLineBreaks`는 제목 안 `\n`의 의미다. 기본 `collapse`는 공백으로 바꾸고, `preserve`는 제품이 의도한 줄바꿈을 그린다.
44
+ `resolveTopTitle`이 두 renderer의 그릴 글자와 제목 이름을 함께 만든다. 이름은 언제나 줄을 공백 하나로 이은 한 문장이라
45
+ 줄바꿈이 보조기기의 제목을 끊지 않는다. 이전에는 같은 `\n`을 Web은 공백으로, Native는 줄바꿈으로 그렸다.
46
+ 포트폴리오 사이트 히어로가 `pre-line` span을 Heading에 넣어야 해서 Top을 채택하지 못한 것이 계기다([사용 지침](usage/components/top.md)).
@@ -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.40.0의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
10
+ API 안정성은 1.41.2의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
11
11
  Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품 반영을 의미하지 않는다.
12
12
  `분류`는 Storybook 제목 `<배포|실험>/<단계>/<분류>/<항목>`의 셋째 마디이고, 토큰·구성·화면은 Storybook 메뉴와 같은 순서다.
13
13
 
@@ -181,7 +181,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
181
181
  | [Splitter](components/splitter.md) | 레이아웃 | 넓은 Web 화면에서 두 영역의 경계를 사용자가 드래그나 키보드로 옮겨 크기를 정할 때 쓴다. | 1.12.1 | Web | Web stable / Native unsupported | 배포 |
182
182
  | [Stack](components/stack.md) | 레이아웃 | 자식들을 한 방향으로 늘어놓고 사이 간격을 토큰으로 맞출 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
183
183
  | [Statistic](components/statistic.md) | 데이터 표시 | 라벨이 붙은 **숫자 지표 하나**(또는 여러 개)를 보여 줄 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
184
- | [Steps](components/steps.md) | 탐색 | 여러 단계로 된 **선형 흐름에서 지금 어디인지** 보여 줄 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
184
+ | [Steps](components/steps.md) | 탐색 | 여러 단계로 된 **선형 흐름에서 지금 어디인지** 보여 줄 때 쓴다. | 미게시(1.41.2 이후) | Web · Native | Web stable / Native stable | 배포 |
185
185
  | [StreamingAnswer](components/streaming-answer.md) | 구성/피드백과 복구 | AI 응답처럼 제공자가 조각(`delta`)으로 보내는 답을 받는 대로 표시하고, 생성·다시 생성·중지를 한 묶음으로 제공한다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
186
186
  | [Surface](components/surface.md) | 레이아웃 | 배경·테두리·radius를 가진 **의미 없는 상자**가 필요할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
187
187
  | [SwipeActions](components/swipe-actions.md) | 구성/직접 조작과 모션 | 목록 행 하나에 붙은 **삭제·보관 같은 행 단위 행동**을, Native에서는 행을 밀어 드러내고 Web에서는 행 아래 버튼으로 바로 보여 줄 때 쓴다. | 1.12.1 | Web · Native | 개별 API 계약 참조 | 배포 |
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-06
7
+ - 검토일: 2026-10-23
8
8
  - 근거: [Affix — Web 상단 고정](../../affix.md), `affixRecipe`(`src/affix.ts`)
9
9
  - 스토리북: `배포/컴포넌트/기반 기능/고정 배치`
10
10
 
@@ -50,16 +50,18 @@ Native 사용 예는 없다(renderer 없음).
50
50
  | `offset` | 유한한 0 이상의 CSS px | `0` | 스크롤 조상 상단에서 띄울 거리. 아니면 `TypeError` |
51
51
  | `disabled` | `boolean` | `false` | 고정을 끄고 일반 흐름으로 둔다. 자식은 재마운트되지 않는다 |
52
52
  | `onChange` | `(affixed: boolean) => void` | — | 최초 측정 결과와 이후 붙음↔풀림 전환 때만 호출된다(스크롤마다 호출하지 않는다) |
53
- | `layoutStyle` | margin·width·flex·`alignSelf` | — | sticky 상자의 배치 전용. `position`·`top`은 Affix가 소유한다 |
53
+ | `layer` | `sticky` · `base` | `sticky` | 쌓임 단계. `sticky`는 공통 `layer.sticky`(100, `--hjm-layer-sticky`)로 뒤에 오는 position 지정 콘텐츠 위에 그린다. `base`(0)는 자체 쌓임을 가진 표면(대화상자·측면 패널 본문) 안에서만 쓴다(개발 소스, 미게시) |
54
+ | `fill` | `canvas` · `none` | `canvas` | 상자 뒤 바탕. `canvas`는 화면 배경(`--hjm-color-bg`)을 깔아 지나가는 콘텐츠가 비치지 않게 한다. `none`은 자식이 자기 표면(Card·채운 도구 막대)을 가질 때 쓴다(개발 소스, 미게시) |
55
+ | `layoutStyle` | margin·width·flex·`alignSelf` | — | sticky 상자의 배치 전용. `position`·`top`·`zIndex`는 Affix가 소유한다 |
54
56
  | `children` | `ReactNode` | 필수 | 고정할 영역 |
55
57
 
56
58
  ## 배치
57
59
 
58
60
  | 항목 | 값 | 근거 |
59
61
  | --- | --- | --- |
60
- | 크기 | 자식이 정한다. Affix는 여백·높이를 더하지 않고 화면 배경(`--hjm-color-bg`)만 깔아 아래 내용을 가린다 | `.hjm-affix` |
62
+ | 크기 | 자식이 정한다. Affix는 여백·높이를 더하지 않고 `fill="canvas"`(기본)면 화면 배경(`--hjm-color-bg`)만 깔아 아래 내용을 가린다 | `.hjm-affix` |
61
63
  | 간격 | 스크롤 조상 상단에서 `offset`만큼 띄운다. 상단 바가 있는 화면은 그 높이를 준다(스토리는 `offset={8}`) | `react/src/affix.tsx`, `showcase/web/src/patterns/WebAdditions.stories.tsx` |
62
- | 순서·정렬 | 고정하려는 영역이 원래 있던 자리(DOM 순서)에 그대로 둔다. z-index를 지정하지 않으므로 뒤에 오는 positioned 요소와 겹치면 바깥 wrapper에서 쌓임 순서를 정한다(`layer.sticky` 100) | `react/src/affix.tsx`, `foundations.ts` `layer` |
64
+ | 순서·정렬 | 고정하려는 영역이 원래 있던 자리(DOM 순서)에 그대로 둔다. 쌓임은 `layer`가 정한다: 기본 `sticky`(100)라 뒤에 오는 position 지정 카드·이미지가 고정된 막대 위로 그려지지 않는다. 제품이 바깥 wrapper에 z-index를 따로 주지 않는다 | `affixRecipe.defaults`, `foundations.ts` `layer`, `.hjm-affix[data-layer]` |
63
65
  | 고정·스크롤 | 상단 sticky만 된다. 고정 구간은 부모 높이 안에서만 생기고 부모가 끝나면 함께 밀려 올라간다. 한 스크롤 영역에 Affix를 여러 개 두지 않는다 | `affixRecipe`(`edge: "top"`, `position: "sticky"`) |
64
66
  | 좁은 폭·큰 글자 | 자식이 스크롤 영역 높이 − `offset`보다 크면 고정하지 않는다(`data-oversize`). 좁은 폭·큰 글자에서는 자식을 한 줄 요약으로 줄인다 | `affixRecipe.oversize: "flow"` |
65
67
 
@@ -84,3 +86,15 @@ Native 사용 예는 없다(renderer 없음).
84
86
 
85
87
  - 콘텐츠가 스크롤 영역 높이에서 `offset`을 뺀 값보다 크면 고정이 풀리고 일반 흐름으로 돌아간다
86
88
  (`data-oversize`). 고정이 안 된다고 보이면 콘텐츠 높이부터 확인한다.
89
+
90
+ ## 근거
91
+
92
+ - `layer`·`fill`(2026-10-23 소비 감사): Affix가 z-index를 두지 않아 뒤에 오는 position 지정 내용이 고정된 탭 막대 위로 그려질 수 있었고,
93
+ `layoutStyle`로도 줄 수 없었다. 그래서 포트폴리오 사이트가 `.tabs-bar { position: sticky; z-index: 2; background: var(--hjm-color-bg) }`를
94
+ 따로 유지했다. 숫자 z-index 대신 공통 `layer` 단계 이름으로 받고 기본을 BottomCTA·FAB와 같은 `sticky`로 둔다. 바탕 칠은 기존 동작(`canvas`)을
95
+ 기본값으로 이름 붙이고 자식이 표면을 가질 때만 `none`을 고른다.
96
+
97
+ ```tsx
98
+ // 포트폴리오 사이트 탭 막대 이관 예: sticky·z-index·배경 CSS를 지운다
99
+ <Affix><Container size="content" gutter="regular"><Tabs … /></Container></Affix>
100
+ ```
@@ -29,6 +29,7 @@
29
29
  | --- | --- | --- | --- |
30
30
  | `AlertDialog` | 기본 | `@hjmds/react`, `/overlays` | `@hjmds/react-native`, `/overlays` |
31
31
  | `AlertDialogRequest` 타입 | 보조(문구·행동 descriptor) | `@hjmds/design-contracts/components/alert-dialog` | 같음 |
32
+ | `AlertDialogQueueProvider`·`useAlertDialogQueue` | 동반(명령형 확인과 대기열, 개발 소스·미게시) | `@hjmds/react/alert-dialog-queue` | `@hjmds/react-native/alert-dialog-queue` |
32
33
 
33
34
  ## 최소 사용 예
34
35
 
@@ -151,6 +152,33 @@ import { AlertDialog } from "@hjmds/react-native/overlays";
151
152
  - Native에서 매우 긴 확인 문구는 본문이 스크롤되도록 바뀌었지만 큰 글자 실기기 검증은 끝나지 않았다([Dialog](../../dialog.md)).
152
153
  문구를 자르거나 글자 크기 상한으로 피하지 않는다.
153
154
 
155
+ ### 명령형 확인과 대기열 — `useAlertDialogQueue` (개발 소스·미게시)
156
+
157
+ 화면 곳곳에서 "지울까요?"를 묻고 결과를 기다려야 하면 대화상자 상태를 화면마다 두지 않고 앱 루트(라우터 안)에 공급자를 한 번 둔다.
158
+ 제품의 AppDialog 대기열·결과 Promise·route scope 구현을 이것으로 바꾼다(2026-10-23 번뚝 웹·앱).
159
+
160
+ ```tsx
161
+ // 루트: route가 바뀌면 이전 화면의 질문은 interrupted로 끝난다.
162
+ <AlertDialogQueueProvider scope={pathname}>{children}</AlertDialogQueueProvider>
163
+
164
+ // 화면
165
+ const { open } = useAlertDialogQueue();
166
+ const result = await open({ mode: "confirm", tone: "danger", title: t("delete.title"), description: t("delete.body"),
167
+ confirmLabel: t("delete"), cancelLabel: t("cancel"), onConfirm: () => remove(id), fallbackErrorMessage: t("delete.failed") });
168
+ if (result.outcome === "confirmed") showDone();
169
+ ```
170
+
171
+ | 항목 | 값 | 설명 |
172
+ | --- | --- | --- |
173
+ | `open(request, options?)` | `Promise<AlertDialogResult>` | 앞선 질문이 끝난 뒤 순서대로 뜬다. `{ outcome: "confirmed" }` 또는 `{ outcome: "cancelled", reason }`(`cancel-action`·`escape`·`back`·`programmatic`·`interrupted`) |
174
+ | `options` | Web `{ icon?, size? }`, Native `{ size?, returnFocusRef? }` | 요청별 표현. 문구·행동은 `request` |
175
+ | 공급자 `scope` | string | 질문의 주인(보통 route). 바뀌면 다른 scope 질문이 화면에서 내려가고 `interrupted` |
176
+ | 공급자 `modalPriority`·`portalContainer`(Web) | number · HTMLElement | 대기열 대화상자의 층(기본 0)과 portal. 필수 gate는 더 높은 우선순위 모달로 둔다 |
177
+
178
+ - `request.onConfirm`이 Promise면 처리 중에는 닫히지 않고, 실패하면 열린 채 `fallbackErrorMessage`를 보이며 `open`은 성공하거나 취소될 때까지 기다린다.
179
+ - 대화상자가 떠 있는 동안 trigger를 disabled로 두지 않는다. 배경은 이미 inert이고, disabled trigger로는 닫힌 뒤 초점이 돌아가지 못한다.
180
+ - 공급자 밖의 `useAlertDialogQueue`는 throw한다. 한 번에 하나의 Dialog·Sheet를 여는 `useDialog`/`useSheet`(`/overlay-stack`, Web)와는 별개다([계약](../../overlay-stack.md#결과를-돌려주는-확인-대기열--alertdialogqueueprovider-2026-10-23-개발-소스미게시)).
181
+
154
182
  ### 외부 확인 잠금 (checkout, 미게시)
155
183
 
156
184
  `confirmDisabled`(기본 false)는 요청 제한 시간 등 제품이 판단한 확인 불가 조건이다. Web·Native 모두 확인 행동만 차단하며 취소는 유지한다. 비동기 처리 중 중복 실행·닫힘·실패 상태는 기존 세션이 계속 소유한다. 잠금을 해제해도 열린 세션과 실패 안내를 초기화하지 않는다. Spint의 내 글 내리기 재시도 제한을 공통 확인창에서도 보존하기 위해 추가했다.
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-07
7
+ - 검토일: 2026-10-23
8
8
  - 근거: [Avatar fallback and Blobatar](../../avatar-fallback.md), recipe `avatarRecipe`(`src/component-recipes.ts`)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/아바타` · `배포/컴포넌트/데이터 표시/블로바타 캐릭터` · `배포/컴포넌트/데이터 표시/움직이는 블로바타 캐릭터`
10
10
 
@@ -67,14 +67,17 @@ import { Avatar } from "@hjmds/react-native/data-display";
67
67
 
68
68
  | prop | 값 | 기본값 | 설명 |
69
69
  | --- | --- | --- | --- |
70
- | `name` | `string` | 필수 | 이니셜과 기본 접근성 이름의 원천. 공백뿐이면 `TypeError` |
70
+ | `name` | `string` | 의미 있는 아바타는 필수 | 이니셜과 기본 접근성 이름의 원천. 공백뿐이면 `TypeError`. `decorative`이면 생략할 수 있다(이니셜 없이 액자·대체 그림만) |
71
+ | `decorative` | `boolean` | `false` | 옆에 이름이 보이는 작성자 표시나 사람 없는 장식. Web은 루트 `aria-hidden`·`img alt=""`이고 `alt`를 함께 줄 수 없다. Native는 `accessibilityLabel`을 함께 줄 수 없다 |
71
72
  | Web `src` · Native `source` | `string` · `ImageSourcePropType` | — | 사진이 없으면 prop을 뺀다. 주소(Native는 source 내용)가 바뀌면 실패 상태를 지우고 다시 시도한다 |
72
- | Web `size` | `small`(32) · `medium`(40) · `large`(48) · `xlarge`(64) | `medium` | — |
73
- | Web `shape` | `circle` · `rounded` | `circle` | — |
74
- | Native `size` | 숫자(pt) | 44 | 24 미만이면 `RangeError`. 모양은 항상 원이다 |
73
+ | `size` | `xxsmall`(22) · `xsmall`(28) · `small`(32) · `medium`(40) · `touch`(44) · `large`(48) · `xlarge`(64). Native는 기존 숫자(pt)도 받는다 | Web `medium` · Native 44(`touch`) | `avatarRecipe.sizes`. Native 숫자가 22 미만이면 `RangeError`(2026-10-23 전에는 24). 새 코드는 단계 이름을 쓴다 |
74
+ | `shape` | `circle` · `rounded` | `circle` | `rounded`는 `radius.md`(가장 가까운 디자인 프로필) |
75
+ | `frame` | `outlined` · `surface` · `none` | `outlined` | `avatarRecipe.frames`. `outlined`=canvas 바탕+1px 테두리(기존), `surface`=바탕만, `none`=바탕·테두리 없음(제품 그림이 액자를 채울 때) |
76
+ | `accessory` · `accessoryPlacement` | `ReactNode` · `bottom-end` · `top-end` | — · `bottom-end` | 팀 배지·상태 점 같은 모서리 표식. 아바타 상자 모서리(`avatarRecipe.accessory.offset` 0, Asset·IconTile처럼 바깥으로 나가는 거리)에 붙고 액자에 잘리지 않는다. 누르는 요소를 넣지 않는다 |
75
77
  | `renderFallback` | `(context: { size: number; decorative: true }) => ReactNode` | — | 사진이 없거나 실패했을 때만 불린다. `size`는 px/pt 값. null·undefined를 돌려주면 기본 대체 표시(Web `fallback`→이니셜, Native 이니셜)를 쓴다 |
76
78
  | Web `alt` | `string` | `name` | `""`이면 보조기기에서 숨긴다 |
77
- | Web `imageProps` | `img` 속성(`alt`·`src` 제외) | — | `onError`는 `(event: SyntheticEvent<HTMLImageElement>) => void`이며 대체 표시로 바꾼 뒤 불린다 |
79
+ | Web `imageProps` | `img` 속성(`alt`·`src` 제외) | — | `onError`는 `(event: SyntheticEvent<HTMLImageElement>) => void`이며 대체 표시로 바꾼 뒤 불린다. `renderImage`와 함께 주면 `TypeError` |
80
+ | Web `renderImage` | `(props: AvatarImageRenderProps) => ReactNode` | 기본 `img` | `src`, `alt`(장식이면 `""`), `size`(px), `className`(`hjm-avatar__image`, 그대로 붙인다), 세대가 보호된 `onError`. `next/image` 등 제품 이미지 호스트를 연결한다(2026-10-23) |
78
81
  | Native `accessibilityLabel` · `decorative` | `{ accessibilityLabel: string }` 또는 `{ decorative: true }` | — | 둘 중 하나가 타입으로 강제된다 |
79
82
  | Native `renderImage` | `(context: AvatarImageRenderProps) => ReactNode` | 기본 Native Image | 제품의 이미지 캐시·표시 호스트를 연결한다(1.14.0 추가). `source`, `size`, HJM `fallback`, 세대가 보호된 `onError` 제공 |
80
83
  | Native `initials` | `string` | 이름에서 계산 | 앞뒤 공백을 지우고 최대 3자, 대문자 |
@@ -85,7 +88,7 @@ import { Avatar } from "@hjmds/react-native/data-display";
85
88
 
86
89
  | 항목 | 값 | 근거 |
87
90
  | --- | --- | --- |
88
- | 크기 | Web `small` 32 · `medium` 40 · `large` 48 · `xlarge` 64, Native 숫자(기본 44). 목록 행에는 Web `medium`/Native 기본, 프로필 머리에는 `xlarge`. Avatar 자체는 터치 대상이 아니므로 누르는 프로필은 감싸는 행·버튼이 최소 44를 확보한다 | `.hjm-avatar[data-size]`, `react-native/src/data-display.tsx` |
91
+ | 크기 | `xxsmall` 22 · `xsmall` 28 · `small` 32 · `medium` 40 · `touch` 44 · `large` 48 · `xlarge` 64. 댓글·칩 안 작은 작성자 표시는 `xxsmall`/`xsmall`, 목록 행에는 Web `medium`/Native 기본, 프로필 머리에는 `xlarge`. 감싼 요소 크기(`h-8` 등)로 맞추지 말고 단계를 고른다. Avatar 자체는 터치 대상이 아니므로 누르는 프로필은 감싸는 행·버튼이 최소 44를 확보한다 | `.hjm-avatar[data-size]`, `react-native/src/data-display.tsx` |
89
92
  | 간격 | 자체 바깥 여백이 없다. `AvatarGroup`은 크기의 30%만큼 겹치고 각 아바타에 `bg` 색 2px 테두리를 둘러 경계를 만든다 | `.hjm-avatar-group` |
90
93
  | 순서·정렬 | 행·카드·댓글 머리의 시작 쪽에 두고 이름·본문이 끝 쪽으로 이어진다. `AvatarGroup`의 넘친 수 표시는 맨 끝에 온다 | `.hjm-avatar-group__overflow` |
91
94
  | 고정·스크롤 | 고정 영역이 없다 | — |
@@ -93,6 +96,9 @@ import { Avatar } from "@hjmds/react-native/data-display";
93
96
 
94
97
  ## 꼭 지킬 것
95
98
 
99
+ - 장식인지 먼저 정한다. 같은 줄에 이름이 보이면 `decorative`로 숨기고, 아바타만 사람을 나타내면 `name`(Web)·`accessibilityLabel`(Native)을 준다.
100
+ - 크기·모양·테두리·모서리 표식을 감싸는 요소의 className·절대 좌표로 다시 만들지 않는다. `size`·`shape`·`frame`·`accessory`를 쓴다.
101
+
96
102
  - `name`은 빈 문자열이면 `TypeError`다. 접근성 이름은 Avatar가 소유하고, 대체 그림은 장식으로만 그린다(버튼 등 상호작용 금지).
97
103
  - Blobatar `seed`에는 공개 제품 식별자를 쓴다. 이름·이메일·자격증명은 seed로 쓰지 않는다.
98
104
  - 사진·표시 이름·남은 인원 문구는 제품 소유다. `overflow`는 i18n 키로 만든다. `overflow`는 보조기기에서 숨겨지므로 전체 인원은 `label` 문구에 넣는다.
@@ -103,9 +109,10 @@ import { Avatar } from "@hjmds/react-native/data-display";
103
109
  | 항목 | Web | Native |
104
110
  | --- | --- | --- |
105
111
  | 사진 | `src`(문자열), `imageProps`로 `img` 속성·`onError` 전달 | `source`(`ImageSourcePropType`). `imageStyle`은 제거된 속성 — layoutStyle 또는 `size` |
106
- | 접근성 이름 | `alt`(기본 `name`), `alt=""`이면 숨김 | `accessibilityLabel` 필수, 또는 `decorative` |
112
+ | 접근성 이름 | `alt`(기본 `name`), `alt=""`이면 숨김, `decorative`면 루트까지 숨김 | `accessibilityLabel` 필수, 또는 `decorative` |
107
113
  | 대체 표시 | `fallback` 노드 또는 이니셜 | `initials`(최대 3자) 또는 이니셜 |
108
- | 크기·모양 축 | 4단 이름 · `circle`/`rounded` | 숫자 · 원만 |
114
+ | 크기·모양 축 | 단계 이름 · `circle`/`rounded` | 단계 이름 또는 숫자 · `circle`/`rounded` |
115
+ | 이미지 호스트 | `renderImage`(img 대신) 또는 `imageProps` | `renderImage`(HJM `fallback`도 전달) |
109
116
  | 묶음 | `AvatarGroup` | 없음 |
110
117
 
111
118
  2026-10-07 실제 Showcase 비교에서 `AvatarGroup`을 `ListRow.leading`에 넣으면 단일 이미지용
@@ -146,3 +153,17 @@ A→B→A로 되돌아와도 무시한다. source를 같은 값으로 재생성
146
153
  기본 Native Image 경로는 그대로다. Web은 img/imageProps 경로를 사용하며 src가 바뀌면 요청 img 노드를 교체해 이전 DOM 오류가 새 사진의 대체 표시를 켜지 않도록 한다.
147
154
 
148
155
  Web animated Blobatar는 IntersectionObserver를 사용할 수 없으면 같은 seed·표정의 정적 그림을 유지한다. 장식의 관찰 기능 미지원 때문에 Avatar 본문을 실패시키지 않는다.
156
+
157
+ ### 작은 장식 아바타와 모서리 표식 (2026-10-23)
158
+
159
+ 번뚝 `UserAvatar`(22·28·32·44, 투명 원 안의 생성 SVG)와 야잘알 `PlayerAvatar`(둥근 사각형, 모서리 팀 배지)가
160
+ Avatar를 쓰지 못한 이유(이름 필수, Web 이미지 호스트 없음, 4단 크기, Native 24 하한, 강제 테두리·바탕)를 공개 축으로 옮겼다.
161
+
162
+ ```tsx
163
+ // Web: 작성자 이름 옆의 장식 아바타, 제품 SVG가 액자를 채운다
164
+ <Avatar decorative size="xsmall" frame="none" {...(svgUrl ? { src: svgUrl } : {})} fallback={emoji} />
165
+
166
+ // Native: 둥근 사각형 선수 사진 + 모서리 팀 배지
167
+ <Avatar name={player.name} accessibilityLabel={player.name} size="large" shape="rounded"
168
+ {...(uri ? { source: { uri } } : {})} accessory={<TeamBadge team={team} />} />
169
+ ```
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.12.1
7
- - 검토일: 2026-10-18
7
+ - 검토일: 2026-10-23
8
8
  - 근거: recipe `badgeRecipe`(`src/component-recipes.ts`)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/배지`
10
10
 
@@ -43,11 +43,25 @@ import { Badge } from "@hjmds/react-native/data-display";
43
43
  <Badge tone="success" size="small" label={t("order.status.done")} />
44
44
  ```
45
45
 
46
- 왼쪽 → 라벨 → 오른쪽 순서로 배치한다. 아이콘 콜백은 배지 글자와 같은 `color`와 `glyph.xs`(14) `size`를 받는다. Native는 실제 테마 색을, Web은 `currentColor`를 전달한다. 이 콜백 API는 미게시다. 아이콘은 장식이며 별도 누르기 동작을 넣지 않는다.
46
+ 왼쪽 → 라벨 → 오른쪽 순서로 배치한다. 아이콘 콜백은 배지 글자와 같은 `color`, `glyph.xs`(14) `size`, Icon `regular`와 같은 선 굵기 `strokeWidth`(2, `badgeRecipe.iconStrokeWidth`)를 받는다. 제품 글리프 렌더러(Lucide 등)에 셋을 그대로 넘기고 Icon 노드를 따로 만들지 않는다(strokeWidth는 개발 소스, 미게시). Native는 실제 테마 색을, Web은 `currentColor`를 전달한다. 이 콜백 API는 미게시다. 아이콘은 장식이며 별도 누르기 동작을 넣지 않는다.
47
47
 
48
48
  ```tsx
49
49
  // Web
50
- <Badge tone="success" leading={({ color, size }) => <Trophy size={size} color={color} />} trailing={({ color, size }) => <Check size={size} color={color} />}>완료</Badge>
50
+ <Badge tone="success" leading={({ color, size, strokeWidth }) => <Trophy size={size} color={color} strokeWidth={strokeWidth} />} trailing={({ color, size }) => <Check size={size} color={color} />}>완료</Badge>
51
+ ```
52
+
53
+ ### 제품 분류 색 — 개발 소스, 미게시
54
+
55
+ 경기 시리즈·FA 등급처럼 **상태도 브랜드도 아닌 제품 분류**는 `tone` 대신 `categoryColor`에 테마별 색을 넣는다.
56
+ 제품이 색을 정하고, HJM이 틀·글자 굵기·간격과 글자색을 정한다. `filled`는 `onStatusFill`(흰색)과 어두운 글자
57
+ (`badgeRecipe.category.darkInk`, 기본 light `text`) 중 대비가 높은 쪽을 고르고, `outline`은 그 색을 글자·테두리로 쓴다.
58
+ 어느 쪽도 WCAG AA 4.5:1에 못 미치면(outline은 `bg`·`surface` 모두) `RangeError`를 던진다. 어두운 테마에서 읽히지 않는 색은
59
+ `dark` 값을 따로 준다. `tone`과 함께 주면 `TypeError`다.
60
+
61
+ ```tsx
62
+ // Native
63
+ const seriesColor = { light: "#216BA8", dark: "#216BA8" } as const; // 제품 분류 정의
64
+ <Badge size="small" label={t("series.regular")} categoryColor={seriesColor} />
51
65
  ```
52
66
 
53
67
  ## 축과 기본값
@@ -55,6 +69,7 @@ import { Badge } from "@hjmds/react-native/data-display";
55
69
  | prop | 값 | 기본값 | 설명 |
56
70
  | --- | --- | --- | --- |
57
71
  | `tone` | `neutral` · `strong` · `brand` · `info` · `success` · `warning` · `attention` · `danger` | `neutral` | 단색 배경과 흰색 굵은 글자를 사용한다. `neutral`은 회색, `strong`은 더 진한 회색, 나머지는 각 의미의 solid fill이다. `outline`은 투명 배경과 의미색 글자다 |
72
+ | `categoryColor` | `{ light: string; dark: string }`(6자리 hex) | — | `tone` 대신 쓰는 제품 분류 색. 글자색은 HJM이 고르고 AA를 검사한다(위 절). 개발 소스, 미게시 |
58
73
  | `variant` | `filled` · `outline` | `filled` | — |
59
74
  | `size` | `small` · `medium` | `medium` | — |
60
75
  | `leading` · `trailing` | `ReactNode \| (context: BadgeAdornmentContext) => ReactNode` | — | 라벨 왼쪽·오른쪽 아이콘. 양쪽 동시 사용 가능하며 보조기기에서 숨겨진다 |
@@ -62,7 +77,7 @@ import { Badge } from "@hjmds/react-native/data-display";
62
77
  | Native `accessibilityLabel` | `string` | `String(label)` | 숫자 라벨에 단위를 붙여 읽힐 때 쓴다 |
63
78
  | `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native `style`·`labelStyle`은 제거된 속성 — layoutStyle 또는 tone/토큰 |
64
79
 
65
- 행동 콜백은 없다. 아이콘 렌더 콜백은 색과 크기만 받으며 배지는 누를 수 없는 표시다.
80
+ 행동 콜백은 없다. 아이콘 렌더 콜백은 색·크기·선 굵기만 받으며 배지는 누를 수 없는 표시다.
66
81
 
67
82
  상태 채움의 글자·아이콘은 `onStatusFill`, 브랜드 채움은 `onPrimary`, 위험 채움은 `onDanger`를 쓴다. 브랜드의 전경색이 어두워도 상태 배지의 대비를 독립적으로 유지하기 위한 역할 분리다(미게시).
68
83
 
@@ -81,7 +96,7 @@ Web·Native 모두 `radius.sm`(8)의 둥근 사각형을 사용한다. 2026-10-1
81
96
  ## 꼭 지킬 것
82
97
 
83
98
  - 라벨은 i18n 키로 넣고 짧게 둔다. 상태 뜻을 색에만 싣지 않는다(글자가 뜻을 말해야 한다).
84
- - 색은 `tone`으로만 고른다. 브랜드 색을 `style`·`className`·`labelStyle`로 덮지 않는다.
99
+ - 색은 `tone`으로 고르고, 제품 분류 색만 `categoryColor`로 넣는다. 브랜드 색을 `style`·`className`·`labelStyle`로 덮거나 배지 하나를 `brandPalette` Provider로 감싸지 않는다(2026-10-23 야잘알 GameSeriesBadge·FA 등급 감사: Provider 중첩은 그 범위의 모든 토큰을 바꾼다).
85
100
  - 배치는 Web·Native 모두 `layoutStyle`로만 한다. Native `style`·`labelStyle`은 제거된 속성이다.
86
101
 
87
102
  ## 플랫폼 차이
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.21.0
7
- - 검토일: 2026-10-15
7
+ - 검토일: 2026-10-23
8
8
  - 근거: [Chart](../../chart.md), `src/chart.ts`(`chartRecipe`, `describeChart`, `resolveLineGeometry`), `src/dataviz.ts`(`resolveDatavizSeriesColor`, `datavizChromeTokens`)
9
9
  - 스토리북: `배포/컴포넌트/데이터 표시/차트`
10
10
 
@@ -181,6 +181,7 @@ import { BarChart, Sparkline } from "@hjmds/react-native/chart";
181
181
  | `width`·`height`(`Sparkline`) | `number` | 72 · 28 | `chartRecipe.sparkline` |
182
182
  | `accessibilityLabel`(`Sparkline`) | `string` | — | 없으면 장식(보조 기술에서 숨김) |
183
183
  | `items`(`ChartLegend`) | `{ id, label, color, value?: string }[]` | — (필수) | `value`는 서식이 끝난 문자열 |
184
+ | `layout`(`ChartLegend`) | `stack` · `inline` | Web: `onSelect` 없으면 `inline`, 있으면 `stack` · Native: `stack` | `stack`은 계열마다 한 행(Native 최소 높이 `chartRecipe.legend.layouts.stack.rowMinHeight` 32, 값은 끝 정렬). `inline`은 항목을 한 줄로 흘리고 넘치면 다음 줄로 감는다(항목 사이 `spacing.md` 16, 줄 사이 `spacing.xxs` 4, 견본·이름·값 사이 `spacing.xs` 8). 지도·차트 아래 짧은 범례에 쓴다(개발 소스, 미게시) |
184
185
 
185
186
  ## 배치
186
187
 
@@ -284,3 +285,20 @@ Native centerContent에는 Text 등 Native 노드를 쓴다. 모두 제외하거
284
285
  2026-10-21 포트폴리오의 성긴 일별 관측이 끊겨 읽기 어렵다는 사용자 요청으로 `bridge`를 추가했다. 실제 관측선과 결측 연결 안내선을 구분하며 0 채움·수치 보간은 수행하지 않는다.
285
286
 
286
287
  2026-10-23 오늘야구 선수 최근 7시즌 화면에서 앞쪽 결측의 경계 점선이 빠진 문제를 수정했다(개발 소스, 미게시). 앞·뒤 결측도 실제 관측값 위치에서 하단까지 표시하며, 관측값이 전혀 없으면 점선을 만들지 않는다.
288
+
289
+ 2026-10-23 소비 감사: 선택의창 지도 하단의 한 줄 범례(`RouteMapScreen`의 PlaceDot + caption 묶음)는 Native ChartLegend가
290
+ 행을 세로로만 쌓아 채택하지 못했다. `layout="inline"`으로 항목을 가로로 흘려 감고, 견본·간격·흐린 정도는
291
+ `chartRecipe.legend`가 소유한다. 알약 모양 바탕은 범례가 아니라 지도 위에 띄우는 제품 구성이므로 `Surface radius="full"`로
292
+ 감싼다. Web의 기본값(선택 버튼이 없으면 가로)은 1.41 동작 그대로이며, Native 기본값은 기존 소비 화면(야잘알 StockChart)을
293
+ 바꾸지 않도록 `stack`으로 둔다. 두 플랫폼 기본값의 차이는 기존 동작이며 통일은 별도 결정이다.
294
+
295
+ ```tsx
296
+ // Native: 선택의창 지도 범례
297
+ <Surface tone="raised" radius="full" padding="xs">
298
+ <ChartLegend layout="inline" items={[
299
+ { id: "left", label: t("mapLegendLeft"), color: mapColors.leftExposure },
300
+ { id: "right", label: t("mapLegendRight"), color: mapColors.rightExposure },
301
+ { id: "neutral", label: t("mapLegendNeutral"), color: mapColors.neutralExposure },
302
+ ]} />
303
+ </Surface>
304
+ ```
@@ -69,6 +69,7 @@ import { CheckboxGroup } from "@hjmds/react-native/inputs";
69
69
  | `label` · `accessibilityLabel` | `string` | — | 둘 중 하나는 필수 |
70
70
  | `orientation` | `vertical` · `horizontal` | `vertical` | — |
71
71
  | `presentation` | `card` · `plain` · `grouped` · `chips` | `card` | — |
72
+ | `overflow` | `wrap` · `scroll` | `wrap` | 미게시. `chips`의 줄 넘침. `scroll`은 한 줄 가로 스크롤([칩 한 줄 스크롤](#칩-한-줄-스크롤-미게시)). `chips`가 아니거나 `grid`와 함께 쓰면 `TypeError` |
72
73
  | `size` | `medium` · `small` | `medium` | — |
73
74
  | `renderLeading` | Web `(item, appearance: { selected, color: "currentColor", size }) => ReactNode` · Native `(item, props: { checked, selected, disabled, readOnly, color, size }) => ReactNode` | — | 항목 아이콘 |
74
75
  | `layoutStyle` | margin·width·flex·`alignSelf` | — | 그룹 외곽(Web `fieldset`) 배치 |
@@ -80,7 +81,7 @@ import { CheckboxGroup } from "@hjmds/react-native/inputs";
80
81
  | 크기 | 폭을 채우는 묶음. 각 행은 [Checkbox](checkbox.md) 규격(최소 높이 `medium` 56 · `small` 44, 표시 24 · 20) | `selectionControlRecipe.sizes`, `.hjm-choice` |
81
82
  | 간격 | 그룹 이름(legend)↔항목 `spacing.xs` 8. 항목 사이 세로 `card` `spacing.xs` 8 · `plain` `spacing.xxs` 4 · `grouped` 0, 가로 `card` `spacing.md` 16 · `plain` `spacing.sm` 12 · `grouped` 0. 설명·오류는 `formSupportContract.gap` `spacing.xs` 8 | `selectionGroupRecipe.orientations`, `.hjm-checkbox-group*` |
82
83
  | 순서·정렬 | 위→아래 [그룹 이름(medium 500)] → [설명] → [항목들] → [오류]. 항목은 시작 쪽 정렬. `grouped`는 항목들을 테두리 1px·`radius.lg` 16 카드 하나에 붙여 담는다 | `selectionGroupRecipe.slots`, `.hjm-checkbox-group[data-presentation="grouped"]` |
83
- | 고정·스크롤 | 고정 영역이 없다. 목록이 길어도 그룹 안에서 스크롤 영역을 만들지 않는다 | `.hjm-checkbox-group__items` |
84
+ | 고정·스크롤 | 고정 영역이 없다. 세로 목록은 그룹 안에서 스크롤 영역을 만들지 않는다. `chips` + `overflow="scroll"`만 가로 한 줄 스크롤이다 | `.hjm-checkbox-group__items`, `.hjm-checkbox-group__scroller` |
84
85
  | 좁은 폭·큰 글자 | `horizontal`은 줄바꿈된다(`flex-wrap: wrap`). 좁은 폭·큰 글자에서는 `vertical`을 쓴다 | `.hjm-checkbox-group[data-orientation="horizontal"]` |
85
86
 
86
87
  ## 꼭 지킬 것
@@ -136,3 +137,25 @@ Chip/직접 wrap으로 조립하고 있었다. `presentation="chips"`는 기존
136
137
  ### 같은 행의 카드 높이 (checkout 수정·미게시)
137
138
 
138
139
  `presentation="card"` 항목은 같은 Grid 행에서 가장 높은 콘텐츠에 맞춰 칸을 채운다. Native의 Grid 칸 안에서 카드가 내용 높이만 사용하던 문제를 공통 ChoiceRow에서 수정했다(2026-10-20 다에리 공개 범위 선택 카드 지적). 제품에서 높이를 고정하거나 설명을 줄여 맞추지 않는다. 행별 높이는 해당 행의 콘텐츠로 결정한다.
140
+
141
+ ### 칩 한 줄 스크롤 (미게시)
142
+
143
+ `presentation="chips" overflow="scroll"`은 여러 개를 고르는 칩을 한 줄에 두고 가로로 스크롤한다.
144
+ 단일 선택 [SegmentedControl](segmented-control.md)의 `overflow="scroll"`과 같은 뜻이며, 다중 선택 의미(체크박스·`Set` 값)는 그대로다.
145
+
146
+ ```tsx
147
+ <CheckboxGroup accessibilityLabel={t("filter.teams")} presentation="chips" size="small" overflow="scroll"
148
+ items={teams.map((team) => ({ id: team, label: team }))} value={selected} onValueChange={setSelected}
149
+ renderLeading={(item) => <TeamBadge team={item.id} size="small" />} />
150
+ ```
151
+
152
+ | 항목 | 값 | 근거 |
153
+ | --- | --- | --- |
154
+ | 줄 | 줄바꿈 없이 한 줄, 칩 사이 `spacing.xs` 8. 칩은 줄어들지 않는다 | `selectionGroupRecipe.orientations.*.gap.chips` |
155
+ | 끝 여백 | 스크롤 영역 안쪽 사방 `selectionGroupRecipe.overflow.scrollInset` `spacing.xxs` 4, 바깥은 같은 값만큼 당겨 첫 칩이 열 가장자리에 맞는다. 마지막 칩 뒤에도 같은 4가 남는다 | `.hjm-checkbox-group__scroller`, Native `ScrollView` padding/margin |
156
+ | 키보드·포커스 | Web은 Tab으로 칩마다 이동하고 Space로 켜고 끈다. 포커스한 칩이 가려져 있으면 최소 거리만 스크롤해 inset까지 보인다. 4는 포커스 테두리(2 + 간격 2)와 작은 칩의 터치 여유(4)가 잘리지 않는 값이다 | `resolveScrollRevealOffset`(behaviors) |
157
+ | 처음 위치 | 시작 쪽에서 시작한다. 선택을 바꿔도 스크롤하지 않는다(여러 개를 켜는 동안 줄이 튀지 않게) | — |
158
+
159
+ SegmentedControl의 스크롤 노출 계산도 같은 `resolveScrollRevealOffset`(inset 0)을 쓴다.
160
+ 근거: 2026-10-23 야잘알 `TeamGameFilter.tsx`가 SegmentedControl(단일 선택)에만 있는 가로 스크롤을 쓰지 못해
161
+ 다중 선택 칩을 제품 `ScrollView`로 직접 감쌌다(`gap: 6`, `paddingRight: spacing.xs`).
@@ -14,6 +14,20 @@
14
14
  누르면 시트에서 고른다. 저장이 비동기면 시트가 저장 중으로 잠기고, 실패하면 시트를 연 채 오류를 보여 준다.
15
15
  행에는 제품이 저장을 확인한 값만 보이므로, 실패한 선택이 저장된 것처럼 남지 않는다.
16
16
 
17
+ ### 설정 값 선택의 기준
18
+
19
+ 2026-10-23 정리: ChoiceRow 지침은 화면 테마를 ChoiceRow 용도로 적었지만 SettingsScreen 지침과 두 플랫폼의 앱 설정 예제는
20
+ 테마·언어를 `Select` 또는 `ListRow`+`Sheet`+`RadioGroup`으로 안내했다. 그래서 선택의창은 ChoiceRow, Unairplane은 Sheet+RadioGroup을 써서
21
+ 같은 설정이 앱마다 갈렸다. ChoiceRow는 바로 이 반복(언어·테마 행을 제품마다 조립, 1.41.0 도입 근거)을 없애려고 만든 API이므로 기준을 하나로 둔다.
22
+
23
+ | 설정 | 쓸 것 |
24
+ | --- | --- |
25
+ | 설정 화면의 즉시 저장 한 값 선택(언어·화면 테마·기본 정렬·셸 테마) | ChoiceRow. 항목마다 그림이 필요하면 `ChoiceRowItem.leading` |
26
+ | 폼 안에서 다른 필드와 함께 제출하는 한 값 | [Select](select.md) 또는 [RadioGroup](radio-group.md) |
27
+ | 선택 외 입력·설명이 많은 편집 패널 | [Sheet](sheet.md)를 직접 구성 |
28
+
29
+ `ListRow`+`Sheet`+`RadioGroup`으로 같은 행과 시트를 다시 조립하지 않는다. 행의 확인된 값, 시트의 선택·저장 중·실패 유지는 ChoiceRow가 소유한다.
30
+
17
31
  ## 쓰지 않을 때
18
32
 
19
33
  | 상황 | 대신 쓸 것 |
@@ -53,11 +67,11 @@ import { ChoiceRow } from "@hjmds/react-native/choice-row";
53
67
  | prop | 값 | 기본값 | 설명 |
54
68
  | --- | --- | --- | --- |
55
69
  | `value` | 선택지 값 또는 null | 필수 | 제품이 확인한 값. null이면 `placeholder`가 필요하다 |
56
- | `items` | `ChoiceRowItem[]` | 필수 | 2개 이상, 값 중복 금지. `description`·`disabled` 지원 |
70
+ | `items` | `ChoiceRowItem[]` | 필수 | 2개 이상, 값 중복 금지. `description`·`disabled`·`leading`(항목 그림, 개발 소스·미게시) 지원 |
57
71
  | `onValueChange` | `(value) => void` 또는 `(value) => Promise<void>` | 필수 | 동기 저장은 시트를 바로 닫는다. Promise를 돌려주면 끝날 때까지 시트가 잠기고, 거부되면 시트를 연 채 `errorLabel`을 보인다 |
58
72
  | `errorLabel` | string | 비동기 저장일 때 필수 | 동기 저장에는 줄 수 없다. 시트 안 실패가 없기 때문이다(1.41.2) |
59
73
  | `sheetTitle`·`sheetDescription` | string | `title`·없음 | 시트 머리 |
60
- | `leading`·`trailing` | ReactNode | 없음 | 제품 아이콘·꼬리 표시. ListRow 슬롯 그대로 |
74
+ | `leading`·`trailing` | ReactNode | `leading`은 확인된 항목의 `leading`, `trailing`은 없음 | 제품 아이콘·꼬리 표시. ListRow 슬롯 그대로. 행 `leading`을 주면 항목 그림보다 우선한다 |
61
75
  | `density` | ListRow density | ListRow 기본값 | 주변 설정 행과 같은 밀도(예: `compact`)를 맞춘다 |
62
76
  | Native `accessibilityHint` | string | 없음 | 행을 눌렀을 때 일어날 일(예: 언어 목록 열기)을 제품 언어로 |
63
77
  | `disabled`·`loading` | boolean | false | ListRow의 비활성·자리 유지 로딩 |
@@ -66,9 +80,9 @@ import { ChoiceRow } from "@hjmds/react-native/choice-row";
66
80
 
67
81
  | 항목 | 값 | 근거 |
68
82
  | --- | --- | --- |
69
- | 크기 | ListRow 기본 높이와 터치 영역. 시트는 내용 높이(`size="auto"`) | [ListRow](list-row.md#배치), [Sheet](sheet.md#배치) |
83
+ | 크기 | ListRow 기본 높이와 터치 영역. 시트는 내용 높이(`size="auto"`). 항목 그림은 `choiceRowRecipe.itemLeading` 40×40 상자 가운데에 놓이고 넘치면 잘린다(ListRow leading과 같은 크기) | [ListRow](list-row.md#배치), [Sheet](sheet.md#배치), `choiceRowRecipe` |
70
84
  | 간격 | ListRow·RadioGroup recipe 간격을 그대로 쓴다 | 같은 recipe |
71
- | 순서·정렬 | 행: leading → 제목·현재 값(description) → trailing. 시트: 제목 → 설명 → 세로 선택지 → 오류 | [설정 화면](../screens/common-settings.md#버튼과-행동-위치) |
85
+ | 순서·정렬 | 행: leading → 제목·현재 값(description) → trailing. 시트: 제목 → 설명 → 세로 선택지(선택 표시 → 그림 → 이름·설명) → 오류. 그림이 있는 항목이 하나라도 있으면 그림 없는 항목도 빈 상자를 둬 이름 시작선을 맞춘다 | [설정 화면](../screens/common-settings.md#버튼과-행동-위치) |
72
86
  | 고정·스크롤 | 행은 바깥 화면 스크롤을 따른다. 선택지가 길면 시트 본문이 스크롤된다 | Sheet |
73
87
  | 좁은 폭·큰 글자 | 제목과 현재 값이 줄바꿈되며 행 높이가 늘어난다 | ListRow |
74
88
 
@@ -79,3 +93,4 @@ import { ChoiceRow } from "@hjmds/react-native/choice-row";
79
93
  - 저장 중에는 시트를 닫거나 다른 값을 고를 수 없다. 다시 고르면 이전 오류는 지워진다.
80
94
  - 행은 시트가 열렸는지를 낭독한다(Web `aria-expanded`·`aria-haspopup="dialog"`, Native `expanded`). 시트는 머리를 고정하고 선택지만 스크롤한다(1.41.1, utilverse 언어 행 이관에서 확인).
81
95
  - 문구·선택지·저장은 제품이 공급한다. 언어 목록이나 테마 적용 로직은 이 컴포넌트에 없다.
96
+ - 항목 그림(`leading`)은 장식이다. 보조기술은 항목 이름(`label`)만 읽으므로 그림에만 있는 정보를 두지 않는다. 크기·정렬을 제품 style로 다시 맞추지 않는다(2026-10-23 유틸버스 셸 테마 그림 때문에 ChoiceRow 대신 `ListRow`+`Sheet`+`RadioGroup`을 조립했던 사례).
@@ -4,7 +4,7 @@
4
4
  - 상태: 배포
5
5
  - 지원: Web · Native
6
6
  - 적용: 1.23.0
7
- - 검토일: 2026-10-20
7
+ - 검토일: 2026-10-23
8
8
  - 근거: 양 renderer `src/screens.tsx`, [소유권](../SELECTION.md#소유권). 제품별 제목·배지·상태·하단 영역 재조립을 줄이기 위해 기존 Stack·상태 계약을 합성한다.
9
9
  - 스토리북: `배포/컴포넌트/레이아웃/구성 골격`, `배포/화면/설정/알림 설정`
10
10
 
@@ -62,6 +62,7 @@ import { CompositionLayout } from "@hjmds/react-native/screens";
62
62
  | footer | ReactNode | 없음 | 본문 다음 보조/주 행동; 고정은 바깥 ScreenLayout 소유 |
63
63
  | state | ScreenContentState | ready | 본문 교체; loading은 Spinner만 표시. empty 제목은 body(14), 오류·접근 제한 제목은 title |
64
64
  | stateAction | ReactNode | 없음 | 본문 상태와 중앙 정렬되는 복구 행동 |
65
+ | stateHeight | fit · compact · regular · large | fit | loading·empty·error·restricted가 본문을 대신할 때 그 자리에서 확보하는 최소 높이(`screenPatternRecipe.compositionState.minHeight`: compact 96 · regular 160 · large 220, fit은 상태 자체 높이). 부모가 높이를 주지 않아도 상태가 그 영역 가운데에 온다. ready 본문에는 적용하지 않는다. 개발 소스, 미게시 |
65
66
  | layoutStyle | HjmCompositionStyleProp | 없음 | 부모가 제공하는 영역 크기·배치 |
66
67
 
67
68
  ## 배치
@@ -75,7 +76,7 @@ footer
75
76
 
76
77
  | 항목 | 값 | 근거 |
77
78
  | --- | --- | --- |
78
- | 크기 | 부모의 가용 폭·높이 | 별도 고정 높이나 스크롤 없음 |
79
+ | 크기 | 부모의 가용 폭·높이. 상태 교체 영역만 `stateHeight` 단계의 최소 높이를 가진다 | 별도 고정 높이나 스크롤 없음 |
79
80
  | 제목·상태 문구 | 제목은 두 플랫폼 모두 `Heading level5`(문서 단계는 `headingLevel`). 상태 블록은 제목(`Text title`)·설명이 블록 줄로 가운데 정렬된다 | renderer. 2026-10-17 화면 검토: Native 제목이 작은 `Text title`이었고 Web 상태 문구가 한 줄로 이어져 왼쪽 정렬됐다 |
80
81
  | 간격 | 영역 spacing.md 16, 제목 행 spacing.sm 12, 배지 spacing.xs 8, 제목/설명 spacing.xxs 4 | Stack props |
81
82
  | 순서·정렬 | 머리글 → notice → 본문 → footer | renderer |
@@ -87,6 +88,7 @@ footer
87
88
  - 데이터·권한·초안·저장·배지 의미는 제품에서 연결한다. 전송 성공을 추정하거나 초안을 자동으로 지우지 않는다.
88
89
  - 본문 교체는 초기 조회에, 기존 입력을 유지하는 실패는 notice에 연결한다.
89
90
  - 중앙 상태는 이 구성에 주어진 영역을 기준으로 한다. 화면 높이를 임의 여백으로 흉내 내지 않는다.
91
+ - 부모 높이가 없는 구획(카드·패널·탭 본문)에서 상태 자리를 확보할 때는 `stateHeight`를 쓴다. `<View style={{ minHeight: N }}>`로 감싸고 `layoutStyle={{ flex: 1 }}`을 주는 조립을 하지 않는다. 단계 선택: 레일·인라인 저장 오류·공유 카드 행은 `compact`, 목록·표 구획 본문은 `regular`, 필드·차트 패널은 `large`, 한 줄 인라인 로딩은 `fit`.
90
92
  - 별도 스크롤을 중첩하지 않는다. 기본 구조를 children 안에서 다시 만들지 않는다.
91
93
 
92
94
  ## 플랫폼 차이
@@ -101,4 +103,6 @@ footer
101
103
  2026-10-19 사용자 지시: 앱·모바일 웹뷰의 큰 글씨를 지양하므로 구획 제목은 두 renderer 모두 18(level5)을 기본으로 쓴다.
102
104
  headingLevel은 문서 구조만 결정한다. 기존 24는 상한이며 일반 카드·선택 영역 제목의 기본값이 아니다.
103
105
 
106
+ `stateHeight` 근거: 2026-10-23 소비 감사. 야잘알이 QueryRegion·LeadersRail·MatchFieldPanel 등 13곳 이상에서 상태 영역을 `minHeight` 36·68·88·96·120·160·180·220 View로 감쌌다. 이 값들을 세 단계(68–96 → 96, 120–180 → 160, 220)로 묶어 recipe가 소유한다.
107
+
104
108
  `aside`는 제목·본문 묶음의 끝 쪽에 놓이고 footer는 카드 전체 폭을 사용한다. 지원 그림 자체의 크기·브랜드는 제품이 결정하며, 위치·본문의 가변 폭·footer 줄바꿈은 이 구성이 소유한다.
@@ -72,13 +72,13 @@ import { CounterBadge } from "@hjmds/react-native/data-display";
72
72
  | --- | --- | --- |
73
73
  | 크기 | `medium` 높이·최소 폭 20, `small` 높이·최소 폭 16, `dot` 8×8, 모두 `radius.full`. 자릿수가 늘면 폭만 늘어난다(`99+`). 터치 대상이 아니므로 누르는 것은 부모다 | `src/counter-badge-recipe.ts`, `packages/react/src/styles.css` `.hjm-counter-badge` |
74
74
  | 간격 | 좌우 여백 `medium` `spacing.xs` 8, `small` `spacing.xxs` 4. `floating` 테두리 `stroke.strong` 2가 아이콘과 배지를 떼어 보인다 | `src/counter-badge-recipe.ts` |
75
- | 순서·정렬 | `inline`: 탭 라벨·목록 행의 끝 쪽에 텍스트와 나란히 둔다. `floating`: 컴포넌트가 스스로 위치를 잡지 않으므로 부모를 relative로 두고 배지를 위·끝 모서리(`top: 0`, 끝 0)에 absolute로 놓는다. 공개된 `NotificationBell`(`/notification-bell`)이 IconButton 위에 이 배치를 그대로 쓴다 | `packages/react/src/notification-bell.tsx`, `packages/react-native/src/notification-bell.tsx` |
75
+ | 순서·정렬 | `inline`: 탭 라벨·목록 행의 끝 쪽에 텍스트와 나란히 둔다. `floating`: 컴포넌트가 스스로 위치를 잡지 않으므로 부모를 relative로 두고 배지를 위·끝 모서리에 absolute로 놓는다. 아이콘 위에 둘 때는 숫자 판이 아이콘을 덮지 않게 올린다. 공개된 `NotificationBell`(`/notification-bell`)은 `iconButtonRecipe.badge`(`small`, 끝 0, 아이콘 칸에 테두리 2만 겹치는 높이)를 쓴다 | `packages/react/src/notification-bell.tsx`, `packages/react-native/src/notification-bell.tsx` |
76
76
  | 고정·스크롤 | — | — |
77
77
  | 좁은 폭·큰 글자 | Web은 `flex-shrink: 0`이라 좁아져도 줄지 않는다 | `packages/react/src/styles.css` `.hjm-counter-badge` |
78
78
 
79
79
  ```text
80
80
  inline (행 끝) floating (아이콘 모서리)
81
- ┌─────────────────────────────┐ ┌──────┐(3) ← top 0, 끝 0
81
+ ┌─────────────────────────────┐ ┌──────┐(3) ← 끝 0, 테두리만 아이콘에 닿게 위로
82
82
  │ 받은 편지함 (12) │ │ 🔔 │
83
83
  └─────────────────────────────┘ └──────┘ IconButton 44×44
84
84
  ```
@@ -12,7 +12,7 @@
12
12
 
13
13
  앱 루트에 한 번 둔다. theme(light/dark/system)·방향·글자 배율·reduced motion을 해석하고, 제품 브랜드색을
14
14
  HJM semantic token 위에 얹는 **유일한 진입점**이다. Native HJM 컴포넌트는 테마를 이 Provider에서 읽으므로
15
- Provider 없이 렌더하면 예외가 난다. 화면 일부의 밀도·테마만 바꿀 때는 중첩 Provider를 둔다.
15
+ Provider 없이 렌더하면 예외가 난다. 화면 일부의 밀도·테마만 바꿀 때는 중첩 Provider를 둔다. 화면 일부의 바탕·표면 같은 몇 색 역할만 바꿀 때는 중첩 Provider에 그 역할만 `brandPalette`로 넘기고 `brandPaletteMode="extend"`를 준다. 제품에서 상위 덮어쓰기를 누적하는 scope 컴포넌트를 만들지 않는다.
16
16
 
17
17
  ## 쓰지 않을 때
18
18
 
@@ -86,6 +86,7 @@ expect(checkBrandPaletteContrast(PRODUCT_BRAND_PALETTE)).toEqual({ light: [], da
86
86
  | `reducedMotion` | `true` · `false` | `false` | — |
87
87
  | `minimumVisualTarget` | `true` · `false` | `false` | 작은 컨트롤의 표시 높이를 최소 44로 올리는 선택 기능. 2026-10-21 사용자 지시로 포트폴리오 앱·웹은 `false`를 사용한다. 터치 확장 `hitSlop`과 별개다 |
88
88
  | `brandPalette` | `{ light?, dark? }` | — | 각각 `ThemeColors` 20개 key 중 필요한 것만 넘긴다(부분 병합). 상태 강조색은 덮을 수 없다. 중첩 Provider는 가장 가까운 상위의 값을 물려받는다 |
89
+ | `brandPaletteMode` | `"replace"` · `"extend"` | `"replace"` | 중첩 Provider에서 `brandPalette`가 상위 팔레트를 대체할지(`replace`), theme별로 상위 위에 넘긴 역할만 덮을지(`extend`) 정한다. `brandPalette` 없이는 효과 없음(개발 소스·미게시, [브랜드 경계 §1.1](../../brand-boundary.md#11-하위-범위의-일부-역할만-바꾸기--brandpalettemode-개발-소스미게시)) |
89
90
  | `designProfile` | `HjmDesignProfile` | 가장 가까운 상위 프로필 또는 없음 | 기본 프로필·표면 질감은 1.15.0 추가. `hjmDesignPresets` 또는 `defineHjmDesignProfile` 결과만 넣는다. 글자 역할·관련 구성 개선은 1.16.0 변경. [프로필 계약](../../design-profile.md)의 토큰·질감·전환·구성·화면 기본값을 상속한다 |
90
91
  | `value` | `DesignSystemProviderValue`(`resolveDesignSystemProviderValue` 결과) | — | 테스트·스토리·임베딩용. 환경 prop·`brandPalette`·`designProfile`과 함께 쓸 수 없고(타입이 막는다), 주면 OS theme·모션 관찰과 상위 `brandPalette` 상속이 멈춘다 |
91
92
  | `surfaceEffects`(Native) | `{ renderBackdrop?, insetShadows? }` | 상위 host 또는 없음 | 1.15.0 실험 API. 실제 blur host·inset capability를 한 번 등록하며 `{}`로 하위 영역을 해제. [Surface 질감 계약](../../design-profile.md#surfacecard의-유리클레이-질감) |