@hjmds/design-contracts 1.19.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 (144) 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 +34 -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/bottom-navigation.d.ts +10 -0
  10. package/dist/bottom-navigation.d.ts.map +1 -1
  11. package/dist/bottom-navigation.js +10 -0
  12. package/dist/bottom-navigation.js.map +1 -1
  13. package/dist/calendar.d.ts +9 -1
  14. package/dist/calendar.d.ts.map +1 -1
  15. package/dist/calendar.js +4 -2
  16. package/dist/calendar.js.map +1 -1
  17. package/dist/catalog.d.ts +148 -2
  18. package/dist/catalog.d.ts.map +1 -1
  19. package/dist/catalog.js +6 -2
  20. package/dist/catalog.js.map +1 -1
  21. package/dist/chart.d.ts +398 -0
  22. package/dist/chart.d.ts.map +1 -0
  23. package/dist/chart.js +414 -0
  24. package/dist/chart.js.map +1 -0
  25. package/dist/collection-editing.d.ts +38 -0
  26. package/dist/collection-editing.d.ts.map +1 -0
  27. package/dist/collection-editing.js +53 -0
  28. package/dist/collection-editing.js.map +1 -0
  29. package/dist/communication.d.ts +29 -0
  30. package/dist/communication.d.ts.map +1 -0
  31. package/dist/communication.js +11 -0
  32. package/dist/communication.js.map +1 -0
  33. package/dist/component-definitions.d.ts +1 -0
  34. package/dist/component-definitions.d.ts.map +1 -1
  35. package/dist/component-definitions.js +1 -0
  36. package/dist/component-definitions.js.map +1 -1
  37. package/dist/component-recipes.d.ts +25 -0
  38. package/dist/component-recipes.d.ts.map +1 -1
  39. package/dist/component-recipes.js +13 -0
  40. package/dist/component-recipes.js.map +1 -1
  41. package/dist/data-table.d.ts +229 -5
  42. package/dist/data-table.d.ts.map +1 -1
  43. package/dist/data-table.js +242 -6
  44. package/dist/data-table.js.map +1 -1
  45. package/dist/data-workflows.d.ts +70 -0
  46. package/dist/data-workflows.d.ts.map +1 -0
  47. package/dist/data-workflows.js +109 -0
  48. package/dist/data-workflows.js.map +1 -0
  49. package/dist/dataviz.d.ts +7 -6
  50. package/dist/dataviz.d.ts.map +1 -1
  51. package/dist/dataviz.js +7 -6
  52. package/dist/dataviz.js.map +1 -1
  53. package/dist/gooey-navigation.d.ts +7 -0
  54. package/dist/gooey-navigation.d.ts.map +1 -1
  55. package/dist/gooey-navigation.js +10 -0
  56. package/dist/gooey-navigation.js.map +1 -1
  57. package/dist/index.d.ts +2 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/media-transport.d.ts +16 -0
  62. package/dist/media-transport.d.ts.map +1 -0
  63. package/dist/media-transport.js +17 -0
  64. package/dist/media-transport.js.map +1 -0
  65. package/dist/recipes.d.ts +1 -0
  66. package/dist/recipes.d.ts.map +1 -1
  67. package/dist/recipes.js +1 -0
  68. package/dist/recipes.js.map +1 -1
  69. package/dist/semantic-colors.d.ts +9 -0
  70. package/dist/semantic-colors.d.ts.map +1 -1
  71. package/dist/semantic-colors.js +5 -0
  72. package/dist/semantic-colors.js.map +1 -1
  73. package/dist/showcase.d.ts.map +1 -1
  74. package/dist/showcase.js +3 -1
  75. package/dist/showcase.js.map +1 -1
  76. package/dist/social-compositions.d.ts +19 -0
  77. package/dist/social-compositions.d.ts.map +1 -0
  78. package/dist/social-compositions.js +33 -0
  79. package/dist/social-compositions.js.map +1 -0
  80. package/dist/streaming-answer.d.ts +18 -0
  81. package/dist/streaming-answer.d.ts.map +1 -0
  82. package/dist/streaming-answer.js +61 -0
  83. package/dist/streaming-answer.js.map +1 -0
  84. package/dist/task-controls.d.ts +31 -0
  85. package/dist/task-controls.d.ts.map +1 -0
  86. package/dist/task-controls.js +25 -0
  87. package/dist/task-controls.js.map +1 -0
  88. package/dist/toggle-feedback.d.ts +22 -0
  89. package/dist/toggle-feedback.d.ts.map +1 -0
  90. package/dist/toggle-feedback.js +32 -0
  91. package/dist/toggle-feedback.js.map +1 -0
  92. package/dist/transfer-task.d.ts +13 -0
  93. package/dist/transfer-task.d.ts.map +1 -0
  94. package/dist/transfer-task.js +13 -0
  95. package/dist/transfer-task.js.map +1 -0
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/version.js.map +1 -1
  99. package/docs/authoring-tools.md +31 -0
  100. package/docs/bottom-navigation.md +11 -0
  101. package/docs/calendar.md +3 -1
  102. package/docs/catalog-freeze.json +5 -0
  103. package/docs/chart.md +206 -0
  104. package/docs/collection-editing.md +83 -0
  105. package/docs/communication.md +35 -0
  106. package/docs/data-table.md +56 -6
  107. package/docs/data-workflows.md +26 -0
  108. package/docs/generated/component-maturity.md +6 -5
  109. package/docs/generated/renderer-evidence.json +95 -5
  110. package/docs/generated/renderer-evidence.md +6 -3
  111. package/docs/generated/showcase-manifest.json +66 -3
  112. package/docs/gooey-navigation.md +20 -0
  113. package/docs/library-gap-analysis.md +4 -2
  114. package/docs/media-transport.md +37 -0
  115. package/docs/social-compositions.md +51 -0
  116. package/docs/streaming-answer.md +57 -0
  117. package/docs/task-controls.md +38 -0
  118. package/docs/toggle-feedback.md +56 -0
  119. package/docs/transfer-task.md +33 -0
  120. package/docs/usage/README.md +15 -4
  121. package/docs/usage/components/activity-chip.md +2 -2
  122. package/docs/usage/components/bottom-navigation.md +14 -1
  123. package/docs/usage/components/button.md +39 -0
  124. package/docs/usage/components/calendar.md +4 -2
  125. package/docs/usage/components/chart.md +277 -0
  126. package/docs/usage/components/data-table.md +92 -18
  127. package/docs/usage/components/screen-layout.md +3 -1
  128. package/docs/usage/components/tabs.md +25 -0
  129. package/docs/usage/components/toast.md +1 -1
  130. package/docs/usage/compositions/action-feedback.md +18 -2
  131. package/docs/usage/compositions/activity-status.md +2 -2
  132. package/docs/usage/compositions/authoring-tools.md +90 -0
  133. package/docs/usage/compositions/collection-editing.md +110 -0
  134. package/docs/usage/compositions/communication.md +100 -0
  135. package/docs/usage/compositions/data-workflows.md +91 -0
  136. package/docs/usage/compositions/media-transport.md +87 -0
  137. package/docs/usage/compositions/social-compositions.md +91 -0
  138. package/docs/usage/compositions/streaming-answer.md +97 -0
  139. package/docs/usage/compositions/task-controls.md +93 -0
  140. package/docs/usage/compositions/toggle-feedback.md +84 -0
  141. package/docs/usage/compositions/transfer-task.md +86 -0
  142. package/docs/usage/compositions/web-additions.md +2 -2
  143. package/docs/usage/screens/common-shell.md +4 -0
  144. package/package.json +67 -1
