@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.
Files changed (130) hide show
  1. package/dist/authoring-tools.d.ts +38 -0
  2. package/dist/authoring-tools.d.ts.map +1 -0
  3. package/dist/authoring-tools.js +35 -0
  4. package/dist/authoring-tools.js.map +1 -0
  5. package/dist/behaviors.d.ts +33 -3
  6. package/dist/behaviors.d.ts.map +1 -1
  7. package/dist/behaviors.js +2 -0
  8. package/dist/behaviors.js.map +1 -1
  9. package/dist/calendar.d.ts +9 -1
  10. package/dist/calendar.d.ts.map +1 -1
  11. package/dist/calendar.js +4 -2
  12. package/dist/calendar.js.map +1 -1
  13. package/dist/catalog.d.ts +129 -2
  14. package/dist/catalog.d.ts.map +1 -1
  15. package/dist/catalog.js +6 -2
  16. package/dist/catalog.js.map +1 -1
  17. package/dist/chart.d.ts +398 -0
  18. package/dist/chart.d.ts.map +1 -0
  19. package/dist/chart.js +414 -0
  20. package/dist/chart.js.map +1 -0
  21. package/dist/collection-editing.d.ts +38 -0
  22. package/dist/collection-editing.d.ts.map +1 -0
  23. package/dist/collection-editing.js +53 -0
  24. package/dist/collection-editing.js.map +1 -0
  25. package/dist/communication.d.ts +29 -0
  26. package/dist/communication.d.ts.map +1 -0
  27. package/dist/communication.js +11 -0
  28. package/dist/communication.js.map +1 -0
  29. package/dist/component-definitions.d.ts +1 -0
  30. package/dist/component-definitions.d.ts.map +1 -1
  31. package/dist/component-definitions.js +1 -0
  32. package/dist/component-definitions.js.map +1 -1
  33. package/dist/data-table.d.ts +229 -5
  34. package/dist/data-table.d.ts.map +1 -1
  35. package/dist/data-table.js +242 -6
  36. package/dist/data-table.js.map +1 -1
  37. package/dist/data-workflows.d.ts +70 -0
  38. package/dist/data-workflows.d.ts.map +1 -0
  39. package/dist/data-workflows.js +109 -0
  40. package/dist/data-workflows.js.map +1 -0
  41. package/dist/dataviz.d.ts +7 -6
  42. package/dist/dataviz.d.ts.map +1 -1
  43. package/dist/dataviz.js +7 -6
  44. package/dist/dataviz.js.map +1 -1
  45. package/dist/gooey-navigation.d.ts +7 -0
  46. package/dist/gooey-navigation.d.ts.map +1 -1
  47. package/dist/gooey-navigation.js +10 -0
  48. package/dist/gooey-navigation.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/media-transport.d.ts +16 -0
  54. package/dist/media-transport.d.ts.map +1 -0
  55. package/dist/media-transport.js +17 -0
  56. package/dist/media-transport.js.map +1 -0
  57. package/dist/recipes.d.ts +1 -0
  58. package/dist/recipes.d.ts.map +1 -1
  59. package/dist/recipes.js +1 -0
  60. package/dist/recipes.js.map +1 -1
  61. package/dist/showcase.d.ts.map +1 -1
  62. package/dist/showcase.js +3 -1
  63. package/dist/showcase.js.map +1 -1
  64. package/dist/social-compositions.d.ts +19 -0
  65. package/dist/social-compositions.d.ts.map +1 -0
  66. package/dist/social-compositions.js +33 -0
  67. package/dist/social-compositions.js.map +1 -0
  68. package/dist/streaming-answer.d.ts +18 -0
  69. package/dist/streaming-answer.d.ts.map +1 -0
  70. package/dist/streaming-answer.js +61 -0
  71. package/dist/streaming-answer.js.map +1 -0
  72. package/dist/task-controls.d.ts +31 -0
  73. package/dist/task-controls.d.ts.map +1 -0
  74. package/dist/task-controls.js +25 -0
  75. package/dist/task-controls.js.map +1 -0
  76. package/dist/toggle-feedback.d.ts +22 -0
  77. package/dist/toggle-feedback.d.ts.map +1 -0
  78. package/dist/toggle-feedback.js +32 -0
  79. package/dist/toggle-feedback.js.map +1 -0
  80. package/dist/transfer-task.d.ts +13 -0
  81. package/dist/transfer-task.d.ts.map +1 -0
  82. package/dist/transfer-task.js +13 -0
  83. package/dist/transfer-task.js.map +1 -0
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/dist/version.js.map +1 -1
  87. package/docs/authoring-tools.md +31 -0
  88. package/docs/calendar.md +3 -1
  89. package/docs/catalog-freeze.json +5 -0
  90. package/docs/chart.md +206 -0
  91. package/docs/collection-editing.md +83 -0
  92. package/docs/communication.md +35 -0
  93. package/docs/data-table.md +56 -6
  94. package/docs/data-workflows.md +26 -0
  95. package/docs/generated/component-maturity.md +6 -5
  96. package/docs/generated/renderer-evidence.json +95 -5
  97. package/docs/generated/renderer-evidence.md +6 -3
  98. package/docs/generated/showcase-manifest.json +66 -3
  99. package/docs/gooey-navigation.md +20 -0
  100. package/docs/library-gap-analysis.md +4 -2
  101. package/docs/media-transport.md +37 -0
  102. package/docs/social-compositions.md +51 -0
  103. package/docs/streaming-answer.md +57 -0
  104. package/docs/task-controls.md +38 -0
  105. package/docs/toggle-feedback.md +56 -0
  106. package/docs/transfer-task.md +33 -0
  107. package/docs/usage/README.md +15 -4
  108. package/docs/usage/components/activity-chip.md +2 -2
  109. package/docs/usage/components/button.md +39 -0
  110. package/docs/usage/components/calendar.md +4 -2
  111. package/docs/usage/components/chart.md +277 -0
  112. package/docs/usage/components/data-table.md +92 -18
  113. package/docs/usage/components/screen-layout.md +3 -1
  114. package/docs/usage/components/tabs.md +25 -0
  115. package/docs/usage/components/toast.md +1 -1
  116. package/docs/usage/compositions/action-feedback.md +18 -2
  117. package/docs/usage/compositions/activity-status.md +2 -2
  118. package/docs/usage/compositions/authoring-tools.md +90 -0
  119. package/docs/usage/compositions/collection-editing.md +110 -0
  120. package/docs/usage/compositions/communication.md +100 -0
  121. package/docs/usage/compositions/data-workflows.md +91 -0
  122. package/docs/usage/compositions/media-transport.md +87 -0
  123. package/docs/usage/compositions/social-compositions.md +91 -0
  124. package/docs/usage/compositions/streaming-answer.md +97 -0
  125. package/docs/usage/compositions/task-controls.md +93 -0
  126. package/docs/usage/compositions/toggle-feedback.md +84 -0
  127. package/docs/usage/compositions/transfer-task.md +86 -0
  128. package/docs/usage/compositions/web-additions.md +2 -2
  129. package/docs/usage/screens/common-shell.md +4 -0
  130. package/package.json +67 -1
