@hjmds/design-contracts 1.20.0 → 1.21.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/authoring-tools.d.ts +38 -0
- package/dist/authoring-tools.d.ts.map +1 -0
- package/dist/authoring-tools.js +35 -0
- package/dist/authoring-tools.js.map +1 -0
- package/dist/behaviors.d.ts +33 -3
- package/dist/behaviors.d.ts.map +1 -1
- package/dist/behaviors.js +2 -0
- package/dist/behaviors.js.map +1 -1
- package/dist/calendar.d.ts +9 -1
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +4 -2
- package/dist/calendar.js.map +1 -1
- package/dist/catalog.d.ts +129 -2
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +6 -2
- package/dist/catalog.js.map +1 -1
- package/dist/chart.d.ts +398 -0
- package/dist/chart.d.ts.map +1 -0
- package/dist/chart.js +414 -0
- package/dist/chart.js.map +1 -0
- package/dist/collection-editing.d.ts +38 -0
- package/dist/collection-editing.d.ts.map +1 -0
- package/dist/collection-editing.js +53 -0
- package/dist/collection-editing.js.map +1 -0
- package/dist/communication.d.ts +29 -0
- package/dist/communication.d.ts.map +1 -0
- package/dist/communication.js +11 -0
- package/dist/communication.js.map +1 -0
- package/dist/component-definitions.d.ts +1 -0
- package/dist/component-definitions.d.ts.map +1 -1
- package/dist/component-definitions.js +1 -0
- package/dist/component-definitions.js.map +1 -1
- package/dist/data-table.d.ts +229 -5
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +242 -6
- package/dist/data-table.js.map +1 -1
- package/dist/data-workflows.d.ts +70 -0
- package/dist/data-workflows.d.ts.map +1 -0
- package/dist/data-workflows.js +109 -0
- package/dist/data-workflows.js.map +1 -0
- package/dist/dataviz.d.ts +7 -6
- package/dist/dataviz.d.ts.map +1 -1
- package/dist/dataviz.js +7 -6
- package/dist/dataviz.js.map +1 -1
- package/dist/gooey-navigation.d.ts +7 -0
- package/dist/gooey-navigation.d.ts.map +1 -1
- package/dist/gooey-navigation.js +10 -0
- package/dist/gooey-navigation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/media-transport.d.ts +16 -0
- package/dist/media-transport.d.ts.map +1 -0
- package/dist/media-transport.js +17 -0
- package/dist/media-transport.js.map +1 -0
- package/dist/recipes.d.ts +1 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js +1 -0
- package/dist/recipes.js.map +1 -1
- package/dist/showcase.d.ts.map +1 -1
- package/dist/showcase.js +3 -1
- package/dist/showcase.js.map +1 -1
- package/dist/social-compositions.d.ts +19 -0
- package/dist/social-compositions.d.ts.map +1 -0
- package/dist/social-compositions.js +33 -0
- package/dist/social-compositions.js.map +1 -0
- package/dist/streaming-answer.d.ts +18 -0
- package/dist/streaming-answer.d.ts.map +1 -0
- package/dist/streaming-answer.js +61 -0
- package/dist/streaming-answer.js.map +1 -0
- package/dist/task-controls.d.ts +31 -0
- package/dist/task-controls.d.ts.map +1 -0
- package/dist/task-controls.js +25 -0
- package/dist/task-controls.js.map +1 -0
- package/dist/toggle-feedback.d.ts +22 -0
- package/dist/toggle-feedback.d.ts.map +1 -0
- package/dist/toggle-feedback.js +32 -0
- package/dist/toggle-feedback.js.map +1 -0
- package/dist/transfer-task.d.ts +13 -0
- package/dist/transfer-task.d.ts.map +1 -0
- package/dist/transfer-task.js +13 -0
- package/dist/transfer-task.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/authoring-tools.md +31 -0
- package/docs/calendar.md +3 -1
- package/docs/catalog-freeze.json +5 -0
- package/docs/chart.md +206 -0
- package/docs/collection-editing.md +83 -0
- package/docs/communication.md +35 -0
- package/docs/data-table.md +56 -6
- package/docs/data-workflows.md +26 -0
- package/docs/generated/component-maturity.md +6 -5
- package/docs/generated/renderer-evidence.json +95 -5
- package/docs/generated/renderer-evidence.md +6 -3
- package/docs/generated/showcase-manifest.json +66 -3
- package/docs/gooey-navigation.md +20 -0
- package/docs/library-gap-analysis.md +4 -2
- package/docs/media-transport.md +37 -0
- package/docs/social-compositions.md +51 -0
- package/docs/streaming-answer.md +57 -0
- package/docs/task-controls.md +38 -0
- package/docs/toggle-feedback.md +56 -0
- package/docs/transfer-task.md +33 -0
- package/docs/usage/README.md +15 -4
- package/docs/usage/components/activity-chip.md +2 -2
- package/docs/usage/components/button.md +39 -0
- package/docs/usage/components/calendar.md +4 -2
- package/docs/usage/components/chart.md +277 -0
- package/docs/usage/components/data-table.md +92 -18
- package/docs/usage/components/screen-layout.md +3 -1
- package/docs/usage/components/tabs.md +25 -0
- package/docs/usage/components/toast.md +1 -1
- package/docs/usage/compositions/action-feedback.md +18 -2
- package/docs/usage/compositions/activity-status.md +2 -2
- package/docs/usage/compositions/authoring-tools.md +90 -0
- package/docs/usage/compositions/collection-editing.md +110 -0
- package/docs/usage/compositions/communication.md +100 -0
- package/docs/usage/compositions/data-workflows.md +91 -0
- package/docs/usage/compositions/media-transport.md +87 -0
- package/docs/usage/compositions/social-compositions.md +91 -0
- package/docs/usage/compositions/streaming-answer.md +97 -0
- package/docs/usage/compositions/task-controls.md +93 -0
- package/docs/usage/compositions/toggle-feedback.md +84 -0
- package/docs/usage/compositions/transfer-task.md +86 -0
- package/docs/usage/compositions/web-additions.md +2 -2
- package/docs/usage/screens/common-shell.md +4 -0
- package/package.json +67 -1
|
@@ -125,3 +125,42 @@ import { Button } from "@hjmds/react-native/actions";
|
|
|
125
125
|
|
|
126
126
|
2026-10-10 사용자 요청으로 선택 시 추가되는 테두리를 제거했다. `selected` 접근성 상태와
|
|
127
127
|
contentBrand 글자·아이콘 색은 유지하며 키보드 focus outline은 선택과 별도로 유지한다.
|
|
128
|
+
|
|
129
|
+
## 저장 확정 라벨과 프레임 예약 (미게시 · 2026-10-15)
|
|
130
|
+
|
|
131
|
+
`completion={{ confirmed, label }}`는 실제 mutation의 확정 결과를 표시한다. 두 라벨의
|
|
132
|
+
공간을 처음부터 예약하므로 `children`을 저장 중/완료 문구로 교체하지 않는다.
|
|
133
|
+
`confirmed`는 현재 초안과 마지막 서버 확정 값이 같은지 제품이 판단해 전달한다.
|
|
134
|
+
`loading`이 true이면 확정 라벨보다 중앙 로딩을 우선한다. 타이머로 완료로 바꾸지 않는다.
|
|
135
|
+
버튼은 별도 live region을 만들지 않으므로 결과 안내가 필요하면 기존 결과 영역 하나로 알린다.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
// Web
|
|
139
|
+
<Button loading={feedback.pending} disabled={!feedback.canSubmit}
|
|
140
|
+
completion={{ confirmed: feedback.currentSaved, label: t("saved") }} onClick={save}>
|
|
141
|
+
{t("save")}
|
|
142
|
+
</Button>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// Native
|
|
147
|
+
<Button loading={feedback.pending} disabled={!feedback.canSubmit}
|
|
148
|
+
completion={{ confirmed: feedback.currentSaved, label: t("saved") }} onPress={save}>
|
|
149
|
+
{t("save")}
|
|
150
|
+
</Button>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
이 옵션의 children은 문자열·숫자 또는 그것들만의 배열이어야 한다. 아이콘은 기존
|
|
154
|
+
leading/trailing을 쓴다. 임의 JSX를 숨겨 복제하면 내부 상태·포커스가 중복되므로 허용하지
|
|
155
|
+
않는다. 옵션 없는 Button의 기존 JSX children 계약은 유지한다.
|
|
156
|
+
Web은 두 숨김 라벨을 같은 grid 영역에 놓아 두 라벨의 최대 고유 폭·높이를 예약한다.
|
|
157
|
+
Native는 두 숨김 라벨을 같은 flow에 항상 두어 Yoga의 최대 고유 폭을 사용하고,
|
|
158
|
+
현재 recipe 행 높이×줄 상한을 초기 높이로 예약한다. 각 라벨의 native layout 높이가
|
|
159
|
+
더 크면 최댓값을 반영한다. 따라서 짧은 라벨만 있어도 Native slot은 기존 줄 상한만큼
|
|
160
|
+
공간을 예약할 수 있다. 폭·글꼴·라벨이 바뀌면 두 라벨을 다시 측정한다.
|
|
161
|
+
상태만 바뀌는 동안에는 같은 라벨 쌍과 아이콘을 유지해야 프레임이 고정된다.
|
|
162
|
+
|
|
163
|
+
Button의 loading은 표시와 UI 재실행을 막는 기능이다. 같은 프레임의 중복 실행·늦은 응답
|
|
164
|
+
무효화는 기존 action-session 또는 제품 mutation이 맡는다. 저장 실패 뒤에는 입력과
|
|
165
|
+
마지막 확정 값을 유지한다. [버튼 완료 피드백](../compositions/action-feedback.md)의
|
|
166
|
+
`resolveSaveFeedback` 조합을 사용한다.
|
|
@@ -73,18 +73,20 @@ import { Calendar } from "@hjmds/react-native/calendar";
|
|
|
73
73
|
| `composeAccessibleName` | `(info: { date: string; isToday: boolean; isSelected: boolean; disabled: boolean; content?: Content }) => string` | 필수 | 날짜 칸의 접근성 이름 |
|
|
74
74
|
| `renderCellContent` | `(cell: ResolvedCalendarDateCell<Content>) => ReactNode` | — | `cell`은 셀 descriptor에 `date`·`row`·`column`·`isToday`·`isSelected`·`selectable`·`accessibleName`이 더해진 값. 결과는 접근성에서 숨는다 |
|
|
75
75
|
| Web `onNavigateBeyondGrid` | `(detail: { date: string; intent: "next-day" \| "previous-day" \| "next-week" \| "previous-week" \| "first-of-week" \| "last-of-week"; overflow: "before" \| "after" }, focusDate: (date: string) => void) => void` | — | 방향키가 격자 밖으로 나갈 때. 제품이 다음 달 grid를 넘긴 뒤 `focusDate`를 부른다 |
|
|
76
|
-
| `size` | `medium` · `large` | `medium` | — |
|
|
76
|
+
| `size` | `compact` · `medium` · `large` | `medium` | — |
|
|
77
77
|
| `ref` | `{ focusDate(date: string): void }` | — | 날짜에 초점을 요청한다 |
|
|
78
78
|
| Web `autoFocus` | `boolean` | `false` | — |
|
|
79
79
|
| `layoutStyle` | margin·width·flex·`alignSelf` | — | 배치 전용. Native도 같은 배치 슬롯을 받는다 |
|
|
80
80
|
|
|
81
81
|
## 배치
|
|
82
82
|
|
|
83
|
+
2026-10-14 야잘알 경기 달력에서 날짜 원이 승패 라벨보다 과하게 커 보인 사용자 피드백으로 `compact`를 추가했다. 날짜 표시는 28px 원으로 줄이고, 실제 터치 영역과 7열 최소 폭은 기존 44px 기준을 유지한다.
|
|
84
|
+
|
|
83
85
|
Native `renderCellContent` 최소 높이는 Provider의 `tokens.typography.label.lineHeight × textScale`다. 날짜의 원형 geometry와 선택 계약은 별도다.
|
|
84
86
|
|
|
85
87
|
| 항목 | 값 | 근거 |
|
|
86
88
|
| --- | --- | --- |
|
|
87
|
-
| 크기 | 날짜
|
|
89
|
+
| 크기 | 날짜 칸은 모든 size에서 최소 44, 선택·오늘 원은 `compact` 28 / 나머지 44(`medium` `control.minTouchTarget`, `large` `glyph.xxl` 44 — large는 글자만 `bodyLarge`로 커진다). 격자 최소 폭 7 × 44 = 308. 월 이동 버튼 44 원(`control.buttonHeight.medium`). 셀 `content`는 날짜 아래 label 한 줄 높이 | `calendarRecipe.sizes`·`header.navButton`, `.hjm-calendar__grid` |
|
|
88
90
|
| 간격 | 머리↔격자, 머리 안 `spacing.xs` 8. 요일 줄 위아래 `spacing.xs` 8. 날짜↔`content` `spacing.xxs` 4. Web 격자 바깥 `spacing.xxs` 4 | `calendarRecipe.header.gap`, `.hjm-calendar*` |
|
|
89
91
|
| 순서·정렬 | 위→아래 [이전 월][월 제목(가운데, title)][다음 월] → 요일 줄 → 주 단위 7열. 화면 본문에 인라인으로 두며 팝업·시트를 열지 않는다 | `.hjm-calendar__header`, `calendarRecipe` 주석(`shared`) |
|
|
90
92
|
| 고정·스크롤 | 폭이 308보다 좁으면 격자가 가로 스크롤한다(Web `.hjm-calendar__viewport` `overflow-x: auto`, Native `ScrollView horizontal`) | `.hjm-calendar__viewport`, `react-native/src/calendar.tsx` |
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Chart
|
|
2
|
+
|
|
3
|
+
- 단계: 컴포넌트
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.20.0 이후)
|
|
7
|
+
- 검토일: 2026-10-15
|
|
8
|
+
- 근거: [Chart](../../chart.md), `src/chart.ts`(`chartRecipe`, `describeChart`, `resolveLineGeometry`), `src/dataviz.ts`(`resolveDatavizSeriesColor`, `datavizChromeTokens`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/차트`
|
|
10
|
+
|
|
11
|
+
## 언제 쓰나
|
|
12
|
+
|
|
13
|
+
수치의 흐름·비교·몫·모양·관계를 그림으로 읽게 할 때 쓴다. 시간에 따른 흐름(기록·가격·순위)은 `LineChart`,
|
|
14
|
+
범주끼리의 크기 비교는 `BarChart`, 전체 안의 몫은 `PieChart`, 여러 능력치의 모양은 `RadarChart`,
|
|
15
|
+
두 수치의 관계는 `ScatterChart`, 목록 행·표 셀 안의 작은 추세는 `Sparkline`을 쓴다.
|
|
16
|
+
여러 계열의 이름과 색을 차트 밖에 따로 보일 때 `ChartLegend`를 쓴다.
|
|
17
|
+
|
|
18
|
+
## 쓰지 않을 때
|
|
19
|
+
|
|
20
|
+
| 상황 | 대신 쓸 것 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| 숫자 지표 하나 또는 몇 개(현재 값·증감) | [Statistic](statistic.md) |
|
|
23
|
+
| 진행률·완료 비율 하나 | [Progress](progress.md) |
|
|
24
|
+
| 1년 단위 날짜별 활동 밀도 | [ActivityHeatmap](activity-heatmap.md) |
|
|
25
|
+
| 정확한 값을 읽고 정렬·비교해야 하는 기록 | [DataTable](data-table.md) |
|
|
26
|
+
| 계열이 8개(`datavizSeriesCount`)를 넘는 비교 | [DataTable](data-table.md) — 팔레트가 순환해 색으로 구분되지 않는다 |
|
|
27
|
+
| 성공·경고처럼 의미가 있는 상태 표시 | [Badge](badge.md)·[Tag](tag.md)·[Notice](notice.md) — 차트는 상태색을 쓰지 않는다 |
|
|
28
|
+
| 캔들스틱·지도·트리맵·이중 y축·확대/이동 | 제품 라이브러리 + `dataviz` 토큰([계약의 넣지 않은 것](../../chart.md#넣지-않은-것)) |
|
|
29
|
+
|
|
30
|
+
## 공개 이름과 import
|
|
31
|
+
|
|
32
|
+
| 이름 | 역할 | Web | Native |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `LineChart` | 기본 — 선·영역·쌓은 영역, 스크럽 | `@hjmds/react/chart` | `@hjmds/react-native/chart` |
|
|
35
|
+
| `BarChart` | 기본 — 세로·가로, 묶음·쌓기 막대 | `@hjmds/react/chart` | `@hjmds/react-native/chart` |
|
|
36
|
+
| `PieChart` | 기본 — 파이·도넛과 몫 범례 | `@hjmds/react/chart` | `@hjmds/react-native/chart` |
|
|
37
|
+
| `RadarChart` | 기본 — 공통 범위 레이더 | `@hjmds/react/chart` | `@hjmds/react-native/chart` |
|
|
38
|
+
| `ScatterChart` | 기본 — 두 수치의 산점도 | `@hjmds/react/chart` | `@hjmds/react-native/chart` |
|
|
39
|
+
| `Sparkline` | 동반 — 축 없는 작은 추세선 | `@hjmds/react/chart` | `@hjmds/react-native/chart` |
|
|
40
|
+
| `ChartLegend` | 보조 — 색 견본 + 이름(+ 값) 범례 행 | `@hjmds/react/chart` | `@hjmds/react-native/chart` |
|
|
41
|
+
|
|
42
|
+
데이터 타입(`ChartSeries`·`ScatterDatum`)과 `describeChart`·`chartRecipe`는 `@hjmds/design-contracts/components/chart`에서,
|
|
43
|
+
팔레트는 `@hjmds/design-contracts/dataviz`에서 가져온다.
|
|
44
|
+
|
|
45
|
+
## 최소 사용 예
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// Web
|
|
49
|
+
import { useState } from "react";
|
|
50
|
+
import { LineChart } from "@hjmds/react/chart";
|
|
51
|
+
import type { ChartSeries } from "@hjmds/design-contracts/components/chart";
|
|
52
|
+
|
|
53
|
+
const series: ChartSeries[] = [
|
|
54
|
+
{ id: "avg", label: t("record.battingAverage"), values: seasons.map((season) => season.avg ?? null) },
|
|
55
|
+
];
|
|
56
|
+
const [focus, setFocus] = useState<number | null>(null);
|
|
57
|
+
const shown = focus ?? seasons.length - 1;
|
|
58
|
+
|
|
59
|
+
<>
|
|
60
|
+
<p>{formatAverage(seasons[shown]?.avg ?? null)}</p>
|
|
61
|
+
<LineChart
|
|
62
|
+
categories={seasons.map((season) => season.label)}
|
|
63
|
+
series={series}
|
|
64
|
+
accessibilityLabel={t("record.battingAverageTrend")}
|
|
65
|
+
formatValue={formatAverage}
|
|
66
|
+
missingLabel={t("record.noRecord")}
|
|
67
|
+
showAxis
|
|
68
|
+
onFocusChange={(index) => setFocus(index)}
|
|
69
|
+
/>
|
|
70
|
+
</>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// Native
|
|
75
|
+
import { BarChart, Sparkline } from "@hjmds/react-native/chart";
|
|
76
|
+
|
|
77
|
+
<>
|
|
78
|
+
<BarChart
|
|
79
|
+
categories={months.map((month) => month.label)}
|
|
80
|
+
series={[{ id: "wins", label: t("team.wins"), values: months.map((month) => month.wins) }]}
|
|
81
|
+
accessibilityLabel={t("team.winsByMonth")}
|
|
82
|
+
formatValue={formatCount}
|
|
83
|
+
missingLabel={t("team.noGame")}
|
|
84
|
+
showValues
|
|
85
|
+
/>
|
|
86
|
+
{/* 옆 텍스트가 값을 말하므로 이름 없이 장식으로 둔다 */}
|
|
87
|
+
<Sparkline values={lastTenGames} />
|
|
88
|
+
</>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 축과 기본값
|
|
92
|
+
|
|
93
|
+
모든 차트 공통: 보이는 문구와 숫자 서식은 제품이 준다(HJM 안에서 번역·서식하지 않는다).
|
|
94
|
+
`series[].values` 길이는 `categories` 길이와 같아야 하고 `null`은 빈 관측이다.
|
|
95
|
+
|
|
96
|
+
### LineChart
|
|
97
|
+
|
|
98
|
+
| prop | 값 | 기본값 | 설명 |
|
|
99
|
+
| --- | --- | --- | --- |
|
|
100
|
+
| `categories` | `readonly string[]` | — (필수) | x축 범주(시간 순서) |
|
|
101
|
+
| `series` | `{ id, label, values: (number \| null)[], color? }[]` | — (필수) | `color`가 없으면 dataviz 팔레트 index 색 |
|
|
102
|
+
| `accessibilityLabel` | `string` | — (필수) | 차트 이름. Web은 `aria-label`로 나간다 |
|
|
103
|
+
| `formatValue` | `(value: number) => string` | — (필수) | 축 눈금·텍스트 대안·포커스 값 서식 |
|
|
104
|
+
| `missingLabel` | `string` | — (필수) | 텍스트 대안에서 빈 값을 읽는 문구 |
|
|
105
|
+
| `height` | `number` | `chartRecipe.heights.regular` 180 | 가로는 부모 폭을 채운다 |
|
|
106
|
+
| `curve` | `linear` · `monotone` · `step` | `linear` | `monotone`은 데이터 밖으로 넘지 않는 곡선 |
|
|
107
|
+
| `area` | `boolean` | 계열이 하나면 `true` | 선 아래 그라데이션(0.32 → 0) |
|
|
108
|
+
| `stacked` | `boolean` | `false` | 쌓은 영역. 0을 항상 포함 |
|
|
109
|
+
| `invert` | `boolean` | `false` | 작을수록 위(순위·평균자책점) |
|
|
110
|
+
| `domain` | `readonly [number, number]` | 관측 범위 | 값 범위 고정 |
|
|
111
|
+
| `includeZero` | `boolean` | `false` | 선 범위에 0을 포함 |
|
|
112
|
+
| `referenceValue` | `number` · `"first"` | — | 점선 가로 기준선(`"first"`는 첫 관측값) |
|
|
113
|
+
| `extremes` | `{ high: string; low: string }` | — | 포커스가 없을 때 첫 계열 최고·최저 표시와 라벨 |
|
|
114
|
+
| `showAxis` | `boolean` | `false` | 왼쪽 y 눈금 + plot 아래 첫·끝 범주 |
|
|
115
|
+
| `focusIndex` | `number \| null` | — | 제어 포커스 |
|
|
116
|
+
| `defaultFocusIndex` | `number \| null` | 마지막 관측 index | 비제어 시작 포커스 |
|
|
117
|
+
| `onFocusChange` | `(index: number \| null, detail: { scrubbing: boolean }) => void` | — | 스크럽 중 `scrubbing: true`, 손을 떼면 `false` |
|
|
118
|
+
| `cursorLabel` | `(index: number) => string` | `categories[index]` | 스크럽 중 커서 위 말풍선 |
|
|
119
|
+
|
|
120
|
+
### BarChart
|
|
121
|
+
|
|
122
|
+
| prop | 값 | 기본값 | 설명 |
|
|
123
|
+
| --- | --- | --- | --- |
|
|
124
|
+
| `categories`·`series`·`accessibilityLabel`·`formatValue`·`missingLabel` | LineChart와 같음 | — (필수) | — |
|
|
125
|
+
| `height` | `number` | `chartRecipe.heights.regular` 180 | — |
|
|
126
|
+
| `orientation` | `vertical` · `horizontal` | `vertical` | 범주 이름이 길면 `horizontal` |
|
|
127
|
+
| `layout` | `grouped` · `stacked` | `grouped` | 범위는 항상 0을 포함 |
|
|
128
|
+
| `domain` | `readonly [number, number]` | 0을 포함한 관측 범위 | — |
|
|
129
|
+
| `showAxis` | `boolean` | `false` | — |
|
|
130
|
+
| `showValues` | `boolean` | `false` | 막대 끝 값 글자 |
|
|
131
|
+
| `focusIndex`·`defaultFocusIndex`·`onFocusChange` | LineChart와 같음 | — | 누름·끌기로 범주 선택, 나머지 막대 불투명도 `chartRecipe.bar.restOpacity` 0.4 |
|
|
132
|
+
|
|
133
|
+
### PieChart
|
|
134
|
+
|
|
135
|
+
| prop | 값 | 기본값 | 설명 |
|
|
136
|
+
| --- | --- | --- | --- |
|
|
137
|
+
| `data` | `{ id, label, value: number \| null, color? }[]` | — (필수) | 0 이하·`null`은 조각에서 빠진다 |
|
|
138
|
+
| `accessibilityLabel` | `string` | — (필수) | — |
|
|
139
|
+
| `formatValue` | `(value: number) => string` | — (필수) | 범례 값 서식 |
|
|
140
|
+
| `donut` | `boolean` | `true` | 안쪽 반지름 비율 `chartRecipe.pie.donutRatio` 0.62 |
|
|
141
|
+
| `size` | `number` | 180 | 정사각 지름 |
|
|
142
|
+
| `centerContent` | `ReactNode` | — | 도넛 가운데(합계 등) |
|
|
143
|
+
| `selectedId` | `string \| null` | — | 제어 선택 |
|
|
144
|
+
| `defaultSelectedId` | `string \| null` | `null` | 비제어 선택 |
|
|
145
|
+
| `onSelect` | `(id: string \| null) => void` | — | 조각·범례 선택 |
|
|
146
|
+
| `showLegend` | `boolean` | `true` | 견본 + 이름 + 서식 값 + 백분율 행 |
|
|
147
|
+
|
|
148
|
+
### RadarChart
|
|
149
|
+
|
|
150
|
+
| prop | 값 | 기본값 | 설명 |
|
|
151
|
+
| --- | --- | --- | --- |
|
|
152
|
+
| `categories` | `readonly string[]` (3개 이상) | — (필수) | 축 이름 |
|
|
153
|
+
| `series`·`accessibilityLabel`·`formatValue`·`missingLabel` | LineChart와 같음 | — (필수) | — |
|
|
154
|
+
| `domain` | `readonly [number, number]` | `[0, 모든 계열 최댓값]` | 모든 축이 같은 범위 |
|
|
155
|
+
| `levels` | `number` | `chartRecipe.radar.levels` 4 | 격자 고리 수 |
|
|
156
|
+
| `size` | `number` | 240 | 정사각 지름 |
|
|
157
|
+
| `showLegend` | `boolean` | 계열이 둘 이상이면 `true` | — |
|
|
158
|
+
|
|
159
|
+
### ScatterChart
|
|
160
|
+
|
|
161
|
+
| prop | 값 | 기본값 | 설명 |
|
|
162
|
+
| --- | --- | --- | --- |
|
|
163
|
+
| `points` | `{ id, label, x, y, seriesIndex? }[]` | — (필수) | — |
|
|
164
|
+
| `accessibilityLabel` | `string` | — (필수) | — |
|
|
165
|
+
| `xLabel`·`yLabel` | `string` | — (필수) | 축 이름 |
|
|
166
|
+
| `formatX`·`formatY` | `(value: number) => string` | — (필수) | 축 눈금·포커스 값 서식 |
|
|
167
|
+
| `height` | `number` | `chartRecipe.heights.tall` 240 | — |
|
|
168
|
+
| `seriesLabels` | `readonly string[]` | — | `seriesIndex`별 범례 이름 |
|
|
169
|
+
| `focusId` | `string \| null` | — | 제어 포커스 |
|
|
170
|
+
| `onFocusChange` | `(id: string \| null) => void` | — | 가장 가까운 점(32 안) 포커스 |
|
|
171
|
+
|
|
172
|
+
### Sparkline · ChartLegend
|
|
173
|
+
|
|
174
|
+
| prop | 값 | 기본값 | 설명 |
|
|
175
|
+
| --- | --- | --- | --- |
|
|
176
|
+
| `values`(`Sparkline`) | `readonly (number \| null)[]` | — (필수) | 빈 값은 끊김 |
|
|
177
|
+
| `color`(`Sparkline`) | `string` | 첫 dataviz 계열색 | — |
|
|
178
|
+
| `invert`(`Sparkline`) | `boolean` | `false` | 작을수록 위 |
|
|
179
|
+
| `domain`(`Sparkline`) | `readonly [number, number]` | 관측 범위 | 여러 행의 스파크라인을 같은 척도로 비교할 때 고정 |
|
|
180
|
+
| `width`·`height`(`Sparkline`) | `number` | 72 · 28 | `chartRecipe.sparkline` |
|
|
181
|
+
| `accessibilityLabel`(`Sparkline`) | `string` | — | 없으면 장식(보조 기술에서 숨김) |
|
|
182
|
+
| `items`(`ChartLegend`) | `{ id, label, color, value?: string }[]` | — (필수) | `value`는 서식이 끝난 문자열 |
|
|
183
|
+
|
|
184
|
+
## 배치
|
|
185
|
+
|
|
186
|
+
| 항목 | 값 | 근거 |
|
|
187
|
+
| --- | --- | --- |
|
|
188
|
+
| 크기 | 선·막대·산점도는 부모 폭을 채우고 높이는 `chartRecipe.heights`(`compact` 128 · `regular` 180 · `tall` 240)에서 고른다. 파이 180, 레이더 240 정사각. Sparkline 72 × 28 | `src/chart.ts` `chartRecipe.heights`·`sparkline` |
|
|
189
|
+
| 간격 | plot 안쪽 여백: 축 없음 위 `spacing.lg` 20 · 나머지 `spacing.xs` 8, 축 있음 위 `spacing.md` 16 · 아래 `spacing.lg` 20 · 왼쪽 36(y 눈금 칸). 차트와 위 제목·현재 값, 아래 범례 사이는 부모 [Stack](stack.md)의 gap으로 준다 | `chartRecipe.insets` |
|
|
190
|
+
| 순서·정렬 | 위에서 제목([Section](section.md)·[Heading](heading.md)) → 현재 값 텍스트(제품, `onFocusChange`로 갱신) → 차트 → 범례. y 눈금은 왼쪽, 첫·끝 범주는 plot 아래 양끝 | [계약 스크럽 절](../../chart.md#스크럽linechart) |
|
|
191
|
+
| 고정·스크롤 | 차트는 스크롤하지 않는다. 가로로 끌면 스크럽, 세로로 끌면 페이지 스크롤이다. 가로 스크롤 컨테이너·[Carousel](carousel.md) 안에 `LineChart`를 두지 않는다(제스처 충돌) | 계약 스크럽 3단계 |
|
|
192
|
+
| 좁은 폭·큰 글자 | 높이는 `height`로 고정되고 가로만 줄어든다. 축 글자는 caption 변형(`chartRecipe.chrome.labelVariant`)이다. 범주가 많아도 축에는 첫·끝 범주만 나오므로 범주 이름 길이에 배치가 흔들리지 않는다. 범주 이름이 긴 막대는 `orientation="horizontal"` | `datavizChromeTokens` |
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
┌ Section 제목 ───────────────────────────────┐
|
|
196
|
+
│ 0.312 (현재 값, 제품 텍스트) │
|
|
197
|
+
│ ┌ LineChart ─────────────────── height 180 ┐ │
|
|
198
|
+
│ │ .350┤ ╭─●╮ ┆ (커서) │ │
|
|
199
|
+
│ │ .300┤ ╭──╮ ╱ ╰───┆── │ │
|
|
200
|
+
│ │ .250┤─╯ ╰─╯ ┆ │ │
|
|
201
|
+
│ │ 2021 2025 │ │
|
|
202
|
+
│ └──────────────────────────────────────────┘ │
|
|
203
|
+
│ ● 타율 ● 출루율 (ChartLegend) │
|
|
204
|
+
└──────────────────────────────────────────────┘
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## 꼭 지킬 것
|
|
208
|
+
|
|
209
|
+
- 모든 차트에 `accessibilityLabel`을 준다. 값의 텍스트 대안은 `formatValue`·`missingLabel`로 `describeChart`가 만든다.
|
|
210
|
+
숫자 서식·빈 값 문구는 i18n과 제품 formatter로 공급한다.
|
|
211
|
+
- 빈 관측은 `null`로 넘긴다. 0으로 채우면 "기록 0"이 되고, 앞뒤 값을 이어 붙이면 없는 추세가 생긴다.
|
|
212
|
+
- 계열 색은 제품 `color` 또는 dataviz 팔레트만 쓴다. `success`·`warning` 같은 상태색을 계열색으로 쓰지 않는다.
|
|
213
|
+
- 순위·평균자책점처럼 작을수록 좋은 값은 `invert`로 그린다(데이터를 음수로 바꾸지 않는다).
|
|
214
|
+
- 막대·쌓기의 0 기준선을 `domain`으로 빼지 않는다. 막대 길이가 값의 크기다.
|
|
215
|
+
- 현재 값 텍스트는 차트 밖에 두고 `onFocusChange`로 갱신한다. 차트 안 툴팁에 제품 컴포넌트를 넣는 경로는 없다.
|
|
216
|
+
- 레이더는 한 척도의 값만 넣는다. 단위가 다른 축을 섞어야 하면 [DataTable](data-table.md)이나 막대 여러 개를 쓴다.
|
|
217
|
+
|
|
218
|
+
## 플랫폼 차이
|
|
219
|
+
|
|
220
|
+
| 항목 | Web | Native |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| 그리기 | SVG DOM | `react-native-svg` |
|
|
223
|
+
| 스크럽 입력 | pointer event + `touch-action: pan-y` | `PanResponder`, `\|dy\| > \|dx\|`이면 부모 스크롤에 응답권을 넘김 |
|
|
224
|
+
| 끌지 않는 탐색 | `role="slider"`, `aria-valuetext`, ArrowLeft/ArrowRight/Home/End | `accessibilityRole="adjustable"`, increment/decrement |
|
|
225
|
+
| 차트 이름 | `aria-label` | `accessibilityLabel` |
|
|
226
|
+
|
|
227
|
+
## 함정
|
|
228
|
+
|
|
229
|
+
- `onFocusChange`는 손을 뗄 때도 불린다(`scrubbing: false`, 기본 포커스 index). 이 호출을 "사용자가 고른 값"으로 저장하면 스크럽이
|
|
230
|
+
끝날 때마다 마지막 값으로 덮인다. `detail.scrubbing`으로 구분한다.
|
|
231
|
+
- 스크럽과 화살표 이동은 관측값이 있는 index만 지난다. 모든 계열이 `null`인 범주에는 포커스가 가지 않는다.
|
|
232
|
+
- 2026-10-14 작성 시점에 Web·Native renderer와 Storybook 항목은 구현 중이다. prop 표는 공유 사양 기준이며 renderer가 들어온 뒤
|
|
233
|
+
실제 Props 타입과 다시 대조한다.
|
|
234
|
+
|
|
235
|
+
## 도넛 범주를 실제로 제외하기
|
|
236
|
+
|
|
237
|
+
범주를 강조하려면 selectedId/onSelect, 분모에서 빼려면 exclusion을 쓴다. 기존 강조 API의 의미는 바꾸지 않았다.
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
// Web — Native는 @hjmds/react-native/chart에서 같은 PieChart를 가져온다.
|
|
241
|
+
<PieChart data={data} accessibilityLabel={t("share")} formatValue={formatCount}
|
|
242
|
+
exclusion={{ excludedIds, onExcludedIdsChange: setExcludedIds,
|
|
243
|
+
label: (datum, included) => t(included ? "included" : "excluded", { name: datum.label }) }}
|
|
244
|
+
centerContent={summary => <span>{formatCount(summary.total)}</span>} />;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Native centerContent에는 Text 등 Native 노드를 쓴다. 모두 제외하거나 합계 0이면 조각을 그리지 않는다.
|
|
248
|
+
항목 ID·색 인덱스는 그대로 보존하고 실제 포함값으로 비율을 재계산한다. 차트 의미 전달에는 수치·checkbox 상태도 사용한다.
|
|
249
|
+
|
|
250
|
+
## 데이터 전환과 스크럽 분할 강조
|
|
251
|
+
|
|
252
|
+
실제 기간 필터 결과를 교체할 때 `LineChart`·`BarChart`에 `animateChanges`를 준다.
|
|
253
|
+
`LineChart`는 `scrubEmphasis="before"`(또는 `"after"`)로 포커스 한쪽만 계열 색으로
|
|
254
|
+
강조할 수 있다. 축·접근성 값은 최신 데이터이며 전환 경로 자체는 관측값이 아니다.
|
|
255
|
+
고주기 스트림에서는 기본값 false를 유지한다. 큰 자료(선 구간 양쪽 합계 1,024점 초과,
|
|
256
|
+
막대 200개 초과)·reduced motion은 즉시 교체한다. 제어 포커스는 제품이 범주 변경 후
|
|
257
|
+
새 index로 맞추고, 비제어 포커스는 같은 범주 레이블을 따라간다.
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
// Web: API가 돌려준 기간별 실제 데이터다.
|
|
261
|
+
<LineChart categories={period.labels} series={period.series}
|
|
262
|
+
accessibilityLabel={t("trend")} formatValue={formatValue} missingLabel={t("missing")}
|
|
263
|
+
animateChanges scrubEmphasis="before" holdOnRelease />
|
|
264
|
+
<BarChart categories={comparison.labels} series={comparison.series}
|
|
265
|
+
accessibilityLabel={t("comparison")} formatValue={formatValue} missingLabel={t("missing")}
|
|
266
|
+
animateChanges showAxis />
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// Native: 같은 데이터와 옵션을 @hjmds/react-native/chart에 전달한다.
|
|
271
|
+
<LineChart categories={period.labels} series={period.series}
|
|
272
|
+
accessibilityLabel={t("trend")} formatValue={formatValue} missingLabel={t("missing")}
|
|
273
|
+
animateChanges scrubEmphasis="before" holdOnRelease />
|
|
274
|
+
<BarChart categories={comparison.labels} series={comparison.series}
|
|
275
|
+
accessibilityLabel={t("comparison")} formatValue={formatValue} missingLabel={t("missing")}
|
|
276
|
+
animateChanges showAxis />
|
|
277
|
+
```
|
|
@@ -2,34 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
- 단계: 컴포넌트
|
|
4
4
|
- 상태: 배포
|
|
5
|
-
- 지원: Web
|
|
5
|
+
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.12.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
8
|
-
- 근거: [DataTable](../../data-table.md), `src/data-table.ts`(`dataTableRecipe`, `getNextDataTableSortState`)
|
|
9
|
-
- 스토리북: `배포/컴포넌트/데이터 표시/데이터 표`
|
|
7
|
+
- 검토일: 2026-10-14
|
|
8
|
+
- 근거: [DataTable](../../data-table.md), `src/data-table.ts`(`dataTableRecipe`, `getNextDataTableSortState`, `sortDataTableRows`, `splitPinnedDataTableColumns`, `resolveDataTableHeaderBands`, `resolveDataTableBody`, `percentileRank`, `percentileHeat`, `dataTableFamilyRecipe`, `resolveLineScore`, `resolveMatrix`, `resolveComparisonRow`)
|
|
9
|
+
- 스토리북: `배포/컴포넌트/데이터 표시/데이터 표`, `배포/컴포넌트/데이터 표시/라인스코어`, `배포/컴포넌트/데이터 표시/대전표`, `배포/컴포넌트/데이터 표시/비교 표`
|
|
10
10
|
|
|
11
11
|
## 언제 쓰나
|
|
12
12
|
|
|
13
|
-
여러 행의 데이터를 열로 맞춰 훑고, 열 기준으로 정렬하거나 행을 골라 일괄 작업할 때
|
|
14
|
-
|
|
15
|
-
선택·정렬이 없는 단순 표는 companion `Table`을 쓴다.
|
|
13
|
+
여러 행의 데이터를 열로 맞춰 훑고, 열 기준으로 정렬하거나 행을 골라 일괄 작업할 때 쓴다.
|
|
14
|
+
DataTable은 다음 정렬 상태와 선택 상태만 판정하고 행을 재배열하지 않는다. 로컬 배열 정렬은 계약의 선택형 helper
|
|
15
|
+
`sortDataTableRows`로, 서버 정렬·필터·페이지 나누기는 제품이 한다. 선택·정렬이 없는 단순 Web 표는 companion `Table`을 쓴다.
|
|
16
|
+
Native renderer(기록 표: 고정 열 + 가로 스크롤)는 2026-10-14 추가분이며 미게시(1.20.0 이후)다.
|
|
16
17
|
|
|
17
18
|
## 쓰지 않을 때
|
|
18
19
|
|
|
19
20
|
| 상황 | 대신 쓸 것 |
|
|
20
21
|
| --- | --- |
|
|
21
22
|
| 한 대상의 속성(이름–값) 나열 | [DescriptionList](description-list.md) |
|
|
22
|
-
| 행 하나가 하나의 항목인
|
|
23
|
+
| 행 하나가 하나의 항목인 목록(열 비교가 아님) | [List](list.md), [ListRow](list-row.md) |
|
|
24
|
+
| 값의 흐름·비교를 그림으로 읽기 | [Chart](chart.md) |
|
|
23
25
|
| 아주 긴 목록의 가상 스크롤 | [VirtualList](virtual-list.md) |
|
|
24
26
|
| 행 확장(상세 펼침) | [Accordion](accordion.md)·[Collapsible](collapsible.md)을 행 안에 합성 |
|
|
25
|
-
| Native
|
|
27
|
+
| Native에서 여러 행 체크 선택·일괄 작업 | Native는 행 하나 강조(`onRowPress`·`selectedRowId`)만 있다. [List](list.md)의 선택을 쓴다 |
|
|
26
28
|
|
|
27
29
|
## 공개 이름과 import
|
|
28
30
|
|
|
29
31
|
| 이름 | 역할 | Web | Native |
|
|
30
32
|
| --- | --- | --- | --- |
|
|
31
|
-
| `DataTable` | 기본 — 정렬·행 선택·비동기 상태를 갖는 표 | `@hjmds/react`, `/data-table` |
|
|
33
|
+
| `DataTable` | 기본 — 정렬·행 선택·비동기 상태를 갖는 표 | `@hjmds/react`, `/data-table` | `@hjmds/react-native/data-table` |
|
|
32
34
|
| `Table` | 동반 — 행 객체와 `cell` 렌더러를 받는 단순 표 | `@hjmds/react`, `/display` | 없음 |
|
|
35
|
+
| `LineScore` | 동반 — 회별 점수 + 합계 열(9회 채움·연장 확장·진행 중 회 강조) | `@hjmds/react/data-table` | `@hjmds/react-native/data-table` |
|
|
36
|
+
| `MatrixTable` | 동반 — 같은 id 목록의 행×열 교차표(상대 전적), 대각선은 빈 칸 | `@hjmds/react/data-table` | `@hjmds/react-native/data-table` |
|
|
37
|
+
| `ComparisonTable` | 동반 — 두 대상의 지표를 행마다 값 + 막대로 비교 | `@hjmds/react/data-table` | `@hjmds/react-native/data-table` |
|
|
38
|
+
|
|
39
|
+
정렬 helper(`getNextDataTableSortState`·`dataTableFirstSortDirection`·`toDataTableSortValue`·`compareDataTableValues`·
|
|
40
|
+
`sortDataTableRows`·`splitPinnedDataTableColumns`)는 `@hjmds/design-contracts/components/data-table`에서 가져온다.
|
|
33
41
|
|
|
34
42
|
## 최소 사용 예
|
|
35
43
|
|
|
@@ -59,26 +67,67 @@ import { DataTable } from "@hjmds/react/data-table";
|
|
|
59
67
|
/>
|
|
60
68
|
```
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
```tsx
|
|
71
|
+
// Native
|
|
72
|
+
import { useState } from "react";
|
|
73
|
+
import { DataTable } from "@hjmds/react-native/data-table";
|
|
74
|
+
import {
|
|
75
|
+
sortDataTableRows,
|
|
76
|
+
toDataTableSortValue,
|
|
77
|
+
type DataTableSortState,
|
|
78
|
+
} from "@hjmds/design-contracts/components/data-table";
|
|
79
|
+
|
|
80
|
+
const columns = [
|
|
81
|
+
{ id: "name", header: t("record.col.player"), pinned: "start" as const },
|
|
82
|
+
{ id: "avg", header: t("record.col.avg"), align: "end" as const, sortable: true, sortDescFirst: true },
|
|
83
|
+
{ id: "era", header: t("record.col.era"), align: "end" as const, sortable: true },
|
|
84
|
+
];
|
|
85
|
+
const [sort, setSort] = useState<DataTableSortState<string>>({ columnId: "avg", direction: "descending" });
|
|
86
|
+
// 표시 문자열("0.340", "—")에서 정렬 키를 만들고, 빈 값은 방향과 상관없이 마지막이다.
|
|
87
|
+
const sorted = sortDataTableRows(players, sort, (player, columnId) => toDataTableSortValue(player.cells[columnId]));
|
|
88
|
+
|
|
89
|
+
<DataTable
|
|
90
|
+
columns={columns}
|
|
91
|
+
rows={sorted.map((player) => ({ id: player.id }))}
|
|
92
|
+
renderCell={(rowId, columnId) => cellText(rowId, columnId)}
|
|
93
|
+
labels={{ table: t("record.table"), sortColumn: (header) => t("record.sortBy", { header }) }}
|
|
94
|
+
sortState={sort}
|
|
95
|
+
onSortChange={setSort}
|
|
96
|
+
onRowPress={(rowId) => openPlayer(rowId)}
|
|
97
|
+
summaryRow={(columnId) => totalText(columnId)}
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
63
100
|
|
|
64
101
|
## 축과 기본값
|
|
65
102
|
|
|
66
103
|
| prop | 값 | 기본값 | 설명 |
|
|
67
104
|
| --- | --- | --- | --- |
|
|
68
|
-
| `columns` | `{ id, header: string, align?, sortable?, width? }[]` | — (필수, 하나 이상) | `id`는 비어 있지 않고 중복 없음 |
|
|
105
|
+
| `columns` | `{ id, header: string, align?, sortable?, width?, sortDescFirst?, pinned? }[]` | — (필수, 하나 이상) | `id`는 비어 있지 않고 중복 없음 |
|
|
69
106
|
| `rows` | `{ id, disabled? }[]` | — (필수) | 셀 내용은 `renderCell`이 그린다 |
|
|
70
107
|
| `renderCell` | `(rowId: RowKey, columnId: ColumnKey) => ReactNode` | — (필수) | 행·열 id로 셀을 그린다 |
|
|
71
|
-
| `labels` | `{ table: string; selectAll: string; selectRow: (rowId: string) => string; sortColumn: (header: string, direction: DataTableSortState) => string }` | — (필수) | 모든 문구는 i18n
|
|
108
|
+
| `labels` | `{ table: string; selectAll: string; selectRow: (rowId: string) => string; sortColumn: (header: string, direction: DataTableSortState) => string }` | — (필수) | 모든 문구는 i18n 키. Native는 `table`·`sortColumn`만 받는다 |
|
|
72
109
|
| `sortState` | `{ columnId, direction: "ascending" \| "descending" } \| null` | `null` | 제어 전용(내부 상태 없음) |
|
|
73
110
|
| `onSortChange` | `(next: DataTableSortState<ColumnKey>) => void` | — | 다음 정렬 상태(`null` = 정렬 없음)를 알린다. 재정렬은 제품이 한다 |
|
|
74
|
-
| `sortCycle` | `three-state` · `two-state` | `three-state` | `three-state`는
|
|
75
|
-
| `selection` | `{ mode: "none" }` · `{ mode: "single", selectedKey: Key \| null, onSelectionChange: (key: Key \| null) => void, disallowEmptySelection? }` · `{ mode: "multiple", selectedKeys: ReadonlySet<Key>, onSelectionChange: (keys: ReadonlySet<Key>) => void }` | 없음(선택 열 없음) | 제어(`selectedKey(s)`) 대신 `defaultSelectedKey(s)`를 주면 비제어로 내부 상태가 유지된다 |
|
|
111
|
+
| `sortCycle` | `three-state` · `two-state` | `three-state` | `three-state`는 첫 방향 → 반대 방향 → 정렬 없음. 첫 방향은 열 `sortDescFirst`가 정한다(기본 오름차순) |
|
|
112
|
+
| `selection` | `{ mode: "none" }` · `{ mode: "single", selectedKey: Key \| null, onSelectionChange: (key: Key \| null) => void, disallowEmptySelection? }` · `{ mode: "multiple", selectedKeys: ReadonlySet<Key>, onSelectionChange: (keys: ReadonlySet<Key>) => void }` | 없음(선택 열 없음) | Web만. 제어(`selectedKey(s)`) 대신 `defaultSelectedKey(s)`를 주면 비제어로 내부 상태가 유지된다 |
|
|
76
113
|
| `asyncState` | `{ status: "idle" }` · `{ status: "loading" \| "loadingMore" \| "empty" \| "error", message: string }` | `{ status: "idle" }` | `error`는 `role="alert"`, 나머지는 `role="status"`로 표 위에 나온다 |
|
|
77
114
|
| `density` | `regular` · `compact` | Provider 밀도 | 지정하지 않으면 Provider 밀도를 따른다(`comfortable` → `regular`) |
|
|
78
115
|
| 열 `align` | `start` · `center` · `end` | `start` | 머리 칸과 본문 칸 모두에 적용 |
|
|
79
116
|
| 열 `sortable` | `boolean` | `false` | — |
|
|
80
117
|
| 열 `width` | 양수 px | — | 힌트 |
|
|
118
|
+
| 열 `sortDescFirst` | `boolean` | `false` | 첫 누름이 내림차순(높은 값 먼저). 낮을수록 좋은 열(평균자책점·순위)은 끈다. 미게시(1.20.0 이후) |
|
|
119
|
+
| 열 `pinned` | `"start"` | — | 가로 스크롤 중에도 보이는 열. 시작 쪽에서 끊기지 않고 이어진 열만 고정된다. 미게시(1.20.0 이후) |
|
|
120
|
+
| `summaryRow` | `(columnId: ColumnKey) => ReactNode` | — | 표 끝 합계·평균 행(Web `tfoot`). 값 계산·문구는 제품. 미게시(1.20.0 이후) |
|
|
121
|
+
| `onRowPress`(Native) | `(rowId: RowKey) => void` | — | 행 누름. 미게시(1.20.0 이후) |
|
|
122
|
+
| `selectedRowId`(Native) | `RowKey \| null` | — | 강조할 행 하나. 미게시(1.20.0 이후) |
|
|
123
|
+
| `emptyLabel`(Native) | `string` | — | 본문 상태 문구(제품 문구) |
|
|
81
124
|
| `footer` | `ReactNode` | — | 표 아래 합성 영역 |
|
|
125
|
+
| `columnGroups` | `{ id, header: string, columns: ColumnKey[] }[]` | — | 잎 머리글 위 묶음 머리글(타격 \| 주루). 묶음 열은 이어져야 하고, 묶이지 않은 열은 빈 칸. Web은 `scope="colgroup"`. 미게시(1.20.0 이후) |
|
|
126
|
+
| `rowGroups` | `{ id, label: string, rows: RowKey[] }[]` | — | 행 묶음(선발 / 교체). 묶음마다 이름 행이 먼저 나오고, 묶이지 않은 행은 맨 앞. Web은 묶음마다 `tbody` + `scope="rowgroup"`. 미게시(1.20.0 이후) |
|
|
127
|
+
| 행 `depth` | `0`–`3` | `0` | 하위 행 들여쓰기. 첫 열에 `dataTableFamilyRecipe.depthIndent`(16) × depth |
|
|
128
|
+
| `cellStyle` | `(rowId, columnId) => { tone?, heat? } \| null` | — | 셀 강조. `tone`: `leader` · `positive` · `negative` · `muted`, `heat`: `{ side: "high" \| "low", strength: 1 \| 2 \| 3 }`(백분위 색). 판정은 제품(`percentileRank`·`percentileHeat`). 미게시(1.20.0 이후) |
|
|
129
|
+
| `heatColors` | `{ high: string; low: string }` | 높음 `danger` 빨강 · 낮음 `info` 파랑 | heat 기준색. 칠할 때 `heatAlpha`(0.14·0.26·0.42)를 곱한다 |
|
|
130
|
+
| `maxBodyHeight` | 양수 px | — | 높이 제한. 머리글은 고정되고 본문만 세로 스크롤 |
|
|
82
131
|
| `layoutStyle` | 배치 전용 style(여백·폭·grid 위치) | — | 루트 wrapper에 붙는다. 시각 키는 받지 않는다 |
|
|
83
132
|
|
|
84
133
|
## 배치
|
|
@@ -88,8 +137,8 @@ Native: 없음. Native renderer가 없다.
|
|
|
88
137
|
| 크기 | 본문 폭을 채운다(표 `inline-size: 100%`). 선택 열 폭 `control.minTouchTarget` 44, 정렬 버튼·체크 상자 터치 영역 44 | `packages/react/src/styles.css` `.hjm-data-table*`, `src/data-table.ts` |
|
|
89
138
|
| 간격 | 루트 세로 간격 `spacing.sm` 12. 셀 여백(`dataTableRecipe.density`): `regular` 세로 `spacing.sm` 12 · 가로 `spacing.md` 16, `compact` 세로 `spacing.xs` 8 · 가로 `spacing.sm` 12. `footer` 간격 `spacing.sm` 12 | `packages/react/src/styles.css` |
|
|
90
139
|
| 순서·정렬 | 위에서 [상태 메시지] → 표 → [footer]. 선택 열은 맨 앞. 숫자·금액 열은 `align: "end"`로 끝 정렬하고 머리 행 정렬도 열 `align`을 따른다. 셀은 위 정렬. `footer`는 한 줄 flex(`space-between`, 넘치면 줄바꿈)이며 요약을 앞에, [Pagination](pagination.md)을 끝에 둔다 | `packages/react/src/data-table.tsx` |
|
|
91
|
-
| 고정·스크롤 | 가로 스크롤
|
|
92
|
-
| 좁은 폭·큰 글자 |
|
|
140
|
+
| 고정·스크롤 | `maxBodyHeight`를 주면 Web은 스크롤 상자(`max-block-size`, `overflow: auto`) 안에서 `thead`를 `sticky`로, Native는 머리글을 본문 밖에 두고 본문만 세로 `ScrollView`에 넣는다. 그 밖의 Web 표는 가로 스크롤 컨테이너를 그리지 않고 긴 글자를 줄바꿈한다. `pinned: "start"` 열은 `position: sticky`로 시작 쪽에 붙는다. Native는 고정 열을 제자리에 두고 나머지 열을 가로 `ScrollView`에 넣으며 스크롤 가장자리에 흐림을 둔다 | `packages/react/src/styles.css`, `react-native/src/data-table.tsx`, `splitPinnedDataTableColumns` |
|
|
141
|
+
| 좁은 폭·큰 글자 | 열 비교가 목적인 기록 표는 열을 빼지 말고 이름 열을 `pinned: "start"`로 고정해 가로 스크롤로 보존한다. 행 하나가 하나의 항목이면 [List](list.md)로 바꾼다 | [계약](../../data-table.md#정렬-helper와-native-renderer--2026-10-14) |
|
|
93
142
|
|
|
94
143
|
```text
|
|
95
144
|
┌ DataTable ───────────────────────────────────────┐
|
|
@@ -106,9 +155,28 @@ Native: 없음. Native renderer가 없다.
|
|
|
106
155
|
- 열 `id`·행 `id`는 비어 있지 않고 중복이 없어야 한다. 열은 하나 이상. 어기면 렌더 중 `TypeError`가 난다.
|
|
107
156
|
- `sortState`는 `sortable: true`인 열만 가리킨다. 아니면 `TypeError`.
|
|
108
157
|
- `labels`의 모든 문구는 i18n 키로 만든다. `selectRow`는 행 `id`를 받으므로 사람이 읽을 이름으로 바꿔 돌려준다.
|
|
109
|
-
- 받은 `onSortChange` 값으로 행을 재정렬하는 일은 제품이
|
|
158
|
+
- 받은 `onSortChange` 값으로 행을 재정렬하는 일은 제품이 한다. 로컬 배열은 `sortDataTableRows`를 쓰고 같은 규칙(숫자 해석·빈 값
|
|
159
|
+
마지막·안정 정렬)을 제품 코드에 다시 만들지 않는다. 서버 정렬은 쿼리로 넘긴다.
|
|
160
|
+
- `sortDescFirst` 열은 제품이 `onSortChange` 밖에서 첫 상태를 만들 때도 `dataTableFirstSortDirection(column)`을 쓴다.
|
|
110
161
|
- 페이지네이션·더 보기는 `footer`에 [Pagination](pagination.md)·[LoadMore](load-more.md)로 합성한다.
|
|
111
162
|
- 배치는 `layoutStyle`, 시각 override는 `className`으로만 한다. 행 hover·선택 색을 덮지 않는다.
|
|
163
|
+
- 백분위 색은 같은 열 안의 비교다. `percentileRank(value, 열 전체 값, { lowerIsBetter })`로 열마다 따로 구하고, 낮을수록 좋은
|
|
164
|
+
지표(평균자책·도루자)는 `lowerIsBetter`를 켠다. 색만으로 뜻을 전하지 않는다 — 셀 글자는 그대로 값이다.
|
|
165
|
+
- `LineScore`·`MatrixTable`·`ComparisonTable`은 `accessibilityLabel`(Web은 `aria-label`)이 필수다. 문구는 i18n 키로 만든다.
|
|
166
|
+
|
|
167
|
+
## 플랫폼 차이
|
|
168
|
+
|
|
169
|
+
| 항목 | Web | Native |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| 행 선택 | `selection`(checkbox·radio 열, 전체 선택 tri-state) | `onRowPress` + `selectedRowId` 행 하나 강조. 선택 열 없음 |
|
|
172
|
+
| 넓은 표 | 표가 폭을 채우고 긴 글자는 줄바꿈, `pinned` 열은 sticky | `pinned` 열 고정 + 나머지 가로 `ScrollView`, 가장자리 흐림 |
|
|
173
|
+
| 요약 행 | `summaryRow` → `tfoot` | `summaryRow` → 본문 아래 행 |
|
|
174
|
+
| 접근성 | `role="table"`, 머리 칸 안 정렬 버튼 + `aria-sort` | 정렬 머리 칸 `button`, 머리 행 `header`, 선택 행 `selected` |
|
|
175
|
+
| companion | `Table` | 없음 |
|
|
176
|
+
| 묶음 머리글·행 묶음 | 머리 행 하나 더(`colgroup`) · 묶음마다 `tbody` + `rowgroup` 머리 칸 | 머리 띠 · 묶음 이름 행(`header`) |
|
|
177
|
+
| 높이 제한 | 스크롤 상자 + `thead` sticky | 머리글 분리 + 본문 세로 `ScrollView` |
|
|
178
|
+
| `LineScore`·`MatrixTable` | `<table>`, 진행 중 회 머리 칸 `aria-current="true"`, 대각선 칸 `aria-hidden` | 팀 행마다 합계를 읽는 접근성 이름, 셀 단위 이름 |
|
|
179
|
+
| `ComparisonTable` | `section` + `role="list"`, 행마다 화면 읽기 문장 하나 | 행마다 접근성 이름 하나 |
|
|
112
180
|
|
|
113
181
|
## 함정
|
|
114
182
|
|
|
@@ -120,3 +188,9 @@ Native: 없음. Native renderer가 없다.
|
|
|
120
188
|
따로 필요하면 [EmptyState](empty-state.md)로 표를 대신 그린다.
|
|
121
189
|
- `Table`의 `onSortChange`는 `null`을 받지 못해 항상 two-state로 돈다. `emptyState`는 필수 prop이다.
|
|
122
190
|
- 1.13.0 변경: 1.12.1까지 Web `regular` 셀은 사방 `spacing.sm` 12였다. 열 폭을 그 값으로 맞춘 화면은 가로 16에서 다시 확인한다.
|
|
191
|
+
- `pinned: "start"`는 맨 앞부터 이어진 열에만 적용된다. 가운데 열에 주면 무시된다(`splitPinnedDataTableColumns`).
|
|
192
|
+
- `toDataTableSortValue`는 글자가 둘 이상이면 문자열로 본다. "7승0무3패"는 문자열 정렬이 되므로 승수로 정렬하려면
|
|
193
|
+
`getValue`에서 숫자를 직접 돌려준다.
|
|
194
|
+
- 2026-10-14 작성 시점에 Native renderer와 Native Storybook 등록은 구현 중이다. Native 행은 renderer가 들어온 뒤 실제 Props와 다시 대조한다.
|
|
195
|
+
- `cellStyle`은 셀마다 호출된다. 백분위 표본(열 전체 값)은 함수 밖에서 한 번 만들어 두고 셀에서는 조회만 한다.
|
|
196
|
+
- `columnGroups`의 묶음은 고정 열 경계를 넘지 않게 나눈다. Web은 묶음 안 열이 모두 고정일 때만 묶음 머리 칸도 고정된다.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
- 상태: 배포
|
|
5
5
|
- 지원: Web · Native
|
|
6
6
|
- 적용: 1.17.1
|
|
7
|
-
- 검토일: 2026-10-
|
|
7
|
+
- 검토일: 2026-10-14
|
|
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
|
|
|
@@ -97,6 +97,8 @@ import { ScreenLayout } from "@hjmds/react-native/screens";
|
|
|
97
97
|
|
|
98
98
|
## 꼭 지킬 것
|
|
99
99
|
|
|
100
|
+
- Native에서 `headerAlignment="center"` + `titleVisibility="hidden"`이고 설명·사용자 지정 헤더가 없으면 아이콘 헤더는 최소 48pt, 세로 여백 2pt다. 2026-10-14 야잘알의 상단 버튼–탭 사이 빈 공간 피드백으로 제목용 60pt 높이를 줄였다. `notice`가 없을 때만 본문 상단 여백을 최대 4pt로 줄이며, `notice`에 둔 탭과 본문 카테고리 사이 기본 16pt 간격은 유지한다. 좌우 여백·하단 여백·터치 영역은 그대로다.
|
|
101
|
+
|
|
100
102
|
- Web의 화면 스크롤 본문은 Tab으로 진입할 수 있다. `scroll="content"`의 정상 본문은 자식이 키보드 스크롤을 소유하지만, 오류·빈 상태 등 대체 안내는 화면 스크롤로 바뀌며 Tab 진입도 함께 복원된다.
|
|
101
103
|
- Web에서 본문에 초점을 가진 입력·재시도 버튼이 상태 전환으로 제거되면 같은 본문으로 초점을 복구한다. `scroll="content"`의 본문은 이 복구만을 위한 `tabIndex=-1`을 가지며 일반 Tab 순서에는 추가되지 않는다. 사용자가 다른 컨트롤이나 바깥 영역으로 초점을 옮긴 경우에는 이동시키지 않는다. 2026-10-13 재시도·권한 제한 전환에서 초점이 body로 유실된 경로를 수정했다.
|
|
102
104
|
- 가상화 목록(FlatList, VirtualList)을 넣으면 `scroll="content"`로 둔다. 스크롤을 이중으로 중첩하지 않는다.
|
|
@@ -141,3 +141,28 @@ import { Tabs } from "@hjmds/react-native/navigation";
|
|
|
141
141
|
- Native `Tabs`는 예전 `options` prop을 받으면 `TypeError`("options was removed")를 던진다. `items`로 옮긴다.
|
|
142
142
|
- 외부 `TabPanel`을 쓸 때 Tabs에 `id`를 주지 않으면 Web은 생성 id를 써서 `tabsId`를 맞출 수 없다.
|
|
143
143
|
`id`를 명시하고 같은 값을 `TabPanel tabsId`에 넘긴다.
|
|
144
|
+
|
|
145
|
+
## 가로 페이저와 연속 밑줄 연결 (미게시)
|
|
146
|
+
|
|
147
|
+
실제 페이저의 소수 index를 `swipePosition`에 발행한다. 별도 state 갱신 없이 기존
|
|
148
|
+
측정된 탭 밑줄만 따라간다. 첫 항목은 0, 두 번째로 절반 이동하면 0.5다. 패널 선택은
|
|
149
|
+
페이지가 정착했을 때 `onValueChange`와 동일한 제품 상태로 확정하고, 탭 활성화에서는
|
|
150
|
+
제품 페이저를 해당 index로 이동한다. 이 옵션은 페이지 제스처를 가로채지 않는다.
|
|
151
|
+
세로 Tabs는 무시한다. source의 `subscribe`는 반드시 unsubscribe 함수를 반환한다.
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
// Web
|
|
155
|
+
<Tabs label={t("pages")} items={items} value={selected} onValueChange={selectAndScrollPager}
|
|
156
|
+
swipePosition={{ get: () => pager.index, subscribe: listener => pager.onPosition(listener) }}
|
|
157
|
+
renderPanels={false} />
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
// Native: pager.onScroll/position 이벤트를 같은 source에 연결한다.
|
|
162
|
+
<Tabs label={t("pages")} items={items} value={selected} onValueChange={selectAndScrollPager}
|
|
163
|
+
swipePosition={{ get: () => pager.index, subscribe: listener => pager.onPosition(listener) }}
|
|
164
|
+
renderPanels={false} />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
source 객체는 렌더마다 바꾸지 않도록 `useMemo` 또는 제품 어댑터의 안정 인스턴스를
|
|
168
|
+
사용한다. source는 시각적 위치만 전달하므로 접근성·키보드 선택을 대신하지 않는다.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
- 적용: 1.12.1
|
|
7
7
|
- 검토일: 2026-10-13
|
|
8
8
|
- 근거: [Toast](../../toast.md), [Liquid Toast](../../../../react-native/docs/liquid-toast.md), `src/component-recipes.ts`(`toastRecipe`)
|
|
9
|
-
- 스토리북: `배포/컴포넌트/상태와 알림/토스트`, `배포/컴포넌트/상태와 알림/리퀴드 토스트`,
|
|
9
|
+
- 스토리북: `배포/컴포넌트/상태와 알림/토스트`, `배포/컴포넌트/상태와 알림/리퀴드 토스트`, `배포/구성/피드백과 복구/백그라운드 작업 상태`
|
|
10
10
|
|
|
11
11
|
## 언제 쓰나
|
|
12
12
|
|
|
@@ -88,12 +88,12 @@ const feedback = resolveSaveFeedback({
|
|
|
88
88
|
|
|
89
89
|
```tsx
|
|
90
90
|
// Web
|
|
91
|
-
<Button loading={feedback.pending} disabled={!feedback.canSubmit}
|
|
91
|
+
<Button loading={feedback.pending} disabled={!feedback.canSubmit} completion={{ confirmed: feedback.currentSaved, label: savedLabel }} onClick={save}>{saveLabel}</Button>
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
```tsx
|
|
95
95
|
// Native
|
|
96
|
-
<Button loading={feedback.pending} disabled={!feedback.canSubmit}
|
|
96
|
+
<Button loading={feedback.pending} disabled={!feedback.canSubmit} completion={{ confirmed: feedback.currentSaved, label: savedLabel }} onPress={save}>{saveLabel}</Button>
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Web은 `@hjmds/react`의 해당 granular entry, Native는 `@hjmds/react-native` entry를 쓴다.
|
|
@@ -111,3 +111,19 @@ Button의 실행 콜백은 Web `onClick`, Native `onPress`로 연결한다. 위
|
|
|
111
111
|
| --- | --- | --- |
|
|
112
112
|
| 배치/테마 | Stack과 HjmProvider | Stack과 HjmNativeProvider |
|
|
113
113
|
| 좁은 폭 | 줄바꿈·단일 내용 | 같은 순서, OS 화면 검증은 별도 |
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## 확정 라벨의 크기 예약 (미게시 · 2026-10-15)
|
|
117
|
+
|
|
118
|
+
Lucian 001의 저장→진행→완료 프레임 유지를 기존 Button의 `completion` 옵션으로 보완한다.
|
|
119
|
+
기본 행동 문자열은 항상 같은 children으로 두고, 완료 문자열은 `completion.label`로 처음부터
|
|
120
|
+
알린다. `confirmed`는 `resolveSaveFeedback.currentSaved`처럼 **현재 입력과 마지막 확정
|
|
121
|
+
응답**을 비교한 결과다. 요청 진행 중 편집했다면 이전 응답이 성공해도 완료 라벨을 표시하지
|
|
122
|
+
않는다. 저장 중에는 중앙 로딩만 보이고, 실패하면 같은 기본 행동 문자열로 돌아온다.
|
|
123
|
+
서버가 확정하기 전에는 완료를 표시하지 않으며 자동 성공 타이머·중복 mutation 엔진을 넣지 않는다.
|
|
124
|
+
|
|
125
|
+
Web은 두 라벨을 겹쳐 측정하고 Native는 두 라벨의 고유 폭과 최대 높이를 미리 예약한다.
|
|
126
|
+
자세한 초기 Native 행 예약과 문자열 제한은 [Button](../components/button.md#저장-확정-라벨과-프레임-예약-미게시--2026-10-15)을 따른다.
|
|
127
|
+
기존 결과 영역이 live region을 소유한다. Button이 별도 자동 알림을 만들지 않아 저장 중·완료
|
|
128
|
+
안내가 중복 발표되지 않는다. Showcase는 같은 공개 Button API를 사용하며, 예제 요청임을
|
|
129
|
+
계속 밝힌다. 실제 제품은 자신의 네트워크 요청·정규화된 확정 응답을 기존 session에 전달한다.
|