@@ -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.19.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.19.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.19.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.19.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",
@@ -1,11 +1,11 @@
1
1
  # Renderer evidence coverage
2
2
 
3
- > Generated for HJM 1.19.0. Do not edit directly; run `pnpm evidence:sync`.
3
+ > Generated for HJM 1.21.0. Do not edit directly; run `pnpm evidence:sync`.
4
4
 
5
5
  This projection joins the canonical surface maturity manifest with first-party renderer claims. Missing scenarios are explicit beta promotion debt; stable surfaces are blocked by CI until none remain.
6
6
 
7
- - web: 103/103 active implementations; 103/103 required scenario sets
8
- - native: 83/83 active implementations; 83/83 required scenario sets
7
+ - web: 103/104 active implementations; 103/104 required scenario sets
8
+ - native: 83/85 active implementations; 83/85 required scenario sets
9
9
 
10
10
  | Component | Surface | Maturity | Renderer package | Export | Claimed scenarios | Missing required scenarios | Executable proofs |
11
11
  | --- | --- | --- | --- | --- | --- | --- | --- |
@@ -83,6 +83,7 @@ This projection joins the canonical surface maturity manifest with first-party r
83
83
  | Statistic | web | stable | @hjmds/react | ./display | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy | none | test/default-render.ssr.test.tsx#statistic, test/scenario-matrix.browser.test.tsx#statistic |
