@hjmds/design-contracts 1.30.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 +141 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +14 -5
- package/dist/catalog.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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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 +3 -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/bottom-navigation.md +10 -0
- package/docs/carousel.md +8 -0
- package/docs/dialog.md +23 -0
- package/docs/generated/component-maturity.md +7 -4
- package/docs/generated/renderer-evidence.json +313 -17
- package/docs/generated/renderer-evidence.md +9 -3
- package/docs/generated/showcase-manifest.json +161 -5
- package/docs/icon.md +9 -0
- package/docs/screen-patterns.md +18 -0
- package/docs/steps.md +4 -3
- package/docs/usage/README.md +5 -2
- 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-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 +1 -1
|
@@ -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 기본 |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# TypingIndicator
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 실험
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.30.0 이후)
|
|
7
|
+
- 검토일: 2026-10-18
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md), `src/screen-patterns.ts`의 `typingIndicatorRecipe`·`chatMessageRecipe`; Web·Native `src/screens.tsx`
|
|
9
|
+
- 스토리북: `실험/컴포넌트/상태와 알림/입력 중 표시`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
대화 타임라인에서 상대가 지금 입력 중임을 보여 줄 때 쓴다. 수신 말풍선 자리에 점 세 개가 차례로 깜박이는 말풍선을 그리고,
|
|
14
|
+
보조기술에는 제품이 준 문구("서연님이 입력 중")를 한 번 알린다. 입력 중 여부(presence) 판단과 표시 시점은 제품이 정한다.
|
|
15
|
+
|
|
16
|
+
## 쓰지 않을 때
|
|
17
|
+
|
|
18
|
+
| 상황 | 대신 쓸 것 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 내 메시지 전송 중 | [ChatMessage](chat-message.md) `deliveryLabel` |
|
|
21
|
+
| 화면·영역 로딩 | [Spinner](spinner.md), ScreenLayout `state` |
|
|
22
|
+
| AI 답변 생성 중 | [StreamingAnswer](streaming-answer.md)·[ThinkingOrb](thinking-orb.md) |
|
|
23
|
+
|
|
24
|
+
## 공개 이름과 import
|
|
25
|
+
|
|
26
|
+
| 이름 | 역할 | Web | Native |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `TypingIndicator` | 기본 | `@hjmds/react/screens` | `@hjmds/react-native/screens` |
|
|
29
|
+
|
|
30
|
+
루트 barrel에는 없다.
|
|
31
|
+
|
|
32
|
+
## 최소 사용 예
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Web
|
|
36
|
+
import { ChatMessage, ChatScreen, TypingIndicator } from "@hjmds/react/screens";
|
|
37
|
+
|
|
38
|
+
<ChatScreen title={peer.name} composer={composer}>
|
|
39
|
+
<div>
|
|
40
|
+
{messages.map(m => <ChatMessage key={m.id} {...toMessageProps(m)}>{m.text}</ChatMessage>)}
|
|
41
|
+
{peerTyping ? <TypingIndicator label={t("chat.typing", { name: peer.name })} /> : null}
|
|
42
|
+
</div>
|
|
43
|
+
</ChatScreen>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Native — 가상화 목록의 ListFooterComponent로 둔다
|
|
48
|
+
import { FlatList } from "react-native";
|
|
49
|
+
import { TypingIndicator } from "@hjmds/react-native/screens";
|
|
50
|
+
|
|
51
|
+
<FlatList data={messages} renderItem={renderMessage}
|
|
52
|
+
ListFooterComponent={peerTyping ? <TypingIndicator label={t("chat.typing", { name: peer.name })} /> : null} />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 축과 기본값
|
|
56
|
+
|
|
57
|
+
| prop | 값 | 기본값 | 설명 |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `label` | `string` | 필수 | 지역화한 상태 문구. 화면에는 보이지 않고 보조기술 이름·알림이 된다. 빈 문자열은 `TypeError` |
|
|
60
|
+
| `avatar` | `ReactNode` | 없음 | 단톡방에서 입력 중인 사람 사진. ChatMessage 아바타와 같은 32(`chatMessageRecipe.avatarSize`)로 넘긴다 |
|
|
61
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | 없음 | 행 배치 전용. 색·여백을 덮지 않는다 |
|
|
62
|
+
|
|
63
|
+
## 배치
|
|
64
|
+
|
|
65
|
+
| 항목 | 값 | 근거 |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| 크기 | 점 `dotSize` 6(원), 말풍선 최소 높이 = 본문 줄 높이 + 위아래 `bubblePaddingBlock` 6 — 한 줄 수신 말풍선과 같은 높이; radius `bubbleRadius` 14, 표면 `surface` | `typingIndicatorRecipe`, `chatMessageRecipe` |
|
|
68
|
+
| 간격 | 점 사이 `dotGap`(`spacing.xxs`) 4, 말풍선 안 좌우 10; 위 `groupGap` 12(새 묶음처럼 떨어진다); 아바타–말풍선 `avatarGap` 8 | `typingIndicatorRecipe`, `chatMessageRecipe` |
|
|
69
|
+
| 순서·정렬 | 수신 쪽(시작 쪽) 정렬, 마지막 메시지 뒤. 아바타가 없으면 꼬리 폭 `tailWidth` 7만큼 들여 수신 말풍선과 가장자리를 맞춘다 | Web·Native `TypingIndicator` |
|
|
70
|
+
| 고정·스크롤 | 자체 고정 없음. 타임라인 목록 안에서 함께 스크롤한다 | — |
|
|
71
|
+
| 좁은 폭·큰 글자 | 점 크기는 고정. 글자 배율과 무관하다 | Web·Native `TypingIndicator` |
|
|
72
|
+
|
|
73
|
+
## 꼭 지킬 것
|
|
74
|
+
|
|
75
|
+
- 상대가 실제로 입력 중일 때만 렌더하고, 멈추면 제품이 제거한다. 표시 시간을 HJM이 정하지 않는다.
|
|
76
|
+
- 문구는 제품 i18n으로 넘긴다. 점 표시 옆에 같은 문구를 보이는 글자로 다시 쓰지 않는다.
|
|
77
|
+
- 움직임: 한 주기 `cycleMs` 1200 동안 점마다 `restOpacity` 0.35 ↔ 1로 깜박이고 점 사이 시작 간격은 `staggerMs` 160이다. 동작 줄이기(Provider `reducedMotion`, Web은 `prefers-reduced-motion`도)에서는 점이 깜박이지 않고 불투명하게 멈춘다.
|
|
78
|
+
- 알림은 정중(polite)하다. 나타날 때 한 번 읽고 진행 중인 낭독을 끊지 않는다. 타임라인 전체를 live region으로 감싸지 않는다.
|
|
79
|
+
|
|
80
|
+
## 플랫폼 차이
|
|
81
|
+
|
|
82
|
+
| 항목 | Web | Native |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| 알림 | `role="status"` + 숨긴 문구 | 접근성 요소 하나(`accessibilityLabel`), Android polite live region, iOS는 나타날 때 `announceForAccessibilityWithOptions(queue)` |
|
|
85
|
+
| 움직임 | CSS keyframes(`hjm-typing-pulse`) | `Animated.loop`(native driver) |
|
|
86
|
+
|
|
87
|
+
## 근거
|
|
88
|
+
|
|
89
|
+
- 공통 입력 중 표시: 2026-10-18 번뚝 DM. Web·Native가 각자 점(●●●)과 문구 말풍선을 만들었다(Web `role="status"` 문구, Native 별도 컴포넌트와 Animated 깜박임). 공통 타임라인 구조는 HJM 소유라는 2026-10-15 원칙에 따라 공개 조각으로 옮겼다.
|
|
@@ -18,12 +18,13 @@ AuthScreenLayout을 사용해 로그인 흐름을 구성한다. 제품이 데이
|
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
20
|
host: 남은 높이·safe area·키보드
|
|
21
|
-
└─ 히어로 → 제공자 카드 → 하단 동의
|
|
21
|
+
└─ (머리 띠: [뒤로/닫기] 로그인) → 히어로 → 제공자 카드 → 하단 동의
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
25
25
|
| --- | --- | --- | --- |
|
|
26
26
|
| 바깥 틀 | AuthScreenLayout | route 본문 | [API 배치 규칙](../components/auth-screen-layout.md#배치), host 남은 높이 |
|
|
27
|
+
| 머리(선택) | AuthScreenLayout `title`·`exit`(1.31.0) | 맨 위 고정, 화면 가장자리까지 | ScreenLayout 가운데 머리 띠(최소 52). 바깥에 TopBar를 따로 얹지 않는다 |
|
|
27
28
|
| 내용 | 공개 슬롯 | 히어로 → 제공자 카드 → 하단 동의 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
|
|
28
29
|
| 상태 | pendingLabel 또는 main 안 Notice | 제공자 카드 내부 | 카드 크기를 유지하고 동의 고지를 남긴다 |
|
|
29
30
|
|
|
@@ -33,6 +34,7 @@ host: 남은 높이·safe area·키보드
|
|
|
33
34
|
| --- | --- | --- | --- |
|
|
34
35
|
| 작업 | AuthScreenLayout 공개 행동 슬롯 | 제공자 버튼에는 이름만; 로그인 중 카드 중앙 스피너 하나 | 같은 표면에 경쟁하는 primary 하나만 |
|
|
35
36
|
| 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
|
|
37
|
+
| 나가기 | AuthScreenLayout `exit`(아이콘 뒤로/닫기) | 머리 띠 시작 끝 | 0~1. 로그인 진행 중에는 제품이 `disabled` |
|
|
36
38
|
|
|
37
39
|
## 상태
|
|
38
40
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 구성 API 지침 참조
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-18
|
|
8
8
|
- 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screen-flows.tsx`(`ProfileScreen`·`EditorScreen`), 예제 `showcase/*/screen-flow-previews.tsx`(`AccountFlowPreview`)·`saved-profile-previews.tsx`(`ProfileEditFields`), 기본 얼굴 `showcase/web/src/patterns/ProfileStudio.previews.tsx`·`showcase/native/src/profile-face-preview.tsx`·`showcase/shared/profile-studio.ts`. 2026-10-06 사용자 승인으로 실험 `공통 화면/프로필`을 배포하면서 같은 프로필 수정을 직접 조립으로 보이던 배포 `화면/프로필 편집`과 합쳤다(API 기반 우선, Web id `patterns-profile-studio` 보존, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
9
|
- 스토리북: `배포/화면/계정/프로필`
|
|
10
10
|
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
| 바깥 틀 | ProfileScreen · EditorScreen(둘 다 ScreenLayout) | route 본문, host가 남은 높이·safe area·키보드를 준다 | 폭 최대 720, 바깥 padding `spacing.md` 16([ScreenLayout 배치](../components/screen-layout.md#배치)), 본문 스크롤 |
|
|
54
54
|
| 머리 | `title` + `actions`(IconButton ghost 계정 메뉴) | 맨 위, 고정 | 제목 열 최소 120 × 글자 배율 |
|
|
55
55
|
| 요약 | `summary`(제품) | 본문 맨 위 | 요약 안 배치는 제품 소유 |
|
|
56
|
-
| 수정 진입 | `edit` → Button ghost | 요약 아래 | 요약과 `spacing.md` 16 |
|
|
56
|
+
| 수정 진입 | `edit` → Button ghost | 요약 아래 | 요약과 `spacing.md` 16. 프로필 카드 안에 두는 제품은 `editPlacement="summary"`(ProfileSummary 행동 줄 맨 앞, `secondary` `small`, 1.31.0) |
|
|
57
57
|
| 게시물 | `children` > Stack `gap="md"` > 탭 Button 줄 + Grid `columns={{ compact: 3 }}` | 수정 아래 | 요약 묶음과 `spacing.xl` 24, 격자 간격 `spacing.xxs` |
|
|
58
58
|
| 계정 메뉴 | Sheet > ListRow, `footer` > Button ghost 로그아웃 → AlertDialog | 오버레이 | footer 고정 |
|
|
59
59
|
| 수정 화면 | EditorScreen `submitPlacement="footer"` > 사진·이름·소개 필드 | 같은 route에서 보기 대신 | footer: 초안 상태 → 저장 primary, 사이 `spacing.sm` 12 |
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
|
|
78
78
|
| 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
|
|
79
79
|
| --- | --- | --- | --- |
|
|
80
|
-
| 프로필 수정 | `edit` → Button ghost | 요약 아래 | 1 |
|
|
80
|
+
| 프로필 수정 | `edit` → Button ghost(`editPlacement="summary"`면 ProfileSummary 안 `secondary` `small`) | 요약 아래(또는 요약 카드 안 행동 줄 맨 앞) | 1 |
|
|
81
81
|
| 계정 메뉴 | IconButton ghost(접근성 이름 "계정 메뉴") | 머리 끝(`actions`) | 1. 로그아웃·연결 계정은 메뉴 안 |
|
|
82
82
|
| 게시물·저장됨 전환 | [SegmentedControl](../components/segmented-control.md)(label "프로필 보기") | 게시물 영역 맨 위 | 2개 항목, 단일 선택. 선택 상태를 ghost Button으로 흉내 내지 않는다 |
|
|
83
83
|
| 게시물 열기 | 사진 칸 전체 | 격자 | 칸마다 1. 상세는 ScreenLayout + `exit` 뒤로 아이콘 |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 구성 API 지침 참조
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-18
|
|
8
8
|
- 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/기본 흐름/작성과 수정`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
9
|
- 스토리북: `배포/화면/콘텐츠/작성과 수정`
|
|
10
10
|
|
|
@@ -31,7 +31,7 @@ host: 남은 높이·safe area·키보드
|
|
|
31
31
|
|
|
32
32
|
| 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
|
|
33
33
|
| --- | --- | --- | --- |
|
|
34
|
-
| 작업 | EditorScreen 공개 행동 슬롯 | 취소는
|
|
34
|
+
| 작업 | EditorScreen 공개 행동 슬롯 | 취소는 헤더(단계형은 두 번째 단계부터 `stepBack` 뒤로); 저장은 footer; 수정한 채 이탈하면 확인 | 같은 표면에 경쟁하는 primary 하나만 |
|
|
35
35
|
| 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
|
|
36
36
|
|
|
37
37
|
## 상태
|
|
@@ -54,6 +54,17 @@ host: 남은 높이·safe area·키보드
|
|
|
54
54
|
| 저장 성공 | 제출 snapshot을 saved로 갱신 | 요청 후 새로 쓴 내용까지 saved로 처리하지 않는다. draftStatus는 실제 임시 저장 성공 때만 표시한다 |
|
|
55
55
|
| 브라우저·OS 뒤로 | 제품 router의 이탈 가드 | EditorScreen cancel의 discard와 같은 제품 결정을 공유한다 |
|
|
56
56
|
|
|
57
|
+
### 단계형 작성(1.31.0)
|
|
58
|
+
|
|
59
|
+
색 → 이모지 → 글처럼 단계가 나뉜 작성은 같은 EditorScreen 안에서 단계를 바꾼다. 앞 단계로 돌아가는 길은 둘이다.
|
|
60
|
+
|
|
61
|
+
| 행동 | 연결 | 규칙 |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| 헤더 뒤로 | EditorScreen `stepBack`(두 번째 단계부터) | 헤더 시작 끝이 × 대신 뒤로 화살표가 된다. 단계를 되돌릴 뿐 이탈이 아니라 `discard` 확인을 띄우지 않는다. 첫 단계에서는 `stepBack`을 빼 × 닫기(`cancel`·`discard`)로 돌아간다. 저장 중에는 잠긴다 |
|
|
64
|
+
| 지난 단계 누르기 | 본문 맨 위 [Steps](../components/steps.md) `onStepPress` | 커서 앞 단계만 버튼이다. 현재·다음 단계는 누를 수 없다(검증을 건너뛰지 않는다). 제출 중처럼 흐름이 잠긴 동안은 `onStepPress`를 빼면 모두 표시 전용이 된다 |
|
|
65
|
+
|
|
66
|
+
2026-10-18 spint 작성 화면이 이 두 행동 때문에 EditorScreen 머리를 제품 TopBar로 바꾸고 단계 표시를 직접 그렸다.
|
|
67
|
+
|
|
57
68
|
본문이 ready가 아닌 최초 조회·오류·제한 상태에서는 헤더/하단 어느 위치의 저장도 비활성이다. 실패한 저장은
|
|
58
69
|
본문을 오류 화면으로 교체하지 않고 위 표처럼 ready + notice로 유지한다.
|
|
59
70
|
|