@hjmds/design-contracts 1.29.0 → 1.31.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.
- package/dist/behaviors.d.ts +1 -0
- package/dist/behaviors.d.ts.map +1 -1
- package/dist/bottom-navigation.d.ts +24 -0
- package/dist/bottom-navigation.d.ts.map +1 -1
- package/dist/bottom-navigation.js +34 -0
- package/dist/bottom-navigation.js.map +1 -1
- package/dist/carousel.d.ts +30 -0
- package/dist/carousel.d.ts.map +1 -1
- package/dist/carousel.js +32 -0
- package/dist/carousel.js.map +1 -1
- package/dist/catalog.d.ts +382 -64
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +147 -131
- package/dist/catalog.js.map +1 -1
- package/dist/component-definitions.d.ts +21 -131
- package/dist/component-definitions.d.ts.map +1 -1
- package/dist/component-definitions.js +9 -132
- package/dist/component-definitions.js.map +1 -1
- package/dist/component-recipes.d.ts +87 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +44 -1
- package/dist/component-recipes.js.map +1 -1
- package/dist/icon.d.ts +58 -1
- package/dist/icon.d.ts.map +1 -1
- package/dist/icon.js +69 -0
- package/dist/icon.js.map +1 -1
- package/dist/image-choice.d.ts +7 -0
- package/dist/image-choice.d.ts.map +1 -1
- package/dist/image-choice.js +7 -0
- package/dist/image-choice.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/recipes.d.ts +1 -1
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js.map +1 -1
- package/dist/screen-patterns.d.ts +100 -0
- package/dist/screen-patterns.d.ts.map +1 -1
- package/dist/screen-patterns.js +98 -3
- package/dist/screen-patterns.js.map +1 -1
- package/dist/showcase.d.ts.map +1 -1
- package/dist/showcase.js +10 -0
- package/dist/showcase.js.map +1 -1
- package/dist/steps.d.ts +16 -7
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +6 -1
- package/dist/steps.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/architecture.md +18 -0
- package/docs/bottom-navigation.md +10 -0
- package/docs/carousel.md +8 -0
- package/docs/dialog.md +23 -0
- package/docs/generated/component-maturity.md +32 -29
- package/docs/generated/renderer-evidence.json +1551 -1118
- package/docs/generated/renderer-evidence.md +243 -237
- package/docs/generated/showcase-manifest.json +688 -69
- package/docs/icon.md +9 -0
- package/docs/screen-patterns.md +18 -0
- package/docs/stable-core.md +22 -0
- package/docs/steps.md +4 -3
- package/docs/usage/README.md +28 -25
- package/docs/usage/SELECTION.md +1 -1
- package/docs/usage/components/auth-screen-layout.md +15 -5
- package/docs/usage/components/bottom-navigation.md +65 -2
- package/docs/usage/components/card.md +36 -5
- package/docs/usage/components/carousel.md +1 -0
- package/docs/usage/components/chat-date-separator.md +86 -0
- package/docs/usage/components/chat-message.md +10 -1
- package/docs/usage/components/chat-screen.md +3 -2
- package/docs/usage/components/comment-thread-row.md +11 -3
- package/docs/usage/components/comment-thread-screen.md +1 -1
- package/docs/usage/components/dialog.md +69 -0
- package/docs/usage/components/editor-screen.md +6 -2
- package/docs/usage/components/field.md +13 -0
- package/docs/usage/components/icon-button.md +1 -0
- package/docs/usage/components/icon-tile.md +10 -1
- package/docs/usage/components/icon.md +45 -6
- package/docs/usage/components/image-choice-group.md +3 -1
- package/docs/usage/components/media-selection-screen.md +17 -0
- package/docs/usage/components/message-composer.md +4 -1
- package/docs/usage/components/moderation-screen.md +88 -2
- package/docs/usage/components/page-indicator.md +102 -0
- package/docs/usage/components/profile-screen.md +9 -3
- package/docs/usage/components/profile-summary.md +3 -3
- package/docs/usage/components/screen-layout.md +9 -0
- package/docs/usage/components/steps.md +14 -7
- package/docs/usage/components/tabs.md +21 -1
- package/docs/usage/components/text.md +30 -1
- package/docs/usage/components/typing-indicator.md +89 -0
- package/docs/usage/screens/common-login.md +3 -1
- package/docs/usage/screens/common-profile.md +3 -3
- package/docs/usage/screens/flow-editor.md +13 -2
- package/docs/usage/screens/flow-moderation.md +31 -3
- package/package.json +2 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.17.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-18
|
|
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
|
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
게시물·댓글·사용자 **신고** 화면에 쓴다. 사유 선택, 사유를 고르기 전까지 막힌 신고 버튼,
|
|
14
14
|
선택적인 차단 버튼과 그 확인 대화상자를 [ScreenLayout](screen-layout.md) 위에 조합한다.
|
|
15
|
+
운영자가 신고·이의 신청을 처리하는 화면은 `variant="review"`(1.31.0), 편지·카드·Sheet 안에서 펼치는 신고는
|
|
16
|
+
화면 틀 없는 `ModerationForm`(1.31.0)이다.
|
|
15
17
|
|
|
16
18
|
## 쓰지 않을 때
|
|
17
19
|
|
|
@@ -20,12 +22,16 @@
|
|
|
20
22
|
| 차단·삭제 확인만 필요 | [AlertDialog](alert-dialog.md) |
|
|
21
23
|
| 신고 진입 메뉴(⋯) | [Menu](menu.md) |
|
|
22
24
|
| 자유 입력 위주의 문의 폼 | [Form](form.md), [EditorScreen](editor-screen.md) |
|
|
25
|
+
| 편지·카드·Sheet 안 신고에 ModerationScreen을 넣고 `contentInset="none"`으로 틀을 끄기 | `ModerationForm` |
|
|
26
|
+
| 운영자 처리 화면을 TextArea·Button·Alert로 직접 조립 | `variant="review"` |
|
|
23
27
|
|
|
24
28
|
## 공개 이름과 import
|
|
25
29
|
|
|
26
30
|
| 이름 | 역할 | Web | Native |
|
|
27
31
|
| --- | --- | --- | --- |
|
|
28
32
|
| `ModerationScreen` | supplemental, 루트 barrel에 없음 | `/screen-flows` | `/screen-flows` |
|
|
33
|
+
| `ModerationForm` | companion(1.31.0). 화면 틀 없는 같은 폼 | `/screen-flows` | `/screen-flows` |
|
|
34
|
+
| `ModerationDecision`·`ModerationDecisionReason`·`ModerationFields`·`ModerationFormProps` (타입) | 운영자 결정·처리 사유·두 변형의 공통 필드·폼 props | `/screen-flows` | `/screen-flows` |
|
|
29
35
|
|
|
30
36
|
`@hjmds/react/screen-flows`, `@hjmds/react-native/screen-flows`로만 import한다. 추가 optional peer는 없다.
|
|
31
37
|
|
|
@@ -97,13 +103,86 @@ import { ModerationScreen } from "@hjmds/react-native/screen-flows";
|
|
|
97
103
|
| `block` | 차단 버튼 행동과 `AlertDialog` 확인 요청(`mode: "confirm"`) |
|
|
98
104
|
| ScreenLayout props | `title`, `description`, `exit`(push·모달로 여는 신고 화면의 닫기/뒤로, 헤더 시작 끝), `header`, `actions`, `notice`, `state`, `stateAction` 등(`footer` 제외) |
|
|
99
105
|
|
|
106
|
+
### 운영자 검토 `variant="review"` (1.31.0)
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// Native(Web은 exit의 onPress 대신 onClick)
|
|
110
|
+
<ModerationScreen
|
|
111
|
+
variant="review"
|
|
112
|
+
title={t("moderation.review")}
|
|
113
|
+
exit={{ kind: "back", label: t("moderation.queue"), onPress: back }}
|
|
114
|
+
decisionReason={{ label: t("moderation.reason"), description: t("moderation.reasonHint"), value: reason, onValueChange: setReason, maxLength: 500 }}
|
|
115
|
+
decisions={[
|
|
116
|
+
{ id: "dismiss", label: t("moderation.dismiss"), pending: sending === "dismiss",
|
|
117
|
+
confirmation: { mode: "confirm", title: t("moderation.dismissConfirm"), description: t("moderation.dismissHint"),
|
|
118
|
+
confirmLabel: t("moderation.dismiss"), cancelLabel: t("common.cancel"), fallbackErrorMessage: t("common.error"), onConfirm: () => decide("dismiss") },
|
|
119
|
+
onAction: () => {} },
|
|
120
|
+
{ id: "hide", label: t("moderation.hide"), tone: "danger", pending: sending === "hide", disabled: !detail.available,
|
|
121
|
+
confirmation: { mode: "confirm", tone: "danger", title: t("moderation.hideConfirm"), description: t("moderation.hideHint"),
|
|
122
|
+
confirmLabel: t("moderation.hide"), cancelLabel: t("common.cancel"), fallbackErrorMessage: t("common.error"), onConfirm: () => decide("hide") },
|
|
123
|
+
onAction: () => {} },
|
|
124
|
+
]}
|
|
125
|
+
{...(detail.resolution ? { resolution: <Notice tone="success" title={t("moderation.hidden")} description={detail.resolutionReason} /> } : {})}
|
|
126
|
+
>
|
|
127
|
+
<ReportedContent detail={detail} />
|
|
128
|
+
</ModerationScreen>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| prop | 값 | 기본값 | 설명 |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| `variant` | `"report"` · `"review"` | `"report"` | `review`면 아래 props를 쓰고 신고 props(`reasons`·`reason`·`submit`·`block` 등)는 받지 않는다 |
|
|
134
|
+
| `children` | `ReactNode` | — | 신고된 내용·신고 사유 등 검토 대상(DescriptionList·Notice 등 HJM 구성으로) |
|
|
135
|
+
| `decisionReason` | `{ label, value, onValueChange, description?, maxLength?, required?, disabled? }` | `required` `true` | 처리 사유 TextArea(제어형). `required`면 공백뿐인 사유로는 어떤 결정도 열리지 않는다 |
|
|
136
|
+
| `decisions` | `ModerationDecision[]`(`ScreenFlowAction` + `{ id, tone?, confirmation? }`) | `tone` `"secondary"` | 결정 버튼, footer에 순서대로 세로로 쌓인다(`spacing.xs` 8). `tone`은 `primary`·`secondary`·`danger`. `confirmation`(AlertDialog `confirm`)이 있으면 누를 때 확인창만 열고 `onConfirm`이 실행하며 `onAction`은 부르지 않는다(`block`과 같은 규칙). 한 결정이 `pending`이면 다른 결정과 사유 입력이 잠긴다. 본문 `state`가 `ready`가 아니어도 잠긴다 |
|
|
137
|
+
| `resolution` | `ReactNode` | — | 이미 처리된 신고. 주면 사유 입력과 결정 버튼 자리를 이것이 대체한다(footer 없음) |
|
|
138
|
+
|
|
139
|
+
### ModerationForm: 화면 틀 없는 신고 폼
|
|
140
|
+
|
|
141
|
+
ModerationScreen과 같은 필드(`variant` 두 가지 모두)를 [CompositionLayout](composition-layout.md) 하나로 그린다: (제목) → 본문(사유 선택 또는 검토 대상·처리 사유) → 행동(세로 쌓기 `spacing.xs` 8). 화면 머리·바깥 여백·footer 표면·스크롤이 없어 편지·카드(Surface) 안이나 Sheet 본문에 놓는다. 감싸는 표면·위치·스크롤은 host 소유다.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
// Web — 편지 아래에 펼친 신고
|
|
145
|
+
import { ModerationForm } from "@hjmds/react/screen-flows";
|
|
146
|
+
|
|
147
|
+
<Surface tone="subtle" radius="lg" padding="lg">
|
|
148
|
+
<ModerationForm
|
|
149
|
+
title={t("report.title")}
|
|
150
|
+
reasons={reasons} reason={reason} onReasonChange={setReason} reasonLabel={t("report.reason")}
|
|
151
|
+
submit={{ label: t("report.confirm"), pending: busy, onAction: report }}
|
|
152
|
+
cancel={{ label: t("common.cancel"), disabled: busy, onAction: collapse }}
|
|
153
|
+
>
|
|
154
|
+
{error ? <Notice tone="danger" title={error} /> : null}
|
|
155
|
+
</ModerationForm>
|
|
156
|
+
</Surface>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
// Native — Sheet 안: Sheet가 제목과 닫기를 가지므로 title·cancel을 생략한다
|
|
161
|
+
import { ModerationForm } from "@hjmds/react-native/screen-flows";
|
|
162
|
+
|
|
163
|
+
<Sheet open={open} onOpenChange={setOpen} title={t("report.title")} closeLabel={t("common.close")}>
|
|
164
|
+
<ModerationForm reasons={reasons} reason={reason} onReasonChange={setReason} reasonLabel={t("report.reason")}
|
|
165
|
+
submit={{ label: t("report.confirm"), pending: busy, onAction: report }} />
|
|
166
|
+
</Sheet>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| prop | 값 | 기본값 | 설명 |
|
|
170
|
+
| --- | --- | --- | --- |
|
|
171
|
+
| 신고·검토 필드 | ModerationScreen과 같음 | — | `variant`·`reasons`…·`block` 또는 `decisionReason`·`decisions`·`resolution` |
|
|
172
|
+
| `title` | 문자열 | — | 구성 제목(`level3`, 문서 수준 `headingLevel` 기본 3). 제목 줄이 있는 Sheet·Dialog 안에서는 생략 |
|
|
173
|
+
| `headingLevel` | 1~6 | 3 | 제목의 문서 수준 |
|
|
174
|
+
| `cancel` | `ScreenFlowAction` | — | 행동 줄 맨 끝 ghost 버튼(펼친 신고 접기) |
|
|
175
|
+
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 바깥 배치 전용 |
|
|
176
|
+
|
|
177
|
+
화면 상태(`state`)가 없으므로 신고·결정 버튼은 사유·`disabled`·`pending`으로만 잠긴다.
|
|
178
|
+
|
|
100
179
|
## 배치
|
|
101
180
|
|
|
102
181
|
| 항목 | 값 | 근거 |
|
|
103
182
|
| --- | --- | --- |
|
|
104
183
|
| 크기 | ScreenLayout 폭(최대 720); 사유는 세로 `RadioGroup`, 신고·차단은 `Button` 기본 크기 | `ModerationScreen`, `screen-flows.tsx` `Action` |
|
|
105
184
|
| 간격 | 화면 padding `spacing.md` 16; 본문 사유 선택–`children` `spacing.lg` 20; footer 신고–차단 `spacing.xs` 8(세로로 쌓은 버튼, `actionGroupRecipe.stackedGap`) | Web·Native `ModerationScreen` `Stack gap="lg"`·`gap="xs"` |
|
|
106
|
-
| 순서·정렬 | 헤더(제목) → 본문(사유 → 추가 설명 `children`) → footer(신고 primary → 차단 ghost) | 렌더 순서 |
|
|
185
|
+
| 순서·정렬 | 헤더(제목) → 본문(사유 → 추가 설명 `children`) → footer(신고 primary → 차단 ghost). `review`: 본문(검토 대상 `children` → 처리 사유 TextArea 또는 `resolution`) → footer(결정 버튼, 주어진 순서). `ModerationForm`: (제목) → 본문 → 행동(… → `cancel` ghost) | 렌더 순서 |
|
|
107
186
|
| 고정·스크롤 | 헤더·footer 고정, 본문 화면 스크롤; 차단 확인은 AlertDialog 오버레이 | `ScreenLayout`, `AlertDialog` |
|
|
108
187
|
| 좁은 폭·큰 글자 | 사유 문구는 줄바꿈되고 항목이 세로로 늘어난다; footer 버튼은 세로로 쌓여 좁은 폭에서도 나란히 줄지 않는다 | `RadioGroup orientation="vertical"`, `Stack` |
|
|
109
188
|
|
|
@@ -114,4 +193,11 @@ import { ModerationScreen } from "@hjmds/react-native/screen-flows";
|
|
|
114
193
|
- 서버 신고·권한·차단 mutation, 완료 후 이동은 제품 소유다. 사유 목록과 정책 문구도 제품이 정한다.
|
|
115
194
|
- 신고 화면은 push·모달로 열리므로 `exit`로 나갈 길을 준다. 2026-10-17 화면 검토(S5)에서 exit 없는 예제가 나갈 수 없는 화면으로 렌더링됐다.
|
|
116
195
|
- 확인 대화상자의 성공 후 닫힘·실패 문구는 [AlertDialog](alert-dialog.md) 계약을 따른다.
|
|
196
|
+
- 운영자 결정의 문구·id·서버 결정 mutation은 제품 소유다. 결정이나 그 `confirmation`이 사라지거나 `resolution`이 오면 열린 확인창도 닫힌다.
|
|
197
|
+
- 편지·카드·Sheet 안 신고는 `ModerationForm`이다. ModerationScreen의 `contentInset`·`scroll`·`header`로 화면 틀을 지우지 않는다.
|
|
117
198
|
- `block`을 제거하면 열린 확인 세션도 종료한다. 나중에 차단 행동을 다시 공급해도 사용자가 버튼을 새로 눌러야 확인창이 열린다. 2026-10-13 사라진 행동의 확인 상태가 부활하던 경로를 수정했다. 대상이 바뀌는 제품은 대상 id를 화면 key/초안·요청 수명에 함께 연결한다.
|
|
199
|
+
|
|
200
|
+
## 근거
|
|
201
|
+
|
|
202
|
+
- `variant="review"`(1.31.0): 2026-10-18 앱 감사에서 utilverse 신고·이의 신청 처리 화면이 처리 사유 TextArea, 기각/숨기기 버튼 묶음, OS `Alert` 확인을 화면마다 직접 조립했다. 결정 문구는 큐마다 달라(기각·숨기기·유지·번복) props로 받고, 사유 필수·한 번에 한 결정·확인창 규칙은 HJM이 소유한다.
|
|
203
|
+
- `ModerationForm`(1.31.0): 같은 감사에서 diairy가 편지 안에 펼치는 신고에 ModerationScreen을 넣고 `contentInset="none"`·`scroll="content"`로 화면 틀을 끄면서도 머리 띠가 남았다(Dialog·Sheet는 자기 제목 줄을 가진다). 같은 필드·행동을 화면 틀 없이 그리는 companion으로 분리해 두 표현이 한 구현(`useModeration`)을 공유한다.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# PageIndicator
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 실험
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.30.0 이후)
|
|
7
|
+
- 검토일: 2026-10-18
|
|
8
|
+
- 근거: [Carousel](../../carousel.md), contract `src/carousel.ts`(`resolvePageIndicator`·`getPageIndicatorTarget`), recipe `carouselRecipe.dot`
|
|
9
|
+
- 스토리북: `실험/컴포넌트/탐색/페이지 위치 표시`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
제품이 직접 만든 가로 페이저(Native 페이징 `FlatList`, Web scroll-snap 줄) 아래에 지금 몇 번째 페이지인지 점으로 보여 주고,
|
|
14
|
+
점을 눌러 그 페이지로 옮길 때 쓴다. Carousel의 위치 점 줄과 같은 부품·같은 recipe다. 홈 화면 런처처럼 페이지 안의 내용과
|
|
15
|
+
스크롤을 제품이 소유해 Carousel에 넣을 수 없는 자리다.
|
|
16
|
+
|
|
17
|
+
## 쓰지 않을 때
|
|
18
|
+
|
|
19
|
+
| 상황 | 대신 쓸 것 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 카드 묶음을 HJM이 그리고 넘겨도 됨 | [Carousel](carousel.md)(점·이전/다음이 이미 들어 있다) |
|
|
22
|
+
| 번호가 있는 결과 페이지 이동 | [Pagination](pagination.md) |
|
|
23
|
+
| 같은 화면의 패널 전환 | [Tabs](tabs.md)(페이저와 함께 쓰면 `swipePosition`) |
|
|
24
|
+
| 진행 단계 | [Steps](steps.md) |
|
|
25
|
+
|
|
26
|
+
## 공개 이름과 import
|
|
27
|
+
|
|
28
|
+
| 이름 | 역할 | Web | Native |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `PageIndicator` | 기본 | `@hjmds/react`, `/carousel` | `@hjmds/react-native`, `/carousel` |
|
|
31
|
+
| `resolvePageIndicator`·`getPageIndicatorTarget` | 보조(계약 함수) | `@hjmds/design-contracts/components/carousel` | 같음 |
|
|
32
|
+
|
|
33
|
+
## 최소 사용 예
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Web
|
|
37
|
+
import { PageIndicator } from "@hjmds/react/carousel";
|
|
38
|
+
|
|
39
|
+
<PageIndicator
|
|
40
|
+
label={t("home.pages")}
|
|
41
|
+
count={pages.length}
|
|
42
|
+
currentIndex={page}
|
|
43
|
+
onCurrentIndexChange={(index) => scrollToPage(index)}
|
|
44
|
+
composeAccessibleName={({ position, total }) => t("home.pageName", { position, total })}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Native
|
|
50
|
+
import { PageIndicator } from "@hjmds/react-native/carousel";
|
|
51
|
+
|
|
52
|
+
<PageIndicator
|
|
53
|
+
label={t("homePages")}
|
|
54
|
+
count={pages.length}
|
|
55
|
+
currentIndex={selected}
|
|
56
|
+
onCurrentIndexChange={(index) => pager.current?.scrollToIndex({ index, animated: !reducedMotion })}
|
|
57
|
+
composeAccessibleName={({ position, total }) => t("homePageName", { position, total })}
|
|
58
|
+
labels={{ previous: t("previousPage"), next: t("nextPage") }}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 축과 기본값
|
|
63
|
+
|
|
64
|
+
| prop | 값 | 기본값 | 설명 |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `label` | `string` | 필수 | 페이지 묶음의 이름. Web은 group 이름, Native는 `label: 현재 페이지 이름`으로 읽힌다. 비면 `TypeError` |
|
|
67
|
+
| `count` | 1 이상 정수 | 필수 | 페이지 수. 밖이면 `RangeError` |
|
|
68
|
+
| `currentIndex` | 0부터 `count - 1` | 필수 | 페이저가 확정한 현재 페이지. 컴포넌트는 따로 기억하지 않는다 |
|
|
69
|
+
| `onCurrentIndexChange` | `(index: number) => void` | 필수 | 점 누름·Web 화살표 키·Native 조절 동작이 요청한 페이지. 처음·끝에서는 같은 값이 다시 올 수 있다(넘어가지 않고 멈춘다) |
|
|
70
|
+
| `composeAccessibleName` | `({ position, total }) => string` | 필수 | 점 하나의 이름. 숫자 문법은 제품 문구다. 빈 문자열은 `TypeError` |
|
|
71
|
+
| Native `labels` | `{ previous: string; next: string }` | 필수 | 조절(adjustable) 동작 이름 |
|
|
72
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | — | 배치 전용 |
|
|
73
|
+
|
|
74
|
+
## 배치
|
|
75
|
+
|
|
76
|
+
| 항목 | 값 | 근거 |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 크기 | 점 지름 8, 점마다 누름 영역 44×44(`control.minTouchTarget`). 현재 점 `content.brand`, 나머지 `border.strong` | `carouselRecipe.dot` |
|
|
79
|
+
| 간격 | Web 점 사이 `spacing.xs` 8(`.hjm-page-indicator`). Native는 44 누름 영역끼리 붙여 둔다(Carousel과 같다) | `styles.css`, `react-native/src/carousel.tsx` |
|
|
80
|
+
| 순서·정렬 | 점은 페이지 순서대로 가운데 정렬한다. RTL에서는 줄이 뒤집히고 Web 화살표 키도 읽는 방향을 따른다 | `PageIndicator` |
|
|
81
|
+
| 고정·스크롤 | 스크롤하지 않는다. 보통 페이저 바로 아래에 둔다 | — |
|
|
82
|
+
| 좁은 폭·큰 글자 | 점이 많으면 다음 줄로 넘어간다(`flex-wrap`). 글자가 없어 글자 배율의 영향이 없다 | `styles.css`, `carousel.tsx` |
|
|
83
|
+
|
|
84
|
+
## 꼭 지킬 것
|
|
85
|
+
|
|
86
|
+
- `currentIndex`는 페이저의 실제 위치로 넘긴다. 스크롤이 멈춘 페이지(예: `onScroll`의 반올림 index)를 그대로 쓴다.
|
|
87
|
+
- `onCurrentIndexChange`에서 페이저를 그 페이지로 옮긴다. 이 컴포넌트는 스크롤하지 않는다.
|
|
88
|
+
- 숨은 페이지의 내용은 제품 페이저가 접근성 트리에서 뺀다(`accessibilityElementsHidden`·`importantForAccessibility`).
|
|
89
|
+
- 점의 색·크기를 덮지 않는다. Carousel과 같은 recipe를 쓴다.
|
|
90
|
+
|
|
91
|
+
## 플랫폼 차이
|
|
92
|
+
|
|
93
|
+
| 항목 | Web | Native |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| 접근성 | `role="group"` 안의 점 버튼(현재 점 `aria-disabled`·`data-current`) | 줄 하나가 adjustable 요소(값 `position / total`, increment·decrement 동작) |
|
|
96
|
+
| 키보드 | 점에서 ←/→가 한 페이지씩 요청 | — |
|
|
97
|
+
| `labels` | 없음 | 필수(조절 동작 이름) |
|
|
98
|
+
| ref | `forwardRef`(`div`) | 없음 |
|
|
99
|
+
|
|
100
|
+
## 근거
|
|
101
|
+
|
|
102
|
+
- PageIndicator 추가: 2026-10-18 Utilverse 홈 런처가 Native 페이저 아래 점을 직접 그렸다(HJM 1.30.0 감사 "HJM gap"). Carousel 점 줄을 공개 부품으로 빼서 같은 recipe·같은 이동 규칙을 쓰게 했다.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.17.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-18
|
|
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
|
|
|
@@ -67,7 +67,8 @@ import { ProfileScreen } from "@hjmds/react-native/screen-flows";
|
|
|
67
67
|
| 슬롯·prop | 내용 |
|
|
68
68
|
| --- | --- |
|
|
69
69
|
| `summary` | 아바타·닉네임·소개 등 요약(필수). Avatar·Heading·Text를 제품이 조합한다 |
|
|
70
|
-
| `edit` | 수정 진입 행동 `{ label, onAction, disabled?, pending? }`(필수). ghost 버튼으로 요약 아래에 놓인다 |
|
|
70
|
+
| `edit` | 수정 진입 행동 `{ label, onAction, disabled?, pending? }`(필수). 기본은 ghost 버튼으로 요약 아래에 놓인다 |
|
|
71
|
+
| `editPlacement` | 1.31.0. `"below-summary"`(기본) · `"summary"`. `summary`면 `edit`을 `summary` 안 [ProfileSummary](profile-summary.md)의 행동 줄 맨 앞에 `secondary` `small` 버튼으로 그린다(`profileSummaryRecipe.editAction`). 요약을 카드(Surface)로 감싸도 그 안의 ProfileSummary가 받는다. `summary`에 ProfileSummary가 없으면 수정 버튼이 그려지지 않는다 |
|
|
71
72
|
| `children` | 활동·통계 등 본문 |
|
|
72
73
|
| `accountActions` | 로그아웃·탈퇴 버튼 등. 본문 맨 아래 |
|
|
73
74
|
| ScreenLayout props | `title`, `description`, `header`, `leading`, `actions`, `notice`, `state`, `stateAction` 등(`footer` 제외) |
|
|
@@ -78,7 +79,7 @@ import { ProfileScreen } from "@hjmds/react-native/screen-flows";
|
|
|
78
79
|
| --- | --- | --- |
|
|
79
80
|
| 크기 | ScreenLayout 폭(최대 720); 수정은 ghost `Button` 기본 크기 | `ProfileScreen` |
|
|
80
81
|
| 간격 | 화면 padding `spacing.md` 16; summary–수정 `spacing.md` 16; (summary·수정 묶음)–`children`–`accountActions` `spacing.xl` 24 | Web·Native `ProfileScreen` `Stack gap="xl"`·`gap="md"` |
|
|
81
|
-
| 순서·정렬 | 헤더(제목) → summary → 수정 → `children` → `accountActions` | 렌더 순서 |
|
|
82
|
+
| 순서·정렬 | 헤더(제목) → summary → 수정 → `children` → `accountActions`. `editPlacement="summary"`면 수정이 summary 안(ProfileSummary 수치 아래 행동 줄 맨 앞)으로 들어가고 summary 바로 다음이 `children`(`spacing.xl` 24) | 렌더 순서 |
|
|
82
83
|
| 고정·스크롤 | 헤더 고정, 본문 전체 화면 스크롤(footer 없음); 계정 행동은 본문 끝 | `ScreenLayout` |
|
|
83
84
|
| 좁은 폭·큰 글자 | 요약 줄바꿈·아바타 크기는 `summary`(제품) 소유; 제목 열 최소 폭 120 × 글자 배율 | `screenPatternRecipe.headerMinWidth` |
|
|
84
85
|
|
|
@@ -87,3 +88,8 @@ import { ProfileScreen } from "@hjmds/react-native/screen-flows";
|
|
|
87
88
|
- 계정 정보 조회, 인증, 로그아웃·탈퇴 mutation과 그 확인([AlertDialog](alert-dialog.md))은 제품 소유다.
|
|
88
89
|
- 프로필 이미지·닉네임 문구 등 표시 데이터는 제품이 넘긴다. HJM은 계정 모델을 모른다.
|
|
89
90
|
- 요약 자리에 브랜드 색을 하드코딩하지 않고 HJM 토큰·제품 테마로 연결한다.
|
|
91
|
+
- 수정 행동을 프로필 카드 안에 두려고 ProfileScreen 대신 ScreenLayout + ProfileSummary `actions`로 다시 조립하지 않는다. `editPlacement="summary"`를 쓴다. 비로그인 계정 화면처럼 같은 자리에 "로그인"을 두는 경우도 `edit`의 `label`·`onAction`을 바꿔 연결한다.
|
|
92
|
+
|
|
93
|
+
## 근거
|
|
94
|
+
|
|
95
|
+
- `editPlacement`(1.31.0): 2026-10-15 사용자 결정으로 utilverse 계정 화면은 프로필 수정(비로그인 때 로그인) 행동을 프로필 카드 안에 둔다. ProfileScreen은 수정을 요약 밖 ghost 버튼으로 고정해서, 2026-10-18 채택 때 utilverse가 ProfileScreen을 쓰지 못하고 ScreenLayout과 ProfileSummary `actions`로 같은 틀을 다시 조립했다.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 실험
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.26.0
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-18
|
|
8
8
|
- 근거: [프로필 화면](../screens/common-profile.md), `src/screen-patterns.ts`의 `profileSummaryRecipe`, 양 renderer `src/screen-flows.tsx`
|
|
9
9
|
- 스토리북: 별도 예제 미등록
|
|
10
10
|
|
|
@@ -50,7 +50,7 @@ import { ProfileSummary } from '@hjmds/react-native/screen-flows';
|
|
|
50
50
|
| badge | ReactNode | 없음 | 이름 옆 배지 |
|
|
51
51
|
| nameActions | ReactNode | 없음 | 이름 행의 행동 |
|
|
52
52
|
| stats | `{id,value,label,onPress?,disabled?}[]` | `[]` | 통계 수치. `onPress`가 있으면 버튼 |
|
|
53
|
-
| actions | ReactNode | 없음 | 수치 아래 행동 줄 |
|
|
53
|
+
| actions | ReactNode | 없음 | 수치 아래 행동 줄. [ProfileScreen](profile-screen.md) `editPlacement="summary"` 안에서는 그 화면의 `edit`이 이 줄 맨 앞에 `secondary` `small` 버튼(`profileSummaryRecipe.editAction`)으로 먼저 들어간다(1.31.0) |
|
|
54
54
|
| children | ReactNode | 없음 | 하단 footer |
|
|
55
55
|
|
|
56
56
|
`stats`는 각 수치의 독립 행동을 보존하며 조회·팔로우·인증·라우팅은 앱에서 연결한다. 팔로우 상태 기계까지 필요한 경우 기존 FollowProfileCard 사용 여부를 먼저 검토한다.
|
|
@@ -59,7 +59,7 @@ import { ProfileSummary } from '@hjmds/react-native/screen-flows';
|
|
|
59
59
|
|
|
60
60
|
| 항목 | 값 | 근거 |
|
|
61
61
|
| --- | --- | --- |
|
|
62
|
-
| 크기 | 원형 아바타 프레임 72 | `profileSummaryRecipe.avatarSize` |
|
|
62
|
+
| 크기 | 원형 아바타 프레임 72. ProfileScreen 수정 행동은 `Button` `secondary` `small` | `profileSummaryRecipe.avatarSize`·`editAction` |
|
|
63
63
|
| 간격 | 수치 칸 사이 `spacing.md` 16, 값·라벨 사이 `spacing.xxs` 4, 행동 사이 `spacing.sm` 12. 영역 간격은 [CompositionLayout](composition-layout.md) | 양 renderer |
|
|
64
64
|
| 순서·정렬 | 머리(아바타 → 이름·배지 → nameActions) → 수치 줄 → actions → children, 수치 칸은 균등 폭·가운데 | `CompositionLayout` + `flex: 1` 칸 |
|
|
65
65
|
| 고정·스크롤 | 없음. ProfileScreen 본문 스크롤 안에 놓인다 | — |
|
|
@@ -71,6 +71,7 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
|
|
|
71
71
|
| `keyboardAvoidance` (Native) | `boolean` | `true`(미게시) | 키보드와 실제로 겹치는 만큼 footer·본문을 올리고 포커스된 입력칸을 스크롤해 보인다. `KeyboardAvoiding`·키보드 처리 Sheet/Dialog 안에서는 건너뛴다 |
|
|
72
72
|
| `title` | `string` | 필수 | 화면 제목. `header`를 주면 접근성 이름으로만 쓴다 |
|
|
73
73
|
| `titleBadge` | `ReactNode` | 없음 | 기본 제목 바로 옆 배지. 값·의미는 제품, 간격·정렬은 HJM. `header` 교체 또는 제목 숨김이면 그리지 않는다. 1.23.0 추가 API |
|
|
74
|
+
| `titleLeading` | `ReactNode` | 없음 | 미게시(1.30.0 이후). 제목 앞 식별 표시(팀 로고·도구 아트워크). 장식이라 보조기술에서 숨기고 제목이 heading·접근성 이름으로 남는다. `titleVisibility="hidden"`이면 표시가 제목 자리에 혼자 서고 `title`을 이름으로 하는 heading(level 1)이 된다. 이때도 아이콘 전용 48 띠가 아니라 제목 띠 52를 쓴다. 노드 크기는 제품이 주되 헤더 띠 안에 들어가야 한다. `header` 교체 시 그리지 않는다. `ChatScreen`은 같은 자리를 `avatar`로 받는다 |
|
|
74
75
|
| `header` | `ReactNode` | 없음 | 기존 navigator 헤더 유지용. 주면 기본 헤더(`exit`·제목·`description`·`actions`)를 그리지 않는다 |
|
|
75
76
|
| `description` | `string` | 없음 | 제목 아래 muted 설명 |
|
|
76
77
|
| `exit` | `{ kind: "back" \| "close"; label; onClick(Web) \| onPress(Native); disabled? }` | 없음 | 화면 나가기(뒤로·닫기·"설정 나가기")를 헤더 시작 쪽 아이콘 버튼으로 고정한다. 글자는 보이지 않고 `label`은 보조기술 이름이다. `back`은 ‹(RTL 반전), `close`는 ×. 헤더 시작 쪽에는 이 버튼 외의 자유 슬롯이 없다. 미게시(1.26.1 이후) |
|
|
@@ -79,6 +80,7 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
|
|
|
79
80
|
| `footer` | `ReactNode` | 없음 | 하단 고정 행동. footer가 위 경계·여백·safe area를 맡고, 안에 둔 [BottomCTA](bottom-cta.md)는 자기 바를 그리지 않고 설명·행동 줄만 그린다(2026-10-17, 경계 두 겹·버튼 이중 들여쓰기 수정) |
|
|
80
81
|
| `state` | `{ kind: "ready" }` · `{ kind: "loading" \| "empty" \| "error" \| "restricted"; title; description? }` | `{ kind: "ready" }` | ready가 아니면 children을 그리지 않고 상태와 `stateAction`을 가운데에 놓는다. 로딩은 Spinner 하나만 보이고 `title`·`description`은 Spinner의 접근성 이름이 된다 |
|
|
81
82
|
| `stateAction` | `ReactNode` | 없음 | 상태 안내 아래 행동(재시도 등) |
|
|
83
|
+
| `loadingIndicator` | `ReactNode` | 없음 | 미게시(1.30.0 이후). `state.kind === "loading"`일 때 Spinner 대신 그리는 브랜드 표시. 표시 자체는 보조기술에서 숨기고, `title`·`description`을 이름으로 하는 busy 상태(Web `role="status"`, Native `progressbar`)·가운데 배치·알림은 HJM이 맡는다. [CompositionLayout](composition-layout.md)의 같은 prop과 같은 계약 |
|
|
82
84
|
| `scroll` | `"screen"` · `"content"` | `"screen"` | `content`는 본문 자식(가상화 목록)이 스크롤을 소유한다. 상태 교체 중에는 `screen`으로 돌아간다 |
|
|
83
85
|
| `contentAlignment` | `"start"` · `"center"` | `"start"` | ready 본문을 사용 영역 중앙에 배치. 긴 본문은 위가 잘리지 않게 스크롤한다. 로그인/제품 안내를 가짜 query state로 바꾸지 않는다(1.24.0) |
|
|
84
86
|
| `surface` | `"default"` · `"transparent"` | `"default"` | 투명은 제품 host의 배경·wallpaper를 보존. 배경색 style 우회 대신 사용(1.24.0) |
|
|
@@ -119,6 +121,7 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
|
|
|
119
121
|
- 시점: Native는 화면·시트 진입 전환이 끝난 뒤(`InteractionManager`, 시트는 표시 뒤) — 키보드 애니메이션이 전환과 겹치지 않는다. Web은 mount 직후, `preventScroll`.
|
|
120
122
|
- 빼앗지 않는다: 사용자가 이미 입력 칸에 있으면 그대로 둔다(Native 포커스된 입력, Web 편집 가능한 요소).
|
|
121
123
|
- 오버레이 아래(1.28.2, Web·Native): 위에 HJM `Dialog`·`AlertDialog`·`Sheet`가 열려 있으면 아래 화면·시트는 고르지 않는다. Web은 제품 자체 모달(`aria-modal`·`dialog[open]`)도 막는다. Native는 HJM 오버레이만 알 수 있으므로 화면을 덮는 모달은 HJM 오버레이로 만든다.
|
|
124
|
+
- 오버레이 안의 화면(1.31.0, Web·Native): 화면을 **감싸는** 모달은 그 화면을 막지 않는다. `Dialog`·`Sheet` 본문에 둔 `ScreenLayout`(또는 화면 구성)은 오버레이가 열린 뒤(Web: 모달 활성화, Native: 표시·진입 애니메이션 뒤) 오버레이와 함께 진입해 자기 `initialFocus` 규칙으로 칸을 고른다. 오버레이 자신의 칸이 있으면 그 칸이 먼저이고, 오버레이에 `initialFocusRef`(Web)가 있으면 그쪽이 이긴다. Web 제품 모달(`aria-modal`)도 화면의 입력 칸을 품고 있으면 화면을 감싸는 것으로 보고 막지 않는다. 화면 밖에 열린 제품 모달은 그대로 막는다.
|
|
122
125
|
- 진입 창(1.28.2): 진입 뒤 아직 포커스하지 못한 범위는 사용자가 그 범위를 직접 조작(Native 터치·스크롤, Web 포인터·키·휠·터치)하기 전까지 열려 있다. 그 동안 ① 칸이 새로 나타나면(데이터 로드 뒤 드러나는 확인 칸) ② 진입 때 위에 있던 오버레이가 닫히면(첫 가입 약관 동의) 다시 고른다. 오버레이 안의 조작(동의 탭)은 아래 화면의 조작으로 세지 않는다. 오버레이가 닫힌 시점은 그 오버레이의 반환 포커스와 같은 시점(Native: 퇴장·dismiss 뒤, Web: 반환 포커스 뒤)이다.
|
|
123
126
|
- 나중에 드러나는 주 입력(1.28.2): 진입이 끝난 뒤 `initialFocusTarget`(`true`) 칸이 새로 mount되면 그 칸을 포커스한다. `initialFocus` 모드와 무관하다 — 검색 버튼으로 여는 검색 칸(`none` 화면), 단계형 시트·`EditorScreen`의 단계 칸(각 단계 칸에 `initialFocusTarget`)이 이 경우다. 다른 입력 칸에 초점이 있거나 위에 오버레이가 있으면 하지 않는다. 스스로 다시 mount되는 칸(가상화 목록 행)에는 주지 않는다.
|
|
124
127
|
- 다시 진입: 화면은 mount당 한 번이다. 스택에서 돌아온 화면(이미 mount)은 다시 포커스하지 않는다 — 돌아온 맥락을 키보드가 가리고, HJM은 내비게이션 포커스 신호를 모른다. 시트·대화상자는 열 때마다 다시 고른다.
|
|
@@ -143,6 +146,8 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
|
|
|
143
146
|
- Web host는 실제 남은 높이(`height:100%` 등)를, Native host는 위·옆 safe area와 탭바 inset을 먼저 처리한다. 아래 홈 인디케이터는 host가 SafeAreaView로 들이지 말고 `safeAreaBottom`으로 넘긴다(들인 뒤 footer 16을 더하면 버튼 아래가 50이 된다).
|
|
144
147
|
HJM은 기종별 높이를 추측하지 않는다.
|
|
145
148
|
- 배치는 Web·Native 모두 `layoutStyle`로 한다(Web은 `className`도 있다). 색·여백을 덮지 않는다.
|
|
149
|
+
- 제목 옆 로고·아트워크 때문에 `header`에 TopBar나 직접 만든 헤더를 넣지 않는다. `titleLeading`을 쓰면 가운데 제목·양쪽 같은 폭 열·`exit`·`actions` 배치가 그대로 유지된다.
|
|
150
|
+
- 첫 로딩에 브랜드 로더를 보이려고 `ready` 본문 안에 로딩 화면을 직접 두지 않는다. `state={{ kind: "loading", title }}` + `loadingIndicator`로 넘긴다.
|
|
146
151
|
|
|
147
152
|
## 플랫폼 차이
|
|
148
153
|
|
|
@@ -168,6 +173,10 @@ dashboard는 가로 헤더/1200 목록 폭, editorial은 세로 헤더/720 읽
|
|
|
168
173
|
|
|
169
174
|
## 근거
|
|
170
175
|
|
|
176
|
+
- `titleLeading`: 2026-10-18 HJM 1.30.0 채택 감사. 야잘알 팀 상세는 팀 로고를 제목 자리에 두려고 헤더 전체를 직접 만들었고(측정한 양쪽 폭 맞춤까지 복제), 유틸버스 도구 화면은 도구 아트워크 때문에 `header`에 TopBar를 넣었다. ChatScreen 상대 사진은 이미 같은 자리를 비공개로 쓰고 있었으므로 공개 prop으로 열었다.
|
|
177
|
+
- `loadingIndicator`: 2026-10-18 spint 홈. ScreenLayout 로딩 상태가 브랜드 표시를 받지 않아 첫 로딩을 `ready` 본문 안의 제품 상태 화면으로 그렸다. CompositionLayout에는 같은 prop이 있었다.
|
|
178
|
+
|
|
171
179
|
- 하단 여백 `max(16, inset)`(Native `safeAreaBottom`): 2026-10-17 사용자 보고 "전체적으로 버튼 아래 여백이 많아". Native Storybook의 사진 선택과 업로드 화면에서 버튼 아래가 약 33pt였다. 원인은 둘이었다. 화면 스토리의 route host가 고정 높이 720이라 iPhone 17 Pro 스토리 영역 737보다 17 짧았고(footer 16 + 빈 칸 17), 컴포넌트는 Native footer가 safe area를 다루지 않아 host의 아래 inset 위에 16을, footer 안 BottomCTA의 `safeAreaBottom`도 16 위에 더했다(16 + 34 = 50). Web footer는 이미 `max(16, env())`였지만 footer 안 BottomCTA의 `safeAreaBottom`은 그 위에 더했다. 표준을 "마지막 컨트롤 아래 = footer 위 여백 16, 홈 인디케이터 기기에서는 max(16, inset)"으로 정하고 footer가 한 번만 소유하게 했다.
|
|
172
180
|
- 화면 여백은 ScreenLayout 소유, 바깥 여백 + `none` 미지원: 2026-10-17 바깥 여백 호스트 예제 사용자 검토. utilverse식(`Container gutter="regular"` 20 + `contentInset="none"`)을 HJM에서 지원하려고 본문 확장·footer 바 소유를 붙인 예제가 어색하다는 판단으로 지원을 접었다(2026-10-15 공통 구조는 HJM 소유 원칙). 측정상 그 구성에서는 Web 402·1280에서 행 표면이 양쪽 8 잘리고 본문이 옆으로 8 스크롤되며, Native 3x에서 선택 테두리 옆 선이 그려지지 않았다. 제품은 바깥 여백을 걷고 기본 inset을 쓴다.
|
|
173
181
|
- `contentInset="body-edge"`: 2026-10-17 사용자 결정. 화면 검토 S5#9에서 대화 목록 아바타가 화면 끝에서 약 32pt 안쪽에서 시작했다. 본문 padding 16과 ConversationRow 자체 padding 16이 겹쳤다(인스타그램 DM은 약 16). 앱 약 45곳이 "host가 모든 여백을 소유한다"는 뜻으로 `none`을 쓰고 있어 `none`의 의미를 바꿀 수 없었으므로 값을 새로 추가했다. ChatScreen의 10 타임라인 여백은 `default`일 때만 공통 여백을 대신한다.
|
|
182
|
+
- 오버레이 안의 화면 진입 포커스: 2026-10-18 번뚝 검색 오버레이. Web은 화면을 감싼 제품 `aria-modal`을 화면 위의 모달로 보고 진입 포커스를 막았고, HJM Sheet 안에 둔 ScreenLayout은 Sheet의 첫 컨트롤(닫기)에 포커스를 빼앗겼다. Native는 오버레이 표시 전에 칸을 포커스해 무시될 수 있었다. 감싸는 모달은 막지 않고 오버레이 안의 화면은 오버레이와 함께 진입하도록 고쳤다(1.31.0 patch).
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.12.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-18
|
|
8
8
|
- 근거: [Steps](../../steps.md), [StepPlayer](../../step-player.md), `src/steps.ts`(`stepsRecipe`)
|
|
9
9
|
- 스토리북: `배포/컴포넌트/탐색/단계 탐색`, `배포/컴포넌트/상태와 알림/단계별 진행 표시`
|
|
10
10
|
|
|
11
11
|
## 언제 쓰나
|
|
12
12
|
|
|
13
13
|
여러 단계로 된 **선형 흐름에서 지금 어디인지** 보여 줄 때 쓴다. 온보딩(환영 → 구단 → 알림 → 완료),
|
|
14
|
-
가입·주문 처리 단계가 여기에 속한다.
|
|
14
|
+
가입·주문 처리 단계가 여기에 속한다. 기본은 표시 전용이고, `onStepPress`를 주면 **이미 지난 단계만** 눌러 그 단계로
|
|
15
|
+
돌아갈 수 있다(1.31.0, 단계형 작성).
|
|
15
16
|
|
|
16
17
|
## 쓰지 않을 때
|
|
17
18
|
|
|
@@ -74,28 +75,30 @@ import { Steps } from "@hjmds/react-native/steps";
|
|
|
74
75
|
| `statusLabels` | `{ pending, current, complete, error }`(모두 `string`) | — (필수) | 상태를 읽는 문구 |
|
|
75
76
|
| `composeAccessibleName` | `(info: { position: number, total: number, label: string }) => string` | — (필수) | `position`은 1부터 |
|
|
76
77
|
| `renderMark` | `(status: StepStatus, position: number) => ReactNode` | `✓`·`!`·번호 | 원 안 표시를 바꾼다 |
|
|
78
|
+
| `onStepPress` | `(id) => void` | 없음 | 1.31.0. 커서 앞 단계(`revisitable`, 상태는 늘 `complete`)만 원·라벨 전체가 한 버튼이 되고 누르면 그 `id`를 넘긴다. 현재·다음 단계는 표시 전용으로 남는다(앞으로 건너뛰면 검증을 지나친다). 버튼 이름은 `composeAccessibleName` 결과, 상태 문구는 설명(Web `aria-describedby`·Native `accessibilityHint`). 흐름이 잠긴 동안(제출 중)은 prop을 빼면 모두 표시 전용이 된다 |
|
|
77
79
|
| `layoutStyle` | `HjmCompositionStyleProp` | — | 루트 배치. Web·Native 모두 |
|
|
78
80
|
| `style`(Native) | 지원하지 않음 | — | 제거된 속성 — `layoutStyle`. 현재 공개 props에서 제거되었다 |
|
|
79
81
|
| `StepPlayer` `progress`·`playing` | `number`(0~1)·`boolean` | — (필수) | 호스트 소유 |
|
|
80
82
|
| `StepPlayer` `onPlayingChange`·`onReplay` | `(playing: boolean) => void`·`() => void` | — (필수) | 재생/일시정지·다시 보기 요청 |
|
|
81
83
|
| `StepPlayer` `labels` | `{ play, pause, replay, progress }` | — (필수) | — |
|
|
82
84
|
|
|
83
|
-
방향·
|
|
85
|
+
방향·variant 축과 앞으로 건너뛰는 이동은 없다(계약에서 배제). 항상 가로 한 줄이다.
|
|
84
86
|
|
|
85
87
|
## 배치
|
|
86
88
|
|
|
87
89
|
| 항목 | 값 | 근거 |
|
|
88
90
|
| --- | --- | --- |
|
|
89
|
-
| 크기 | 단계마다 같은 폭(`minmax(0, 1fr)`, Native `flex: 1`). 원(indicator) 24(`glyph.md`), 연결선 1(`stroke.default`), 현재·오류 단계의 원 테두리 2(`stroke.strong`) | `stepsRecipe`, `styles.css` `.hjm-steps*`, `react-native/src/steps.tsx` |
|
|
91
|
+
| 크기 | 단계마다 같은 폭(`minmax(0, 1fr)`, Native `flex: 1`). 원(indicator) 24(`glyph.md`), 연결선 1(`stroke.default`), 현재·오류 단계의 원 테두리 2(`stroke.strong`). 누를 수 있는 단계는 칸 전체가 버튼이고 최소 높이 44(`stepsRecipe.press.minTouchTarget`) | `stepsRecipe`, `styles.css` `.hjm-steps*`, `react-native/src/steps.tsx` |
|
|
90
92
|
| 간격 | 단계 사이·원과 글자 사이 `spacing.xs` 8(`stepsRecipe.gap`) | `stepsRecipe.gap` |
|
|
91
|
-
| 순서·정렬 | 흐름 화면의 맨 위(
|
|
93
|
+
| 순서·정렬 | 흐름 화면의 맨 위(머리 아래, 본문 위)에 가로 한 줄. 화면 아래 행동 버튼과 떨어뜨리고, 같은 수준 화면 사이 이동(탭)으로 쓰지 않는다 | — |
|
|
92
94
|
| 고정·스크롤 | — | — |
|
|
93
95
|
| 좁은 폭·큰 글자 | 라벨·설명은 줄바꿈된다(`overflow-wrap: anywhere`). 좁은 폭에서 라벨이 세 줄 이상이 되면 라벨을 줄이거나 [Progress](progress.md)로 바꾼다. 세로 배치 축은 없다 | `styles.css` `.hjm-steps__label` |
|
|
94
96
|
|
|
95
97
|
## 꼭 지킬 것
|
|
96
98
|
|
|
97
99
|
- `statusLabels` 네 개와 `composeAccessibleName`은 필수다. "3단계 중 2단계" 같은 어순은 제품이 i18n으로 조립한다.
|
|
98
|
-
-
|
|
100
|
+
- 바로 앞 단계로 가는 주 행동은 Steps 밖에 둔다(단계형 작성은 [EditorScreen](editor-screen.md) `stepBack` 헤더 뒤로, 온보딩은 "이전" [Button](button.md)). `onStepPress`는 여러 단계 앞으로 한 번에 돌아가는 보조 길이다.
|
|
101
|
+
- 누를 수 있는 단계 표시를 제품이 Pressable·button으로 다시 그리지 않는다. 지난 단계·현재·다음의 구분과 이름은 HJM이 만든다.
|
|
99
102
|
- `StepPlayer`는 타이머가 없다. `progress`(0~1)·`playing`·커서를 호스트가 소유하고, `onPlayingChange`·`onReplay`에서
|
|
100
103
|
직접 상태를 바꾼다. `labels`(`play`·`pause`·`replay`·`progress`)는 비면 `TypeError`, `progress`가 범위를 벗어나면 `RangeError`다.
|
|
101
104
|
- 실제 비동기 작업의 완료를 `StepPlayer` 시계로 추정하지 않는다([계약](../../step-player.md)).
|
|
@@ -104,7 +107,11 @@ import { Steps } from "@hjmds/react-native/steps";
|
|
|
104
107
|
|
|
105
108
|
| 항목 | Web | Native |
|
|
106
109
|
| --- | --- | --- |
|
|
107
|
-
| 구조 | `<ol>`, 현재 단계 `aria-current="step"`, 상태 문구는 숨은
|
|
110
|
+
| 구조 | `<ol>`, 현재 단계 `aria-current="step"`, 상태 문구는 숨은 텍스트. 누를 수 있는 단계는 `<li>` 안 `<button>`(이름 = 단계 이름, 설명 = 상태 문구) | `accessibilityRole="summary"`, 단계마다 이름 + `accessibilityHint`에 상태 문구. 누를 수 있는 단계는 `Pressable` `button` |
|
|
108
111
|
| 스타일 | `className`·`style`(HTML 속성), 배치는 `layoutStyle` | 배치는 `layoutStyle`(`style`은 제거된 속성) |
|
|
109
112
|
| ref | `forwardRef`(`ol`) | 없음 |
|
|
110
113
|
| import 경로 | root, `/navigation`, `/steps` | root, `/steps`(`/navigation` 없음) |
|
|
114
|
+
|
|
115
|
+
## 근거
|
|
116
|
+
|
|
117
|
+
- `onStepPress`(1.31.0): 2026-10-18 spint 작성(색 → 이모지 → 글)이 지난 단계를 눌러 돌아가려고 단계 레일 전체를 PressableScale로 직접 그렸다. 계약은 "필요해지면 추가"로 clickable을 미뤄 두었고([계약](../../steps.md#공개한-축--배제한-축)), 앞으로 건너뛰기는 검증을 지나치므로 커서 앞 단계만 연다.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.12.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-18
|
|
8
8
|
- 근거: [Gooey navigation](../../gooey-navigation.md), `src/component-recipes.ts`(`tabsRecipe`), `src/behaviors.ts`(`tabsBehaviorDefaults`)
|
|
9
9
|
- 스토리북: `배포/컴포넌트/탐색/탭`, `배포/컴포넌트/탐색/선택 표시가 이어지는 탭`
|
|
10
10
|
|
|
@@ -77,6 +77,7 @@ import { Tabs } from "@hjmds/react-native/navigation";
|
|
|
77
77
|
| `overflow` | `scroll` · `clip` | `scroll` | — |
|
|
78
78
|
| `orientation` | `horizontal` · `vertical` | `horizontal` | — |
|
|
79
79
|
| `loop` | `boolean` | `true` | — |
|
|
80
|
+
| `indicator` | `underline` · `filled` | `underline` | 1.31.0 추가. `filled`는 선택 탭 뒤에 브랜드 연한 면(`surface.brand`) 알약을 깔고 글자를 `content.brand`로 바꾸며 목록 구분선을 없앤다. `slide`·`gooey`·`swipePosition`과 함께 쓰면 알약이 움직인다 |
|
|
80
81
|
| `appearance` | `standard` · `slide` · `gooey` | 미지정 시 프로필의 selectionMotion; 없으면 `standard` | 1.15.0 변경: `slide`는 2점 표시선 이동, `gooey`는 6점 표시선 늘어남. 명시 값 우선, 세로는 standard 유지 |
|
|
81
82
|
| `renderPanels` | `boolean` | `true` | `false`면 패널을 그리지 않는다. 패널을 라우터·스크롤 상태와 함께 따로 둘 때 `TabPanel`에 `tabsId`(Tabs의 `id`와 같게)·`activeValue`·`value`를 넘긴다 |
|
|
82
83
|
| `children`(Native) | `(selectedValue: Value) => ReactNode` | — | 항목 `panel` 대신 선택 값으로 패널을 그린다 |
|
|
@@ -105,6 +106,19 @@ import { Tabs } from "@hjmds/react-native/navigation";
|
|
|
105
106
|
└────────────────────────────────────┘
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
### 채운 선택 표시 (`indicator="filled"`)
|
|
110
|
+
|
|
111
|
+
선택 표시를 2점 밑줄 대신 선택 탭 전체를 채우는 알약으로 그린다. 값은 `tabsRecipe.indicators.filled`다.
|
|
112
|
+
알약 면 `surface.brand`(surfaceAccent), 선택 글자 `content.brand`, 모서리 `radius.full`, 목록 구분선 없음.
|
|
113
|
+
Native는 `filled`일 때 탭 사이에 `tabsRecipe.gap` 8을 두어 이웃 알약이 붙지 않게 한다(Web 목록은 원래 8 간격).
|
|
114
|
+
움직이는 표시(`appearance="slide"`·`"gooey"`, `swipePosition`)와 함께 쓰면 알약이 탭 높이 그대로 라벨 뒤에서 이동한다.
|
|
115
|
+
앱 하단 탭이 현재 위치를 연한 알약으로 표시하는 제품에서 화면 안 탭도 같은 표시를 쓰려고 고른다.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// Native
|
|
119
|
+
<Tabs label={t("sent.group")} layout="fitted" indicator="filled" value={tab} onValueChange={setTab} items={items} />
|
|
120
|
+
```
|
|
121
|
+
|
|
108
122
|
## 꼭 지킬 것
|
|
109
123
|
|
|
110
124
|
- `label`(탭 목록 이름)은 필수이고 비어 있으면 `TypeError`다. 항목 라벨과 함께 i18n 키로 넣는다.
|
|
@@ -129,6 +143,8 @@ import { Tabs } from "@hjmds/react-native/navigation";
|
|
|
129
143
|
|
|
130
144
|
## 함정
|
|
131
145
|
|
|
146
|
+
- `indicator="filled"`는 선택 표시만 바꾼다. 같은 목록의 필터처럼 패널이 하나뿐이면 Tabs가 아니라 [SegmentedControl](segmented-control.md)(`pills`)이다.
|
|
147
|
+
|
|
132
148
|
- 2026-10-07 [Animated Tabs 대조](../../../../../docs/plans/aceternity-interaction-adoption-2026-10-07.md)에서
|
|
133
149
|
선택 표시의 이동과 겹친 패널의 이동을 구분했다. 현재 `appearance="gooey"`는 표시선만 늘어나며,
|
|
134
150
|
패널을 겹쳐 복제하거나 순서를 바꾸지 않는다. 1.15.0 변경 후속 구현에서
|
|
@@ -166,3 +182,7 @@ import { Tabs } from "@hjmds/react-native/navigation";
|
|
|
166
182
|
|
|
167
183
|
source 객체는 렌더마다 바꾸지 않도록 `useMemo` 또는 제품 어댑터의 안정 인스턴스를
|
|
168
184
|
사용한다. source는 시각적 위치만 전달하므로 접근성·키보드 선택을 대신하지 않는다.
|
|
185
|
+
|
|
186
|
+
## 근거
|
|
187
|
+
|
|
188
|
+
- 채운 선택 표시: 2026-10-18 다에리 둥지 화면의 보낸 편지·모음 탭. 앱 하단 탭은 현재 위치를 연한 알약으로 보이는데 화면 안 탭만 밑줄이라 다른 제품처럼 보였다.
|
|
@@ -35,6 +35,7 @@ Text와 Heading의 경계: Text의 `variant="heading"`(24px)은 **모양만**
|
|
|
35
35
|
| --- | --- | --- | --- |
|
|
36
36
|
| `Text` | 기본 | `@hjmds/react`, `/layout` | `@hjmds/react-native`, `/primitives` |
|
|
37
37
|
| `StatusText` | 짧은 상태·결과 알림 companion | `@hjmds/react`, `/status-text` | `@hjmds/react-native`, `/status-text` |
|
|
38
|
+
| `useHjmTextFace` | 보조 — Text 밖 제품 글자의 글꼴 해석 hook(컴포넌트 아님, 1.31.0) | — (CSS 상속) | `@hjmds/react-native`, `/primitives` |
|
|
38
39
|
| `GravityLetters` | 확장(홍보용 글자 낙하 장식, optional-extension) | `/gravity-letters` | `/gravity-letters` |
|
|
39
40
|
|
|
40
41
|
`GravityLetters`는 granular subpath로만 import 된다. 추가 peer 의존성은 없다(Web은 WAAPI, Native는 RN `Animated`).
|
|
@@ -63,6 +64,8 @@ import { Text } from "@hjmds/react-native/primitives";
|
|
|
63
64
|
| `fontRole` | `ui` · `display` · `reading` · `code` | 제목 variant는 display, 본문은 reading, caption/label은 ui | 명시값이 기본을 덮는다. [역할 지침](../tokens/font-roles.md), 1.16.0 변경 |
|
|
64
65
|
| `emphasis` | `regular` 400 · `medium` 500 · `strong` 500(제목 굵기) | 프로필이 있으면 variant 굵기, 없으면 `regular`. `label`은 프로필과 무관하게 variant 굵기 500 | 명시값이 프로필과 variant보다 우선 |
|
|
65
66
|
| `tone` | `primary` · `body` · `muted` · `subtle` · `weak` · `brand` · `danger` · `inverse` | `primary` | — |
|
|
67
|
+
| `color` | `string` | — | 제품이 소유한 도메인 색 역할(팀 색·등락·차트 계열)로 `tone` 색만 바꾼다. variant 크기·굵기·글꼴은 그대로. 1.31.0 추가 |
|
|
68
|
+
| `numeric` | `proportional` · `tabular` | —(글꼴 기본) | `tabular`는 바뀌는 숫자(점수·타이머·개수)의 폭을 고정한다. Web `font-variant-numeric`, Native `fontVariant`. 1.31.0 추가 |
|
|
66
69
|
| `as` | `span` · `p` · `div` · `strong` · `small` | `span` | Web만. 문단은 `as="p"` |
|
|
67
70
|
| `align` | `TextStyle["textAlign"]` | 쓰기 방향의 시작 쪽 | Native만 |
|
|
68
71
|
| `layoutStyle` | `HjmCompositionStyleProp` | — | 배치. Web·Native 모두 |
|
|
@@ -86,15 +89,41 @@ Native는 Text의 역할을 해당 stack에, 입력/조작 host는 제품 프로
|
|
|
86
89
|
- 글자는 i18n 키로 넣는다. 제품 이름·도메인 문구는 제품 소유, 크기·색 체계는 HJM 소유다.
|
|
87
90
|
- 배치(여백·폭·flex)는 `layoutStyle`로만 한다. 색·크기·굵기는 `style`/`className`으로 덮지 않는다.
|
|
88
91
|
(Native Text의 `style`은 layout primitive라 deprecated 경고 대상에서 빠져 있을 뿐 권장 경로가 아니다.)
|
|
89
|
-
- 색으로 의미를 바꿀 때는 `tone
|
|
92
|
+
- 색으로 의미를 바꿀 때는 `tone`을 쓴다(오류 문구 `danger`, 어두운 배경 위 `inverse`). HJM tone에 없는 제품 도메인 색
|
|
93
|
+
(팀 색, 등락 빨강/파랑)만 `color`로 넘긴다. 이때 배경 대비 확인은 제품 몫이다. `style.color`로 덮지 않는다.
|
|
90
94
|
- `GravityLetters`는 장식이며 접근성 트리에서 숨는다. 읽을 수 있는 같은 문구를 함께 두되 화면에 두 번 보이지 않게 한다: Web은 같은 Heading 안의 `VisuallyHidden`, Native는 `accessibilityRole="header"`·`accessibilityLabel`을 가진 감싸는 View.
|
|
91
95
|
`glyphs`는 32개 이하, 빈 문자열·줄바꿈 없이 넘긴다(어기면 `RangeError`). `active` 기본은 `false`다.
|
|
92
96
|
|
|
97
|
+
### 제품 색과 고정 폭 숫자(1.31.0 추가)
|
|
98
|
+
|
|
99
|
+
2026-10-18 야잘알의 제품 색 기록·연속 기록 문구는 HJM Text에 제품 색을 넣을 공개 경로가 없어 RN `Text`로 내려갔고,
|
|
100
|
+
RN은 글꼴을 상속하지 않아 `fonts="pretendard"`에서도 OS 글꼴로 그려졌다. 같은 이유로 숫자 폭 고정도 직접 `fontVariant`를 썼다.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// Native(Web도 같은 prop)
|
|
104
|
+
<Text variant="bodyLarge" emphasis="medium" color={streakColor}>{streakLabel}</Text>
|
|
105
|
+
<Text variant="caption" emphasis="strong" numeric="tabular" color={onAccentFill} align="center">{pitch.pitchSequence}</Text>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
HJM type 단계(최대 24)보다 큰 제품 숫자처럼 Text로 그릴 수 없는 글자는 Native `useHjmTextFace`로 같은 글꼴을 받는다.
|
|
109
|
+
Provider의 글꼴 설정(`fonts="pretendard"`, designProfile `fontFamily`)과 굵기에 맞는 `fontFamily`/`fontWeight`를 돌려준다.
|
|
110
|
+
크기·줄 높이·색은 제품이 정한다.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { useHjmTextFace } from "@hjmds/react-native/primitives";
|
|
114
|
+
|
|
115
|
+
const face = useHjmTextFace({ weight: "800" }); // fontRole 기본 ui
|
|
116
|
+
<RNText style={{ ...face, fontSize: 40, lineHeight: 46, fontVariant: ["tabular-nums"], color }}>{value}</RNText>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Web은 Provider가 CSS 상속으로 글꼴을 내려 주므로 같은 hook이 없다. 제품 요소는 Provider 안에서 HJM 글꼴을 이어받는다.
|
|
120
|
+
|
|
93
121
|
## 플랫폼 차이
|
|
94
122
|
|
|
95
123
|
| 항목 | Web | Native |
|
|
96
124
|
| --- | --- | --- |
|
|
97
125
|
| 요소 선택 | `as` | 없음(항상 RN `Text`) |
|
|
126
|
+
| Text 밖 제품 글자의 글꼴 | CSS 상속 | `useHjmTextFace` |
|
|
98
127
|
| 정렬 | 없음(배치는 부모) | `align`(생략 시 방향에 맞춘 논리 정렬) |
|
|
99
128
|
| 글자 크기 조정 | 브라우저 | provider `textScaling`이 `allowFontScaling`을 정함 |
|
|
100
129
|
| 긴 단어 줄바꿈 | `overflow-wrap: anywhere` | RN 기본 |
|