84
84
  | Timeline | web | stable | @hjmds/react | ./display | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy | none | test/default-render.ssr.test.tsx#timeline, test/scenario-matrix.browser.test.tsx#timeline |
85
85
  | DataTable | web | stable | @hjmds/react | ./data-table | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy, keyboard | none | test/default-render.ssr.test.tsx#data-table, test/scenario-matrix.browser.test.tsx#data-table, test/data-table.keyboard.browser.test.tsx#data-table |
86
+ | Chart | web | beta | @hjmds/react | unclaimed | none | default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard | none |
86
87
  | Tree | web | stable | @hjmds/react | ./tree | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy, keyboard | none | test/default-render.ssr.test.tsx#tree, test/scenario-matrix.browser.test.tsx#tree, test/tree.browser.test.tsx#tree |
87
88
  | Calendar | web | stable | @hjmds/react | ./calendar | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy, keyboard | none | test/default-render.ssr.test.tsx#calendar, test/scenario-matrix.browser.test.tsx#calendar, test/calendar.browser.test.tsx#calendar |
88
89
  | Carousel | web | stable | @hjmds/react | ./carousel | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy, keyboard | none | test/default-render.ssr.test.tsx#carousel, test/scenario-matrix.browser.test.tsx#carousel, test/carousel.browser.test.tsx#carousel |
@@ -176,6 +177,8 @@ This projection joins the canonical surface maturity manifest with first-party r
176
177
  | Accordion | native | stable | @hjmds/react-native | ./data-display | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy, native-actions | none | test/default-render.test.tsx#accordion, test/scenario-matrix.test.tsx#accordion, test/accordion-actions.test.tsx#accordion |
177
178
  | Statistic | native | stable | @hjmds/react-native | ./data-display | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy | none | test/default-render.test.tsx#statistic, test/scenario-matrix.test.tsx#statistic |
178
179
  | Timeline | native | stable | @hjmds/react-native | ./data-display | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy | none | test/default-render.test.tsx#timeline, test/scenario-matrix.test.tsx#timeline |
180
+ | DataTable | native | beta | @hjmds/react-native | unclaimed | none | default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions | none |
181
+ | Chart | native | beta | @hjmds/react-native | unclaimed | none | default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions | none |
179
182
  | Calendar | native | stable | @hjmds/react-native | ./calendar | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy, native-actions | none | test/default-render.test.tsx#calendar, test/scenario-matrix.test.tsx#calendar, test/calendar.test.tsx#calendar |
180
183
  | Carousel | native | stable | @hjmds/react-native | ./carousel | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy, native-actions | none | test/default-render.test.tsx#carousel, test/scenario-matrix.test.tsx#carousel, test/carousel.test.tsx#carousel |
181
184
  | DescriptionList | native | stable | @hjmds/react-native | ./data-display | default, accessibility, dark, large-text, rtl, reduced-motion, long-copy | none | test/default-render.test.tsx#description-list, test/scenario-matrix.test.tsx#description-list |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "designSystemVersion": "1.19.0",
3
+ "designSystemVersion": "1.21.0",
4
4
  "packageName": "@hjmds/design-contracts",
5
5
  "components": [
6
6
  {
@@ -3462,11 +3462,11 @@
3462
3462
  "storyId": "data-display/data-table",
3463
3463
  "name": "DataTable",
3464
3464
  "category": "data-display",
3465
- "platform": "web",
3465
+ "platform": "shared",
3466
3466
  "contractStatus": "stable",
3467
3467
  "surfaces": {
3468
3468
  "web": "stable",
3469
- "native": "unsupported"
3469
+ "native": "beta"
3470
3470
  },
3471
3471
  "requirements": [
3472
3472
  {
@@ -3487,6 +3487,19 @@
3487
3487
  "accessibility",
3488
3488
  "keyboard"
3489
3489
  ]
3490
+ },
3491
+ {
3492
+ "surface": "native",
3493
+ "scenarios": [
3494
+ "default",
3495
+ "dark",
3496
+ "long-copy",
3497
+ "large-text",
3498
+ "rtl",
3499
+ "reduced-motion",
3500
+ "accessibility",
3501
+ "native-actions"
3502
+ ]
3490
3503
  }
3491
3504
  ],
3492
3505
  "documentation": {
@@ -3494,6 +3507,56 @@
3494
3507
  "authored": "docs/data-table.md"
3495
3508
  }
3496
3509
  },