package/docs/chart.md ADDED
@@ -0,0 +1,206 @@
1
+ # Chart contract
2
+
3
+ 검토일: 2026-10-14 · 계약: `src/chart.ts`(`@hjmds/design-contracts/components/chart`) · 색 토큰: `src/dataviz.ts`
4
+ · 사용 지침: [usage/components/chart.md](usage/components/chart.md)
5
+
6
+ **문제.** 사용자가 시간에 따른 값의 흐름(주가·기록·순위), 범주끼리의 크기 비교, 전체 안의 몫,
7
+ 여러 능력치의 모양, 두 수치의 관계를 한눈에 읽는다. 선 위를 손가락으로 훑어 특정 시점의 값을
8
+ 확인하고(스크럽), 누른 막대·조각의 값을 본다. 숫자 하나는 [Statistic](statistic.md), 진행 비율은
9
+ [Progress](progress.md), 연간 일별 밀도는 [ActivityHeatmap](activity-heatmap.md), 정확한 값의
10
+ 비교·정렬은 [DataTable](data-table.md)이 맡는다. 이 계약은 그 사이의 **그림으로 읽는 수치**를 맡는다.
11
+
12
+ ## 결정 번복 — 2026-10-14
13
+
14
+ **이전 결정.** 2026-09 라이브러리 격차 분석은 `Chart`를 "토큰만 채택"으로 닫았고
15
+ ([library-gap-analysis.md](library-gap-analysis.md)), 2026-09-29 카탈로그 정리에서 `Chart` 행을
16
+ 지웠다([catalog-cleanup.md](catalog-cleanup.md)). HJM은 계열 팔레트·축·격자·범례 토큰(`dataviz`)만
17
+ 고정하고 그리기는 제품 라이브러리(Recharts·victory-native·d3)에 맡겼다. 근거는 "축·툴팁·애니메이션·
18
+ 접근성을 다시 만들면 제품이 차트를 두 벌 갖게 된다"였다. `src/dataviz.ts` 머리 주석이 그 근거로 이
19
+ 문서를 가리켰지만 문서는 없었다.
20
+
21
+ **번복.** 2026-10-14 사용자 요청 "유명한 차트·표 라이브러리를 리버스 엔지니어링해서 우리 시스템에 흡수,
22
+ 여러 타입의 차트와 표 다 흡수"에 따라 그리기를 HJM으로 옮긴다. 이전 근거의 전제가 틀렸다는 실측이 있다.
23
+ 제품은 외부 라이브러리를 쓰지 않고 같은 것을 **직접 다시 만들었다** — Yajalal이 스크럽하는 선 차트
24
+ (`modules/mobile/src/components/market/StockChart.tsx`), 막대, 정렬되는 기록 표(`RecordTable.tsx`)를 자체 구현했다.
25
+ 빈 값 처리·축 눈금·정렬 규칙·접근성 문장을 제품 코드가 각자 정하게 된다. 두 벌이 생기는 것은 HJM이 그릴 때가
26
+ 아니라 HJM이 그리지 않을 때였다.
27
+
28
+ **바뀌지 않는 것.** `datavizSeriesPalette`·`resolveDatavizSeriesColor`·`datavizChromeTokens`의 값과 규칙은
29
+ 그대로다. HJM 렌더러가 이 토큰을 소비하는 첫 번째 사용자가 된다. 이 계약이 다루지 않는 종류(아래 "넣지 않은
30
+ 것")를 제품 라이브러리로 그리는 것은 여전히 허용하며, 그때도 색·축·격자는 `dataviz` 토큰을 쓴다.
31
+
32
+ ## 참고한 라이브러리
33
+
34
+ 공개 API와 사용 경험을 연구해 **개념만** 다시 유도했다. 어떤 라이브러리의 소스도 복사·번들하지 않았고
35
+ 렌더러는 새 런타임 의존성을 추가하지 않는다(Native는 기존 `react-native-svg` host를 쓴다).
36
+
37
+ | 라이브러리 | 가져온 개념 | 가져오지 않은 것 |
38
+ | --- | --- | --- |
39
+ | react-native-wagmi-charts | 선 + 커서 십자선, 포커스된 index가 가격·날짜 텍스트를 구동, 손을 떼면 최신 값으로 복귀 | Reanimated worklet 구조, 캔들스틱 |
40
+ | victory-native (XL) | x/y key로 묶는 cartesian 데이터, chart press state, 둥근 막대, domain padding | Skia 캔버스 의존, 임의 render-prop 레이어 |
41
+ | react-native-gifted-charts | 막대·선·파이·레이더의 prop 어휘(누르면 focus, 쌓은 막대, 도넛 가운데 내용) | 수백 개의 시각 prop, 내장 애니메이션 |
42
+ | Recharts | 계열/범주 데이터 모델, 쌓기, nice 눈금, 레이더 극좌표 격자 | 합성 child 컴포넌트 트리(`<XAxis>` 등), 임의 툴팁 컴포넌트 |
43
+ | d3-shape / d3-scale | monotone-X 3차 곡선(Fritsch–Carlson), "nice" 눈금 간격(1·2·2.5·5 × 10ⁿ), band scale | d3 패키지 자체(알고리즘만 `src/chart.ts`에 다시 썼다) |
44
+ | TanStack Table | `sortDescFirst`, `sortUndefined: "last"`, 안정 정렬 row model | headless 테이블 엔진 전체 |
45
+ | AG Grid | 열 고정(pinning) — 시작 쪽 연속 열만 | 그리드 편집·가상화·열 리사이즈 |
46
+
47
+ 표 두 줄(TanStack·AG Grid)은 [DataTable](data-table.md#정렬-helper와-native-renderer--2026-10-14)의 같은 날
48
+ 추가분이다. 차트와 표를 한 요청으로 흡수했기 때문에 출처를 한 곳에 적는다.
49
+
50
+ ## 차트 종류와 기하 함수
51
+
52
+ 모든 기하는 렌더러 중립 계산이다. Web(SVG DOM)과 Native(react-native-svg)는 같은 좌표·path·접근성 문장을 받아
53
+ 칠하기만 한다. 같은 입력이 두 플랫폼에서 다른 그림이 되는 것을 이 경계가 막는다.
54
+
55
+ | 종류 | 컴포넌트 | 기하 함수 | 데이터 |
56
+ | --- | --- | --- | --- |
57
+ | 선·영역(쌓은 영역 포함) | `LineChart` | `resolveLineGeometry`, `linePath`, `areaPath`, `splitSegments`, `stackValues` | `categories` + `ChartSeries[]` |
58
+ | 막대(세로·가로, 묶음·쌓기) | `BarChart` | `resolveBarGeometry`, `scaleBand`, `stackValues` | `categories` + `ChartSeries[]` |
59
+ | 파이·도넛 | `PieChart` | `resolvePieGeometry`, `arcPath` | `{ id, label, value, color? }[]` |
60
+ | 레이더 | `RadarChart` | `resolveRadarGeometry` | `categories`(3개 이상) + `ChartSeries[]` |
61
+ | 산점도 | `ScatterChart` | `resolveScatterGeometry`, `nearestScatterPoint` | `ScatterDatum[]` |
62
+ | 작은 추세선 | `Sparkline` | `linePath`, `resolveChartDomain`, `scaleLinear` | `(number \| null)[]` |
63
+ | 범례 | `ChartLegend` | — | `{ id, label, color, value? }[]` |
64
+
65
+ 공용 도구: `niceTicks`·`niceStep`(축 눈금), `resolveChartDomain`(값 범위), `scaleLinear`·`scaleBand`·`pointPositions`
66
+ (좌표 변환), `nearestChartIndex`·`stepChartIndex`·`observedIndices`(포커스), `seriesExtremes`(최고·최저 표시),
67
+ `describeChart`(텍스트 대안), `validateChartSeries`(입력 검증).
68
+
69
+ `ChartSeries`는 `{ id, label, values: (number | null)[], color? }`다. `values` 길이는 `categories` 길이와 같아야 하고,
70
+ `id`는 비어 있지 않고 중복이 없으며, `label`은 비어 있지 않아야 한다. 어기면 `TypeError`/`RangeError`가 난다.
71
+ 곡선은 `linear`(기본)·`monotone`·`step`, 막대 방향은 `vertical`(기본)·`horizontal`, 배치는 `grouped`(기본)·`stacked`다.
72
+
73
+ ## 그림 규칙
74
+
75
+ - **빈 값은 끊김이다. 0이 아니다.** `null`(또는 유한하지 않은 수)은 관측이 없다는 뜻이다. 선은 `splitSegments`로
76
+ 그 자리에서 끊기고 양옆을 잇지 않는다. 0으로 그리면 "기록 0"이라는 다른 사실을 말하고, 이으면 없는 추세를
77
+ 지어낸다. 막대는 그 자리를 비우고, 쌓기에서는 누적에 더하지 않는다. 텍스트 대안에서는 제품이 준
78
+ `missingLabel`로 읽힌다.
79
+ - **막대는 항상 0 기준선을 포함한다.** `resolveBarGeometry`는 `includeZero: true`로 범위를 잡는다. 막대 길이가
80
+ 값의 크기라서, 0이 아닌 곳에서 시작하면 두 배 차이가 열 배로 보인다. 선은 반대로 관측 범위로 확대한다
81
+ (주가처럼 작은 변화를 읽는 그림). 선에서도 0을 넣어야 하면 `includeZero`를 준다. 쌓은 선(영역)은 항상 0을 넣는다.
82
+ - **순위는 뒤집은 척도로 그린다.** `invert`(`scaleLinear`의 세 번째 인자)는 최솟값을 위에 둔다. 1위·평균자책점처럼
83
+ 작을수록 좋은 값이 위로 가야 "올라갔다 = 좋아졌다"가 유지된다.
84
+ - **레이더는 모든 축이 한 범위를 공유한다.** 기본 `[0, 모든 계열의 최댓값]`, 또는 `domain`으로 고정한다. 축마다 다른
85
+ 척도를 쓰면 다각형 넓이가 아무 뜻도 없어진다. 제품 평점이 한 0–100 척도라는 실측에 맞춘 선택이다. 축은 3개 이상이어야
86
+ 한다(`RangeError`).
87
+ - **파이는 음수와 빈 값을 무시한다.** 전체의 몫은 음수일 수 없다. `resolvePieGeometry`는 0 이하·빈 값을 합계와 조각에서
88
+ 모두 뺀다. 합계가 0이면 조각이 없다. 조각은 12시 방향부터 시계 방향이며 사이 간격은 `chartRecipe.pie.padAngle` 0.012rad다.
89
+ - **쌓기에서 음수는 0 아래로 따로 쌓인다**(`stackValues`의 양수·음수 누적 분리).
90
+ - **눈금은 읽기 좋은 수다.** `niceTicks`는 1·2·2.5·5 × 10ⁿ 간격만 쓴다(0/25/50, 0/23.7/47.4가 아니다).
91
+ - **곡선은 데이터를 넘어가지 않는다.** `monotone`은 Fritsch–Carlson monotone-X라 두 점 사이에서 최고·최저를 새로 만들지 않는다.
92
+ 일반 cubic/Catmull-Rom은 없는 고점을 그려 채택하지 않았다.
93
+ - **평평한 계열**은 위·아래 10%(값이 0이면 ±1)를 넓혀 가장자리에 붙지 않게 한다(`resolveChartDomain`).
94
+
95
+ ## 색
96
+
97
+ - 계열 색은 `series.color ?? resolveDatavizSeriesColor(theme, index)`다. 제품 브랜드 색이나 의미가 있는 계열(우리 팀 등)은
98
+ 제품이 `color`로 주고, 나머지는 `dataviz` 팔레트(light·dark 각 8색)를 index 순서로 쓴다. 9번째부터는 순환한다. 계열이 8개를
99
+ 넘는 화면은 대개 차트가 아니라 표가 맞다.
100
+ - **상태색(info·success·warning·attention)을 계열색으로 쓰지 않는다.** 상태색은 뜻을 가진 색이라 "3번 계열"에 쓰면
101
+ 사용자가 경고로 읽는다. 오르내림의 의미 표시는 차트 밖 [Statistic](statistic.md)의 trend가 맡는다.
102
+ - 축·격자·범례·커서 글자는 차트 전용 회색이 아니라 본문 토큰이다(`chartRecipe.chrome` = `datavizChromeTokens`: 축선 `border`,
103
+ 축 글자 `textMuted`, 격자 `border` 60%, 범례 `textBody`).
104
+ - 색만으로 계열을 구분하게 두지 않는다. 계열이 둘 이상이면 범례(`ChartLegend` 또는 내장 legend)에 이름을 쓰고, 값은 텍스트
105
+ 대안으로도 읽힌다.
106
+
107
+ ## 접근성
108
+
109
+ - **모든 차트는 이름과 텍스트 대안을 갖는다.** `accessibilityLabel`(Web에서는 `aria-label`로 나간다)은 필수다. 그림은
110
+ `role="img"`(Native image)이며, `describeChart(categories, series, formatValue, missingLabel)`가 "계열: 범주 값, 범주 값…"
111
+ 문장을 만든다. 숫자 서식과 빈 값 문구는 제품이 공급한다 — HJM은 숫자를 어떤 언어로도 직접 서식하지 않는다.
112
+ - **스크럽 가능한 선은 끌지 않고도 탐색된다.** Web은 `role="slider"`, `aria-valuetext`(포커스된 범주와 값), `ArrowLeft`/`ArrowRight`/
113
+ `Home`/`End`. Native는 `accessibilityRole="adjustable"`과 increment/decrement 동작이다. 둘 다 `stepChartIndex`로 **관측값이 있는
114
+ index만**(`observedIndices`) 이동한다 — 빈 칸에 멈추지 않는다.
115
+ - 막대·파이·산점도의 누름 선택은 같은 값을 텍스트 대안과 범례에서도 읽을 수 있으므로 별도 접근성 탐색 단위를 만들지 않는다.
116
+ - `Sparkline`은 `accessibilityLabel`이 없으면 장식으로 취급해 보조 기술에서 숨긴다. 옆에 같은 값을 말하는 Statistic이
117
+ 있는 자리(목록 행·표 셀)를 위한 기본값이다. 혼자 의미를 전달하면 이름을 준다.
118
+
119
+ ## 스크럽(LineChart)
120
+
121
+ 1. 누른 채 가로로 끌면 손가락 x에 가장 가까운 관측 index(`nearestChartIndex`, 후보 `observedIndices`)로 포커스가 옮겨 간다.
122
+ `onFocusChange(index, { scrubbing: true })`가 불리고, 커서 위에 `cursorLabel(index)`(기본 `categories[index]`) 말풍선이 뜬다.
123
+ 2. 손을 떼면 기본 포커스(마지막 관측 index)로 돌아가고 `onFocusChange(index, { scrubbing: false })`가 불린다. 제품의 "현재 값"
124
+ 텍스트는 이 콜백으로 갱신한다(wagmi-charts의 가격 텍스트와 같은 구조).
125
+ 3. 세로로 끄는 것은 페이지 스크롤이다. Native는 `|dy| > |dx|`일 때 PanResponder가 응답권을 넘기고, Web은 pointer event와
126
+ `touch-action: pan-y`로 같은 결과를 낸다. 스크롤 화면 안의 차트가 스크롤을 삼키지 않게 하는 규칙이다.
127
+ 4. 포커스된 점은 halo(`chartRecipe.line.focusHaloRadius` 9, 불투명도 0.25) + 점(반지름 4.5)과 세로 커서선으로 그린다. 끄는 중이
128
+ 아니면 커서선은 점선(`chartRecipe.cursor.dash` [2, 3])이다.
129
+ 5. 제어는 `focusIndex`(+ `onFocusChange`), 비제어는 `defaultFocusIndex`다. `extremes`를 주면 포커스가 없을 때 첫 계열의 최고·최저
130
+ (`seriesExtremes`)에 제품 문구 라벨을 붙인다.
131
+
132
+ `BarChart`는 누름·끌기로 범주를 고르고, 포커스가 있으면 나머지 막대를 `chartRecipe.bar.restOpacity` 0.4로 낮춘다.
133
+ `PieChart`는 조각 선택(`selectedId`/`onSelect`), `ScatterChart`는 가장 가까운 점(32px 안, `nearestScatterPoint`) 포커스를 쓴다.
134
+
135
+ ## HJM 기본값 (`chartRecipe`)
136
+
137
+ | 항목 | 값 |
138
+ | --- | --- |
139
+ | 높이 | `sparkline` 28 · `compact` 128 · `regular` 180(LineChart·BarChart 기본) · `tall` 240 |
140
+ | plot 여백(축 없음) | 위 `spacing.lg` 20 · 오른쪽 `spacing.xs` 8 · 아래 `spacing.xs` 8 · 왼쪽 `spacing.xs` 8 |
141
+ | plot 여백(축 있음) | 위 `spacing.md` 16 · 오른쪽 `spacing.xs` 8 · 아래 `spacing.lg` 20 · 왼쪽 36(y 눈금 글자 칸) |
142
+ | 선 | 두께 2.4 · 점 반지름 4.5 · 포커스 halo 9 / 0.25 |
143
+ | 영역 채움 | 위 0.32 → 아래 0 그라데이션 |
144
+ | 막대 | 모서리 `radius.sm`/2 = 4 · 최대 두께 28 · band padding 0.36 · 묶음 간격 2 · 비포커스 0.4 |
145
+ | 파이 | 도넛 안쪽 비율 0.62 · 조각 간격 0.012rad · 조각 라벨 최소 비율(`minLabelPercent`) 4% |
146
+ | 레이더 | 단계 4 · 선 두께 2 · 채움 0.22 · 라벨 거리 `spacing.sm` 12 |
147
+ | 산점도 | 점 반지름 5 · 포커스 8 |
148
+ | Sparkline | 72 × 28 · 선 두께 1.8 · 안쪽 여백 2 |
149
+ | 커서·기준선 | 커서 `stroke.subtle` 1, 점선 [2, 3] · 기준선 점선 [2, 4] |
150
+
151
+ ## 넣지 않은 것
152
+
153
+ 측정된 제품 요구가 없어 계약하지 않는다. 필요한 제품은 이 문서를 갱신하고 vertical slice로 축을 연다. 그 전에는 제품
154
+ 라이브러리를 써도 되고, 색·축은 `dataviz` 토큰을 쓴다.
155
+
156
+ - 캔들스틱(OHLC)·박스 플롯·히스토그램 bin 계산·지도·트리맵·sankey
157
+ - 확대·이동(zoom/pan), brush 범위 선택, 이중 y축, 로그 척도
158
+ - 임의 컴포넌트 툴팁 — 포커스 값은 제품이 `onFocusChange`로 받아 차트 밖 텍스트로 보여 준다(값이 화면 읽기 도구와 큰 글자
159
+ 배치에서 같은 자리에 남는다)
160
+ - 데이터 변경 사이 path morph 애니메이션, 실시간 스트리밍 창
161
+ - 차트 안 번역·숫자 서식 — 모든 문구와 서식은 제품 공급
162
+
163
+ ## 카탈로그
164
+
165
+ `Chart`는 2026-09-29에 지운 이름을 같은 이름으로 되살린다(`data-display`, Web·Native). 공개 컴포넌트 `LineChart`·`BarChart`·`PieChart`·
166
+ `RadarChart`·`ScatterChart`·`Sparkline`·`ChartLegend`는 이 한 계약의 동반 API다 — 같은 `ChartSeries` 모델·색 규칙·포커스·텍스트 대안을
167
+ 공유하므로 종류마다 계약을 따로 두지 않는다. 카탈로그 동결 예외는 `docs/catalog-freeze.json`에 기록한다.
168
+
169
+ ## Renderer와 검증
170
+
171
+ - Web `@hjmds/react/chart`(SVG DOM), Native `@hjmds/react-native/chart`(react-native-svg)는 같은 이름·prop을 쓴다.
172
+ - 계약 검사는 `test/chart.test.ts`(기하·빈 값·쌓기·척도·포커스·텍스트 대안)다.
173
+ - 2026-10-14 작성 시점에 두 renderer는 구현 중이다. renderer 테스트·Storybook(`배포/컴포넌트/데이터 표시/차트`)·기기 확인 결과는
174
+ 들어온 뒤 이 절에 따로 기록한다. 계약 검사 통과를 renderer 검증이나 제품 반영으로 보고하지 않는다.
175
+
176
+ ## 도넛 항목 제외 (2026-10-15, Lucian 097)
177
+
178
+ 선택 강조만으로 범주를 제외한 것처럼 보이던 차이를 해소하기 위해 PieChart에 controlled `exclusion`을 추가했다.
179
+ `excludedIds`, `onExcludedIdsChange`, 제품 문구 `label(datum,included)`를 전달한다. 기존 selectedId/onSelect는 강조만 소유하며 별도 범례 checkbox가 분모를 바꾼다.
180
+ 공유 `resolvePieVisibility`가 원본 인덱스를 보존해 색 변화를 막고, 합계와 geometry에서 제외값을 null로 처리한다.
181
+ `centerContent`는 기존 ReactNode 또는 `(summary) => ReactNode`를 받는다. summary.total은 실제 포함된 관측값 합계, observedCount는 실제 관측 수다.
182
+ 모두 제외/0/미관측이면 조각이 없고 합계 0이다. 100%나 가짜 조각을 만들지 않는다. 제외된 항목이 다시 포함되면 원래 색과 선택 강조를 되찾는다.
183
+ Native도 포함 여부 checkbox와 독립된 강조 행동을 제공한다. onExcludedIdsChange는 데이터 자체를 삭제하지 않는다.
184
+ 미게시 개발 API이며 제품 데이터/선택의 controlled 반영은 소비 제품이 소유한다.
185
+
186
+ ## 실제 데이터 교체와 스크럽 강조 (미게시 · 2026-10-15)
187
+
188
+ Lucian 095·096의 도형 전환을 별도 차트로 복제하지 않고 기존 `BarChart`·`LineChart`의
189
+ `animateChanges` 옵션으로 연결한다. 입력 계열·범주가 바뀌면 기존 기하 함수가 새 축·범위·실제
190
+ 값을 계산하고, 선/영역 경로 또는 막대 경계만 220ms 동안 이전 표현에서 새 표현으로 옮긴다.
191
+ 축·텍스트 대안·값 표시는 최신 입력을 즉시 사용한다. 전환 중간 경로는 관측값이 아니다.
192
+ Web은 SVG attribute, Native는 SVG `setNativeProps`를 갱신하며 프레임마다 React state를 만들지 않는다.
193
+ 같은 계열 id와 범주 문자열(+동일 문자열의 발생 순서)이 막대의 정체성이다. 비제어 선택도
194
+ 같은 범주를 따라가며 삭제되면 해제한다. 제어 선택은 소유자가 새 index로 갱신한다.
195
+
196
+ 기간의 점 개수가 다를 때 `prepareChartLineMorph`는 양쪽의 정규화된 꼭짓점 위치를 합친다.
197
+ `linear` 선은 양 끝의 관측 꼭짓점을 모두 보존한다. `monotone`·`step`도 같은 합친 점에서
198
+ 경로를 다시 계산하고 종료 프레임에서 원래 새 경로로 정확히 정착한다. 빈 관측은 기존의
199
+ 연속 구간별로 따로 처리하므로 없는 관측을 선으로 잇지 않는다. 선 구간 양 끝 점 합계가
200
+ 1,024개를 넘거나 막대가 200개를 넘으면 새 기하로 바로 교체해 프레임 비용을 제한한다.
201
+ reduced motion은 즉시 교체하고, background는 새 표현에 정착하며, unmount/새 입력은 이전
202
+ 프레임 작업을 취소한다. 고주기 실시간 데이터는 기본값 `animateChanges={false}`를 유지한다.
203
+
204
+ `LineChart.scrubEmphasis="before" | "after"`는 현재 포커스의 x에서 클립을 나누어 선택한
205
+ 쪽을 계열 색으로, 나머지를 축 레이블 색으로 표시한다. 새로운 점·수치·추천을 계산하지
206
+ 않으며 기존 드래그·키보드·접근성 포커스와 `holdOnRelease`를 그대로 사용한다.
@@ -0,0 +1,83 @@
1
+ # 목록 편집과 복구 구성
2
+
3
+ 검토일: 2026-10-15 · 상태: 개발 구현, 미게시 · Web / React Native
4
+
5
+ Lucian UX/UI [#033](https://lucianlabs.dev/uxui-prompt/#033), [#092](https://lucianlabs.dev/uxui-prompt/#092),
6
+ [#131](https://lucianlabs.dev/uxui-prompt/#131), [#144](https://lucianlabs.dev/uxui-prompt/#144),
7
+ [#178](https://lucianlabs.dev/uxui-prompt/#178)의 전수 검토 후 도입한 다섯 구성이다.
8
+ 원본의 HTML/CSS/스크립트를 복사하지 않고 기존 HJM API로 구현한다. 특히 시연용 저장 성공,
9
+ 전체 항목 복구, 고정 데이터·색은 가져오지 않는다. 새 의존성·canonical catalog 항목은 추가하지 않는다.
10
+
11
+ ## 소유권과 기존 API와의 관계
12
+
13
+ | 공개 구성 | 기존 기반 | 새로 제공하는 행동 | 제품 소유 |
14
+ | --- | --- | --- | --- |
15
+ | InlineEdit | TextField, Button, ActionSession | 명시적 편집·저장·취소, 실패 초안 보존, Web IME/Enter/Escape, 중복·오래된 완료 차단 | ID, 확정 값, 검증, 저장, 오류 문구 |
16
+ | RepeatableFieldGroup | FieldGroup | stable ID 행 추가·삭제, 행 이동 시 host 유지, 삭제 뒤 다음 행/추가 버튼 포커스 | 행 배열·값·오류, 새 ID 발급, 삭제 실행 |
17
+ | SelectionActionBar | Button, Text, ActionSession | 선택 요약·행동·정확한 선택 snapshot, 작업 진행·실패, 제품 역연산 호출 | 선택 ID, 행동·권한, 실제 보관/복구, 결과 반영 |
18
+ | FilterRecovery | Button, Text, ActionSession | 양수 결과가 있는 제안만 표시, 명시적 적용, 실패 복구 | 검색·필터·실제 개수, 제안 라벨, 확정 결과 |
19
+ | TableColumnSettings | Checkbox, Button, DataTable 제어 columns | 마지막 표시 열 보호, 고정 열 숨김/이동 차단, 인접 교환 | 원본 열 정의, 보이는 순서, 정렬·선택·설정 저장 |
20
+
21
+ 단일 이름 입력이면 TextField, 고정 입력 묶음이면 FieldGroup, 단순 선택이면 CheckboxGroup을 쓴다.
22
+ 이번 구성은 해당 API를 대체하는 엔진이 아니라 그 위의 반복되는 작업 흐름이다.
23
+ 선택 바는 Native DataTable에 다중 선택 기능을 추가하지 않는다. Native에서는 CheckboxGroup 등의 실제 선택 공급원을 연결한다.
24
+
25
+ ## 공개 경로와 상태
26
+
27
+ - Web: `@hjmds/react/collection-editing`
28
+ - Native: `@hjmds/react-native/collection-editing`
29
+ - 순수 모델/헬퍼: `@hjmds/design-contracts/collection-editing`
30
+ - root barrel에 추가하지 않는다. 양 플랫폼 공개 API가 있지만 현재 미게시이며 앱 설치 버전의 제공을 의미하지 않는다.
31
+ - 다섯 구성은 각각 `배포/구성/…` Storybook에 있다. 실행 예제와 배치는 [사용 지침](usage/compositions/collection-editing.md)을 따른다.
32
+
33
+ ## 주요 타입
34
+
35
+ ```ts
36
+ type ColumnSetting = Readonly<{ id: string; label: string; visible: boolean; locked?: boolean }>;
37
+ type FilterRecoveryOption = Readonly<{ id: string; label: string; resultCount: number }>;
38
+ type SelectionAction = Readonly<{ id: string; label: string; destructive?: boolean }>;
39
+ ```
40
+
41
+ 모든 문구와 formatter는 제품에서 지역화해 전달한다. renderer에는 기본 한국어 성공/실패 문구가 없다.
42
+ 색·서체·간격·버튼·입력은 기존 HJM theme/recipe를 사용한다. 묶음 간격 `md`, 행동 간격 `sm`은
43
+ `collectionEditingRecipe`가 소유한다. 상태·오류 묶음은 자기 영역 중앙에 정렬한다.
44
+
45
+ ## 저장·동시성·복구
46
+
47
+ - `onSave`, `onAction`, `onApply`, `onUndo`는 동기 함수 또는 Promise다. 성공은 resolve,
48
+ 실패는 reject로 전달한다. catch 후 정상 resolve하면 HJM은 실패를 알 수 없다.
49
+ - `onSave`는 확정 값이 제품 `value`에 반영된 뒤 resolve한다. 실패는 편집기·초안을 유지한다.
50
+ blur는 자동 저장하지 않는다. 편집 중 외부 값 갱신이 와도 초안을 덮지 않는다. 동시 편집 충돌 처리는 제품 책임이다.
51
+ - `InlineEdit.id`는 엔티티 ID다. 다른 항목으로 바뀌면 내부 편집 세션을 교체해 이전 완료가 새 초안을 닫지 못한다.
52
+ - 비동기 행동은 기존 `createActionSession`으로 직렬화한다. unmount는 결과를 무시할 뿐 이미 서버가 받은 작업을 취소하지 않는다.
53
+ 자동 재시도·저장·네트워크 요청을 추가하지 않는다.
54
+ - `selectedIds`는 실행 시 복사·freeze한다. 이후 선택 변경이 이미 진행 중인 요청의 대상을 바꾸지 않는다.
55
+ 성공했다고 HJM이 선택을 임의로 비우지 않는다. 제품이 실제 결과를 반영하며 부분 실패는 항목 ID별로 처리한다.
56
+ - `recovery`는 제품이 확정한 **해당 작업**의 역연산이다. 다른 작업까지 복구하지 않는다.
57
+ 버튼은 현재 selection과 무관하게 해당 callback을 호출한다. 작업 기록의 만료·중복 방지·idempotency는 제품 소유다.
58
+ - 필터의 `resultCount`는 0 이상 safe integer다. 0개인 제안은 숨긴다. 로컬 자료가 전체가 아니라면
59
+ 서버가 계산한 개수를 전달하고 추정치를 실제 결과인 것처럼 표시하지 않는다.
60
+ - 행 ID는 배열 index가 아니다. `renderField`의 `guardChange`로 값 변경을 감싸고 `focusRef`를 실제 입력 ref에 연결한다.
61
+ `onAdd`는 제품이 발급한 ID를 반환하고 같은 변경에서 descriptor에 포함한다. `minFields`/`maxFields`는 행 수 경계다.
62
+ - 열 설정은 원본 DataTable descriptor를 ID로 다시 고른다. `{id,header}`만 새로 만들어 pinned/width/sort 정보를 버리지 않는다.
63
+ `locked`는 위치 경계이며 주변 열도 그 열을 넘지 못한다. 실제 pinned 열은 제품이 locked로 매핑한다.
64
+
65
+ ## 플랫폼 경계
66
+
67
+ Web은 표준 fieldset/label·포커스·키보드를 사용한다. Enter는 IME 조합 중 저장하지 않고, Escape는 pending이 아닐 때 취소한다.
68
+ Native는 TextInput submit와 명시적 저장·취소 버튼을 제공한다. 각 입력을 독립적으로 접근 가능하게 유지하고 부모 View로 합치지 않는다.
69
+ 공통 구성은 스크롤·safe area·키보드 높이를 소유하지 않는다. 실제 화면 host 안에 넣는다.
70
+
71
+ 검증은 관련 계약·Chromium·Native host renderer와 타입 검사로 구분해 보고한다.
72
+ 이 문서는 iOS/Android 실기기·화면 낭독기·성능·npm 게시 또는 소비 앱 적용 증거가 아니다.
73
+
74
+ ## 이번 변경의 확인 결과
75
+
76
+ 2026-10-15: 관련 순수 계약 3건, Chromium 7건, Native host renderer 5건 통과.
77
+ Chromium에는 375px host에서 두 브랜드 fixture(violet/green)의 light/dark 상속과 가로 넘침 확인을 포함한다.
78
+ 세 패키지 build, Web/Native renderer typecheck와 두 Showcase typecheck를 수행했다.
79
+ 로컬 Storybook에서 다섯 구성의 375px 화면과 제목 저장 실패 상태를 직접 확인했다.
80
+
81
+ 새 Web story ID 15개는 원본을 읽는 기존 생성 로직으로 등록했다. 선택적 전체 Storybook 규약 검사에서
82
+ 기존 `components-data-display--chart` ID의 snapshot 누락을 발견했지만 이번 구성과 무관해 변경하지 않았다.
83
+ 전체 회귀·실기기·VoiceOver/TalkBack·성능 측정·npm 게시·소비 앱 적용은 미실행이다.
@@ -0,0 +1,35 @@
1
+ # 검색·메시지 연결 구성 계약
2
+
3
+ 검토일: 2026-10-15 · Lucian 027·028·078 · Storybook 배포 · 미게시
4
+
5
+ `@hjmds/{design-contracts,react,react-native}/communication` 선택 subpath를 제공한다.
6
+ 기존 `screens`의 MessageComposer에는 선택 속성 `lengthFeedback`만 추가한다.
7
+
8
+ ## ExpandableSearch
9
+
10
+ - 펼침 상태와 입력값은 `expanded/onExpandedChange`, `query/onQueryChange`로 제품이 소유한다. 최근 검색 목록도 제품 소유다.
11
+ - 기존 SearchScreen의 두 단계 검색·제안 선택·최근 검색·debounce·AbortSignal을 재사용한다. 별도 검색 debounce 엔진을 만들지 않는다.
12
+ - `onSearch(query,{signal})`는 실제 결과와 제안 목록을 담은 Promise를 반환한다. query·요청 세대·활성 상태·AbortSignal이 모두 현재인 응답만 표시한다. 신호를 무시하는 늦은 Promise도 이전 결과를 덮지 못한다.
13
+ - 닫기·입력 변경·unmount는 요청을 abort한다. 빈 입력은 서버 요청을 만들지 않는다. 오류를 보존된 입력 아래 중앙에 표시하고 편집/재검색으로 재시도한다.
14
+ - 펼칠 때 검색 입력으로 포커스한다. Web 닫기는 실제 펼침 버튼으로 포커스를 복원하고 Native 닫기는 키보드를 내린다. 검색어를 임의 삭제하거나 최근 검색을 타이핑 중 자동 기록하지 않는다.
15
+ - `onSubmit`은 실제 Enter/제안/최근 검색 확정에서만 호출한다. 제품은 이때 최근 검색 저장과 분석을 수행할 수 있다. 결과·추천은 실제 Promise 데이터이며 타이머 재생이 아니다.
16
+
17
+ ## MessageComposer.lengthFeedback
18
+
19
+ - `{label,formatCount,threshold?}`를 지정한 경우에만 near-limit 원형 Progress와 실제 count를 표시한다. 기존 자동 높이 조절과 전송 동작은 그대로 쓴다.
20
+ - 양의 정수 `maxLength`가 필요하고 기본 threshold는 0.8이다. UTF-16 길이는 기존 controlled 문자열/maxLength와 같은 단위다. 이 단위는 사용자에게 보이는 grapheme 수와 다를 수 있으므로 문구는 제품 계약을 따른다.
21
+ - threshold 이전에는 추가 UI가 없다. 초과 controlled 값도 자르지 않고 실제 count를 보존하며 전송을 막는다. `lengthFeedback`을 생략하면 기존 maxLength API의 동작을 유지한다.
22
+ - 성공 시 초안 삭제를 자동 실행하지 않는다. 실패 후 보존도 기존 controlled 입력 계약을 유지한다.
23
+
24
+ ## ReplyMessage·ReplyComposer
25
+
26
+ - ReplyMessage의 `messageId`와 ReplyComposer의 `replyTarget.id`는 실제 메시지 식별자여야 한다. 보이는 저자·발췌문에서 ID를 추정하지 않는다.
27
+ - ReplyMessage는 기존 ChatMessage의 replyAction에 정확한 ID를 연결한다. `swipeAction="reply"`는 답장, `"timestamp"`는 시간 표시가 드래그를 소유한다. 시간 모드에서도 기존 Web 키보드/Native 접근성 답장 동작은 남는다. 두 드래그를 한 제스처에 중복 실행하지 않는다.
28
+ - ReplyComposer는 기존 MessageComposer의 replyTo와 전송을 묶는다. 요청을 시작한 시점의 `{replyToId}`와 실제 입력을 `onSend`에 전달하고 그 Promise 완료를 기다린다. 중복 전송·pending 중 취소/편집은 기존 입력 잠금 계약으로 막는다.
29
+ - Promise 실패 시 초안과 답장 대상을 유지하고 중앙 오류를 표시한다. 완료 자체를 서버 배달/읽음으로 해석하거나 초안을 지우지 않는다. 제품이 실제 acknowledgment를 확인하고 controlled 값만 갱신한다.
30
+ - 대화·계정이 바뀌면 host를 해당 ID로 key한다. unmount는 내부 늦은 갱신을 무시하지만 외부 서비스 요청을 취소하지 않는다. 제품은 API idempotency와 범위/세대 검증으로 이전 대화의 늦은 완료가 새 초안을 지우지 않게 한다.
31
+
32
+ ## 근거와 확인 범위
33
+
34
+ 원본 027의 펼침 검색을 기존 검색 계약에 연결했고, 028의 높이 조절은 중복하지 않고 실제 near-limit 표시만 추가했다. 078의 가짜 상대 자동 답장을 제거하고 원본 메시지 ID→답장 전송 metadata 경로를 공개한다.
35
+ 계약·Web browser·Native host targeted 검사는 경쟁 응답/abort·포커스·실제 ID·실패 보존·길이 표시를 확인한다. Native 기기 키보드·실제 검색 서버/채팅 서버·npm 게시·제품 적용은 별도다.
@@ -16,7 +16,9 @@
16
16
  "local" | "external"`로 실행 방식을 나누고 검색 실행 자체는 항상 제품(로컬 배열이든
17
17
  서버 쿼리든)에 맡기는 것과 같은 경계다(`src/combobox.ts`). `getNextDataTableSortState`는
18
18
  버튼을 눌렀을 때 **다음 정렬 상태가 무엇이어야 하는지**만 판정하고, 실제 행을
19
- 재배열하는 일은 하지 않는다.
19
+ 재배열하는 일은 하지 않는다. 2026-10-14부터 로컬 배열을 정렬하는 **선택형 helper**
20
+ (`sortDataTableRows`)를 계약이 함께 제공한다. renderer는 여전히 행을 재배열하지 않고,
21
+ 제품이 helper를 부르거나 서버에 정렬을 맡긴다([아래](#정렬-helper와-native-renderer--2026-10-14)).
20
22
  - 행 선택은 `CollectionSelectionModel`(`src/behaviors.ts`)을 **그대로** 재사용한다 —
21
23
  `none | single | multiple`, controlled/uncontrolled 쌍 전부 Select/Menu와 같다.
22
24
  새 선택 타입을 만들지 않는다.
@@ -45,10 +47,11 @@
45
47
  않기로 한 것과 같은 실수가 된다(`docs/dropdown.md`가 Menu/Dropdown에서 이미 같은
46
48
  논리를 적용했다). 확장 가능한 상세 행이 필요한 제품은 `disclosureGroup` 행동
47
49
  계약을 행 단위로 합성한다.
48
- - **넣지 않은 것(불필요/미측정)**: 열 고정(고정열)과 가로 스크롤 동기화는 렌더러
49
- 기법이다. 열 리사이즈·드래그 재정렬·그룹 헤더(colgroup)·요약(합계) 행은 실제
50
+ - **넣지 않은 것(불필요/미측정)**: 열 리사이즈·드래그 재정렬·그룹 헤더(colgroup)는 실제
50
51
  제품 요구가 측정되지 않았다. 각각 나중에 vertical slice가 나오면 이 문서를
51
- 갱신하고 새 축을 연다.
52
+ 갱신하고 새 축을 연다. (2026-10-14 갱신: 처음 목록에 있던 열 고정과 요약(합계) 행은
53
+ Yajalal 기록 표에서 요구가 측정돼 시작 쪽 열 고정 `pinned: "start"`와 renderer의
54
+ `summaryRow` slot으로 열었다.)
52
55
  - **roving-tabindex 그리드 키보드 탐색을 만들지 않는다.** 정렬 가능한 헤더는
53
56
  일반 버튼, 선택 셀은 일반 checkbox/radio native tab stop이다 — ARIA grid의 셀
54
57
  단위 방향키 이동은 측정된 요구가 없고 지금 범위보다 훨씬 큰 접근성 표면이라
@@ -73,7 +76,8 @@
73
76
  별도 roving focus를 만들지 않는다.
74
77
  - Native renderer는 이번 배치에 없다(`platform: "web"`) — `behaviorRegistry`의
75
78
  `native` 필드는 breadcrumb·form 같은 다른 Web 전용 계약과 같은 빈 배열
76
- 자리표시자다.
79
+ 자리표시자다. (2026-10-14 갱신: Native renderer가 들어와 `native`는
80
+ roles `button`·`header`, state `selected`, action `activate`가 됐다.)
77
81
 
78
82
  **검증 화면.** 제품 vertical slice는 필요하지 않다. Renderer 계약을 정렬·tri-state selection·
79
83
  async 상태와 narrow long-copy 회귀로 확인하고, 소비 제품의 데이터 정렬·페이지네이션은 계속
@@ -82,7 +86,8 @@ async 상태와 narrow long-copy 회귀로 확인하고, 소비 제품의 데이
82
86
  ## Web renderer (2026-09-18)
83
87
 
84
88
  `@hjmds/react/data-table`의 `DataTable`이 이 계약을 실행한다. Web renderer는 전용 keyboard·
85
- selection·320px long-copy proof를 통과해 2026-09-29 stable로 승격한다. Native는 `unsupported`다.
89
+ selection·320px long-copy proof를 통과해 2026-09-29 stable로 승격한다. Native는 2026-10-14까지
90
+ `unsupported`였다(아래 절).
86
91
 
87
92
  - **기존 `Table`과 정렬 기능은 공유한다.** 2026-10-01 중복 조사에서 Table도 정렬 버튼과
88
93
  ascending/descending 순환을 제공하는데 표시 전용으로 설명했던 불일치를 확인했다.
@@ -109,3 +114,48 @@ selection·320px long-copy proof를 통과해 2026-09-29 stable로 승격한다.
109
114
  tri-state 선택과 disabled 제외, 단일 선택 radio 의미, 비제어 single·multiple 선택 보관과
110
115
  controlled 소유자 우선, 셀당 컨트롤 하나와 기본 tab 순서, async 상태 발표와 footer 조합, 44px hit area), `test/data-table.keyboard.browser.test.tsx` 1개(실제 Tab·Enter·Space
111
116
  입력), 그리고 `컴포넌트/탐색/Command Palette`의 표 화면.
117
+
118
+ ## 정렬 helper와 Native renderer — 2026-10-14
119
+
120
+ 2026-10-14 사용자 요청("유명한 차트·표 라이브러리를 리버스 엔지니어링해서 … 차트와 표 다 흡수")으로
121
+ [Chart](chart.md)와 함께 표의 경계도 다시 그었다. 처음 경계(정렬 실행은 제품 소유)의 결과로 Yajalal의
122
+ `RecordTable`과 다른 기록 표가 같은 정렬 규칙(표시 문자열 "0.340"·"36 ⅓"·"13.5억"의 숫자 해석, 빈 값을
123
+ 아래로, 같은 값의 원래 순서 유지)을 각자 다시 구현했다. 같은 규칙이 반복되므로 helper를 계약이 소유한다.
124
+ 참고한 개념은 TanStack Table(`sortDescFirst`, `sortUndefined: "last"`, 안정 정렬)과 AG Grid(열 고정)이며
125
+ 소스는 가져오지 않았다.
126
+
127
+ **열 옵션 두 개.**
128
+
129
+ - `sortDescFirst?: boolean` — 첫 누름을 내림차순으로 시작한다. 기록 표는 "높은 값 먼저"로 읽는 열이 많다.
130
+ 평균자책점·순위처럼 낮을수록 좋은 열은 끈다. 다음 상태는
131
+ `getNextDataTableSortState(current, columnId, cycle, dataTableFirstSortDirection(column))`로 정한다 —
132
+ `three-state`는 첫 방향 → 반대 방향 → 정렬 없음, `two-state`는 첫 방향 ↔ 반대 방향이다. 네 번째 인자가
133
+ 없으면 이전과 같이 오름차순으로 시작한다(호환).
134
+ - `pinned?: "start"` — 나머지 열이 가로로 스크롤될 때 그 열을 보이게 둔다(선수 이름 열). 시작 쪽에서
135
+ **끊기지 않고 이어진** 고정 열만 인정한다(`splitPinnedDataTableColumns`). 가운데 열 하나를 고정하면 열 순서가
136
+ 화면과 접근성 순서에서 달라지므로 받지 않는다. 끝 쪽 고정은 측정된 요구가 없다.
137
+
138
+ **정렬 helper(선택형).**
139
+
140
+ - `toDataTableSortValue(text)` — 표시 문자열을 정렬 키로 바꾼다. "0.340"·".340"·"36 ⅓"·"13.5억"·"61.0%"는 수,
141
+ 글자가 둘 이상인 "7승0무3패"·이름은 문자열, "—"·"-"·""는 빈 값이다.
142
+ - `compareDataTableValues(a, b, locale?)` — 수는 수로, 문자열은 `localeCompare`로, 수가 문자열보다 앞이다.
143
+ - `sortDataTableRows(rows, sort, getValue, { locale })` — 안정 정렬이며 **빈 값은 방향과 상관없이 항상 마지막**이다.
144
+ 내림차순에서 빈 값이 맨 위로 올라오면 "기록 없음"이 1위처럼 보이기 때문이다. `sort`가 `null`이면 원래 순서다.
145
+ - 서버 정렬 제품은 helper를 쓰지 않고 지금처럼 `getNextDataTableSortState`만 부른다. renderer는 어느 쪽에서도
146
+ 행을 재배열하지 않는다(제어 `sortState` 경계 유지).
147
+
148
+ **요약 행.** renderer prop `summaryRow?: (columnId) => ReactNode`가 표 끝에 합계·평균 행을 그린다(Web `tfoot`).
149
+ 값 계산과 문구는 제품이 한다. 계약 descriptor 축이 아니라 renderer slot이다.
150
+
151
+ **Native renderer.** `@hjmds/react-native/data-table`의 `DataTable`이 같은 계약을 실행한다. Web과 같은 이름의 prop
152
+ (`columns`·`rows`·`labels.table`·`labels.sortColumn`·`renderCell`·`sortState`/`onSortChange`/`sortCycle`·`asyncState`·
153
+ `density`·`footer`·`summaryRow`)을 쓰고, Native 전용으로 `onRowPress?(rowId)`·`selectedRowId?`를 둔다.
154
+
155
+ - 고정 열은 제자리에 두고 나머지 열은 가로 `ScrollView` 안에 두며 스크롤 가장자리에 흐림(fade)을 표시한다.
156
+ 좁은 폭에서 열을 줄이지 않고 가로 스크롤로 보존하는 선택이다 — 기록 표는 열을 빼면 비교 자체가 안 된다.
157
+ - 정렬 머리 칸은 `button` role이며 머리 행은 `header`, 행 누름은 `activate`, 선택된 행은 `selected` 상태로 나간다.
158
+ - Web의 checkbox/radio 다중 선택 열(`selection`)은 Native에 없다. 행 하나를 고르는 일은 `onRowPress`·`selectedRowId`로
159
+ 제품이 소유한다. 다중 선택 일괄 작업의 Native 요구는 측정되지 않았다.
160
+ - 2026-10-14 작성 시점에 Native renderer는 구현 중이다. renderer 테스트·Storybook(`배포/컴포넌트/데이터 표시/데이터 표`의
161
+ Native 등록)·기기 확인 결과는 들어온 뒤 이 절에 기록한다. 카탈로그 행의 Native 지원 표시는 그때 함께 바꾼다.
@@ -0,0 +1,26 @@
1
+ # 데이터 해석과 가져오기 구성
2
+
3
+ 검토일: 2026-10-15 · Storybook 배포 · Web/Native · 미게시(1.20.0 이후)
4
+
5
+ Lucian 130·160–164를 검토하면서 기존 표·차트·통계의 실제 데이터 연결을 재사용할 필요를 확인했다.
6
+ 별도의 표/차트 엔진을 만들지 않고 DataTable, BarChart, Statistic, Progress, Select를 합성한다.
7
+ MatrixTable은 팀 간 비교에서 대각선을 숨기는 계약이므로 직사각형 코호트나 자기상관 보존에 쓰지 않는다.
8
+ 공개 진입점은 `@hjmds/{react,react-native}/data-workflows`, 계산은 `@hjmds/design-contracts/data-workflows`다.
9
+
10
+ | API | 계약과 선택 기준 |
11
+ | --- | --- |
12
+ | ImportFieldMapping | stable source/target ID, 중복 target·필수 누락이면 가져오기/미리보기 차단. 명시적 skip 지원. 실제 parser/importer는 제품 소유. onImport에 동결된 연결 snapshot 전달. reject 시 연결 유지·재시도 |
13
+ | ConversionFunnel | 순차 단계의 감소하는 실제 정수 count만 허용. 첫 단계/직전 단계 분모 0은 null. 원인은 제품에서 공급하며 자동 추정하지 않음. 선택한 단계의 실제 detailLabel 표시 |
14
+ | CohortRetentionTable | size가 분모. retained의 null/빠진 미래 기간은 미관측, 관측 0은 0%. size 0은 비율 미정. 직사각형 기간을 모두 보존 |
15
+ | CorrelationMatrix | [-1,1] 대칭 정방행렬, 관측된 대각선 1. null 보존. 수치와 dataviz 양/음 색을 함께 제공. 상관 계산·인과 판정은 제품 소유 |
16
+ | TargetSummary | 실제값·목표는 유한한 비음수. 목표 0은 ratio/progress null이며 Progress 생략. 초과 달성 ratio를 보존하고 표시 막대만 1로 제한 |
17
+ | HistogramExplorer | 1–100개 동폭 bin, 마지막 상한 포함. null·범위 밖 값 수 별도 집계. 동일 samples/domain과 모든 구간 선택의 최대 count를 사용해 y축 유지 |
18
+
19
+ 모든 문구·수치 formatter·데이터는 제품에서 제공한다. Native 가져오기 선택 창의 dismissLabel도 제품 문구다.
20
+ 오류/검증 문구는 구성의 중앙에 놓고 임의 상단 여백을 추가하지 않는다.
21
+ 연결의 서버 검증, CSV 보안/인코딩, 저장·인증·권한·멱등성은 실제 제품 importer가 소유한다.
22
+
23
+ 검증: 계산 6건, Web 브라우저 5건, Native host 2건 통과. 비동기 공통 helper 추출 후 기존 목록 편집 Web 7건/Native 5건 통과.
24
+ Native 실제 기기·제품 연결·npm 게시·운영 배포는 미실행이다.
25
+
26
+ 신규 6개 개발 Storybook 화면 확인. 두 제품 팔레트 light/dark 및 375px host 검사 통과. 본 작업의 6012 검사용 서버·탭 종료, 기존 6006 서버 보존.
@@ -1,12 +1,12 @@
1
1
  # Component maturity and evidence manifest
2
2
 
3
- > Generated from `@hjmds/design-contracts` 1.20.0. Do not edit this file directly; run `pnpm contracts:sync`.
3
+ > Generated from `@hjmds/design-contracts` 1.21.0. Do not edit this file directly; run `pnpm contracts:sync`.
4
4
 
5
5
  This document is the human-readable projection of the same catalog and Showcase manifest consumed by product CI.
6
6
 
7
- - Contract: stable 103
8
- - Web renderer: stable 103
9
- - Native renderer: stable 83 · unsupported 20
7
+ - Contract: stable 103 · beta 1
8
+ - Web renderer: stable 103 · beta 1
9
+ - Native renderer: stable 83 · beta 2 · unsupported 19
10
10
 
11
11
  | Component | Canonical story ID | Contract | Web | Native | Documentation | Required evidence |
12
12
  | --- | --- | --- | --- | --- | --- | --- |
@@ -83,7 +83,8 @@ This document is the human-readable projection of the same catalog and Showcase
83
83
  | Accordion | `data-display/accordion` | stable | stable | stable | generated only | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
84
84
  | Statistic | `data-display/statistic` | stable | stable | stable | [authored](../statistic.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
85
85
  | Timeline | `data-display/timeline` | stable | stable | stable | [authored](../timeline.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
86
- | DataTable | `data-display/data-table` | stable | stable | unsupported | [authored](../data-table.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
86
+ | DataTable | `data-display/data-table` | stable | stable | beta | [authored](../data-table.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
87
+ | Chart | `data-display/chart` | beta | beta | beta | [authored](../chart.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
87
88
  | Tree | `data-display/tree` | stable | stable | unsupported | [authored](../tree.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
88
89
  | Calendar | `data-display/calendar` | stable | stable | stable | [authored](../calendar.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
89
90
  | Carousel | `data-display/carousel` | stable | stable | stable | [authored](../carousel.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "designSystemVersion": "1.20.0",
3
+ "designSystemVersion": "1.21.0",
4
4
  "source": "packages/design-contracts/docs/generated/showcase-manifest.json",
5
5
  "surfaces": {
6
6
  "web": {
7
7
  "packageName": "@hjmds/react",
8
- "packageVersion": "1.20.0",
9
- "activeComponents": 103,
8
+ "packageVersion": "1.21.0",
9
+ "activeComponents": 104,
10
10
  "claimedComponents": 103,
11
11
  "completeScenarioComponents": 103,
12
12
  "components": [
@@ -4073,6 +4073,36 @@
4073
4073
  ],
4074
4074
  "missingScenarios": []
4075
4075
  },
4076
+ {
4077
+ "componentId": "chart",
4078
+ "name": "Chart",
4079
+ "status": "beta",
4080
+ "claimed": false,
4081
+ "subpath": null,
4082
+ "exportNames": [],
4083
+ "proofs": [],
4084
+ "requiredScenarios": [
4085
+ "default",
4086
+ "dark",
4087
+ "long-copy",
4088
+ "large-text",
4089
+ "rtl",
4090
+ "reduced-motion",
4091
+ "accessibility",
4092
+ "keyboard"
4093
+ ],
4094
+ "claimedScenarios": [],
4095
+ "missingScenarios": [
4096
+ "default",
4097
+ "dark",
4098
+ "long-copy",
4099
+ "large-text",
4100
+ "rtl",
4101
+ "reduced-motion",
4102
+ "accessibility",
4103
+ "keyboard"
4104
+ ]
4105
+ },
4076
4106
  {
4077
4107
  "componentId": "tree",
4078
4108
  "name": "Tree",
@@ -5642,8 +5672,8 @@
5642
5672
  },
5643
5673
  "native": {
5644
5674
  "packageName": "@hjmds/react-native",
5645
- "packageVersion": "1.20.0",
5646
- "activeComponents": 83,
5675
+ "packageVersion": "1.21.0",
5676
+ "activeComponents": 85,
5647
5677
  "claimedComponents": 83,
5648
5678
  "completeScenarioComponents": 83,
5649
5679
  "components": [
@@ -9123,6 +9153,66 @@
9123
9153
  ],
9124
9154
  "missingScenarios": []
9125
9155
  },
9156
+ {
9157
+ "componentId": "data-table",
9158
+ "name": "DataTable",
9159
+ "status": "beta",
9160
+ "claimed": false,
9161
+ "subpath": null,
9162
+ "exportNames": [],
9163
+ "proofs": [],
9164
+ "requiredScenarios": [
9165
+ "default",
9166
+ "dark",
9167
+ "long-copy",
9168
+ "large-text",
9169
+ "rtl",
9170
+ "reduced-motion",
9171
+ "accessibility",
9172
+ "native-actions"
9173
+ ],
9174
+ "claimedScenarios": [],
9175
+ "missingScenarios": [
9176
+ "default",
9177
+ "dark",
9178
+ "long-copy",
9179
+ "large-text",
9180
+ "rtl",
9181
+ "reduced-motion",
9182
+ "accessibility",
9183
+ "native-actions"
9184
+ ]
9185
+ },
9186
+ {
9187
+ "componentId": "chart",
9188
+ "name": "Chart",
9189
+ "status": "beta",
9190
+ "claimed": false,
9191
+ "subpath": null,
9192
+ "exportNames": [],
9193
+ "proofs": [],
9194
+ "requiredScenarios": [
9195
+ "default",
9196
+ "dark",
9197
+ "long-copy",
9198
+ "large-text",
9199
+ "rtl",
9200
+ "reduced-motion",
9201
+ "accessibility",
9202
+ "native-actions"
9203
+ ],
9204
+ "claimedScenarios": [],
9205
+ "missingScenarios": [
9206
+ "default",
9207
+ "dark",
9208
+ "long-copy",
9209
+ "large-text",
9210
+ "rtl",
9211
+ "reduced-motion",
9212
+ "accessibility",
9213
+ "native-actions"
9214
+ ]
9215
+ },
9126
9216
  {
9127
9217
  "componentId": "calendar",
9128
9218
  "name": "Calendar",