3510
+ {
3511
+ "id": "chart",
3512
+ "storyId": "data-display/chart",
3513
+ "name": "Chart",
3514
+ "category": "data-display",
3515
+ "platform": "shared",
3516
+ "contractStatus": "beta",
3517
+ "surfaces": {
3518
+ "web": "beta",
3519
+ "native": "beta"
3520
+ },
3521
+ "requirements": [
3522
+ {
3523
+ "surface": "contract",
3524
+ "scenarios": [
3525
+ "contract"
3526
+ ]
3527
+ },
3528
+ {
3529
+ "surface": "web",
3530
+ "scenarios": [
3531
+ "default",
3532
+ "dark",
3533
+ "long-copy",
3534
+ "large-text",
3535
+ "rtl",
3536
+ "reduced-motion",
3537
+ "accessibility",
3538
+ "keyboard"
3539
+ ]
3540
+ },
3541
+ {
3542
+ "surface": "native",
3543
+ "scenarios": [
3544
+ "default",
3545
+ "dark",
3546
+ "long-copy",
3547
+ "large-text",
3548
+ "rtl",
3549
+ "reduced-motion",
3550
+ "accessibility",
3551
+ "native-actions"
3552
+ ]
3553
+ }
3554
+ ],
3555
+ "documentation": {
3556
+ "generated": "docs/generated/component-maturity.md",
3557
+ "authored": "docs/chart.md"
3558
+ }
3559
+ },
3497
3560
  {
3498
3561
  "id": "tree",
3499
3562
  "storyId": "data-display/tree",
@@ -65,3 +65,23 @@ not choose a different panel lifetime: products still own controlled drafts or
65
65
  `mountPolicy`. The [QA record](../../../docs/qa/2026-10-07-design-profile-research.md)
66
66
  separates actual Chromium checks from Native mock-host tests; Native device
67
67
  appearance/performance and experiment promotion remain pending.
68
+
69
+ ## Pager-driven underline (unpublished · 2026-10-15)
70
+
71
+ Lucian 048 requires an underline to follow the actual fractional pager position rather than
72
+ animating after a selection has already settled. Horizontal `Tabs.swipePosition` accepts a
73
+ `TabSwipePosition` source (`get(): number`, `subscribe(listener): unsubscribe`). Position 0 is
74
+ the first item; 1.4 is 40% between the second and third. The source should publish actual scroll
75
+ or pager events; `Tabs` never starts, captures or cancels page gestures.
76
+
77
+ Both renderers interpolate measured physical tab rectangles using `resolveSwipeTabIndicator`.
78
+ Out-of-range positions clamp; non-finite positions and incomplete layout are ignored. Web writes
79
+ the indicator's left/width directly; Native writes two `Animated.Value`s. No React update occurs
80
+ per source frame. This direct user-driven position remains continuous under reduced motion;
81
+ there is no autonomous easing to suppress. Vertical tabs ignore the source.
82
+
83
+ Selection, keyboard navigation, disabled tabs and panel mounting remain owned by the ordinary
84
+ `value`/`onValueChange` contract. The product commits selection on pager settle and drives the
85
+ pager when a tab is activated. `swipePosition` only moves the underline, so publishing a frame
86
+ never silently activates an off-screen panel. Source subscriptions are removed on replacement
87
+ or unmount, and tab remeasurement reapplies the current source position.
@@ -76,8 +76,10 @@ control의 `accessibilityLabel`/`accessibilityHint`가 canonical 번역이다.
76
76
  아니라 배치다. 목록 전용 변형을 따로 두면 같은 제목이 화면 위치에 따라 다른 컴포넌트가
77
77
  된다. `Section` + `List` 조합으로 충분하고, 그 조합이 부족하다는 실측이 나오면 그때
78
78
  `Section`의 축으로 검토한다.
79
- - `Chart`: **토큰만 채택한다.** 렌더러는 만들지 않고 계열 팔레트·축·격자·범례 토큰만
80
- 고정한다. 근거는 [catalog-cleanup.md](./catalog-cleanup.md).
79
+ - `Chart`: ~~**토큰만 채택한다.** 렌더러는 만들지 않고 계열 팔레트·축·격자·범례 토큰만
80
+ 고정한다. 근거는 [catalog-cleanup.md](./catalog-cleanup.md).~~ **2026-10-14 번복.** 제품이 같은
81
+ 차트·정렬 표를 직접 다시 만든 것을 확인했고 사용자가 흡수를 요청해 Web·Native 렌더러를 둔다.
82
+ [chart.md](./chart.md).
81
83
 
82
84
  ## 후속 검토
83
85
 
@@ -0,0 +1,37 @@
1
+ # 미디어 재생 제어
2
+
3
+ 검토일: 2026-10-15 · Storybook 배포 · Web/Native · 미게시(1.20.0 이후)
4
+
5
+ Lucian 007의 재생/정지 모핑을 실제 미디어 상태와 연결할 공통 구성이 필요했다.
6
+ 기존 IconButton과 Slider를 합성하며 재생 엔진·탐색 제스처·타이머를 새로 구현하지 않는다.
7
+ `@hjmds/{react,react-native}/media-transport`의 MediaTransport를 사용한다.
8
+ 공유 `@hjmds/design-contracts/media-transport`는 관측값 해석과 모핑 경로만 소유한다.
9
+
10
+ ## 실제 상태와 행동
11
+
12
+ `state`는 제품 플레이어의 관측된 playing/position/duration/buffering이다. 클릭으로 playing을 미리 바꾸지 않는다.
13
+ `onPlay`/`onPause`/`onSeek`는 실제 엔진을 호출하고 실패하면 reject한다. HJM은 callback의 성공만으로 재생 상태를 만들지 않는다.
14
+ `duration:null`은 길이를 모르는 라이브/미확인 상태다. duration 0과 함께 탐색 슬라이더를 생략한다.
15
+ 알려진 양의 길이에서만 [0,duration]을 탐색하며 엔진이 끝을 약간 넘겨 보고한 position은 표시 범위 안으로 제한한다.
16
+ 슬라이더 이동 중 값은 로컬 초안이다. 손/키를 놓으면 실제 onSeek를 한 번 실행하고 확인된 엔진 위치로 돌아온다.
17
+ Web adapter는 seeked/error에서 확정하고, Native adapter는 엔진 setter의 실제 수신/반영 계약으로 확정한다.
18
+ 미디어 버퍼링 중 이미 playing이면 일시 정지는 계속 가능하다. 요청 중에는 공통 action-session이 중복 실행을 막는다.
19
+ 실패하면 현재 관측 상태를 유지하고 같은 제어로 재시도한다. 메시지는 영역 중앙에 표시한다.
20
+
21
+ 제품은 엔진 이벤트 수명·인증·백그라운드 재생·플레이어 해제를 소유한다. 다른 미디어로 바꿀 때 host key를 바꾸고 이전 엔진을 해제한다.
22
+ `active:false`는 숨겨진 컨트롤의 동작·모핑을 멈추고 탐색 초안을 버린다. 엔진 자체의 pause/dispose는 제품 adapter가 수행한다.
23
+
24
+ ## 표현과 플랫폼
25
+
26
+ 동일 vertex 수의 두 path를 실제 playing의 play/pause 모양 사이로 보간한다. 시간/진행률을 가짜로 늘리지 않는다.
27
+ Web requestAnimationFrame은 경로 attribute만 변경한다. Native Animated.Value listener는 SVG host d만 변경하며 useNativeDriver=false인 이유는 d가 경로 문자열이기 때문이다.
28
+ 모핑은 공통 motion.normal 동안만 실행한다. reduced motion·비활성·숨긴 문서/백그라운드에서는 목표 경로로 정착하며 루프를 남기지 않는다.
29
+ 프레임마다 React setState를 호출하지 않는다. 실제 재생 시간 갱신 빈도는 제품 엔진 이벤트 정책이다.
30
+ Native는 기존 optional peer react-native-svg가 필요하며 이 별도 진입점을 import할 때만 사용한다.
31
+ 레이블·시간 formatter·길이 미정·실패 문구는 제품에서 공급한다. 슬라이더 접근성/키보드는 기존 Slider 계약을 따른다.
32
+
33
+ ## 예제와 검증 경계
34
+
35
+ Web 예제는 기존 로컬 preview.mp4의 실제 HTMLVideoElement play/pause/timeupdate/seeked를 연결한다.
36
+ Native 예제는 기존 Expo Video와 sourceLoad/playingChange/timeUpdate를 연결하며 모듈이 없으면 지원되지 않음을 표시한다.
37
+ 두 예제 모두 숨겨졌을 때 실제 엔진을 멈추고 엔진 hook/host 수명에 따라 정리한다. Native 실기기 재생 검증과는 별개다.