@hjmds/design-contracts 1.13.0 → 1.14.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 (126) hide show
  1. package/dist/agreement.d.ts +7 -1
  2. package/dist/agreement.d.ts.map +1 -1
  3. package/dist/agreement.js +14 -3
  4. package/dist/agreement.js.map +1 -1
  5. package/dist/behaviors.d.ts +1 -1
  6. package/dist/catalog.d.ts +5 -0
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/component-recipes.d.ts +14 -0
  9. package/dist/component-recipes.d.ts.map +1 -1
  10. package/dist/component-recipes.js +16 -1
  11. package/dist/component-recipes.js.map +1 -1
  12. package/dist/content-transition.d.ts +17 -0
  13. package/dist/content-transition.d.ts.map +1 -1
  14. package/dist/content-transition.js +21 -0
  15. package/dist/content-transition.js.map +1 -1
  16. package/dist/date-entry.d.ts +71 -0
  17. package/dist/date-entry.d.ts.map +1 -0
  18. package/dist/date-entry.js +78 -0
  19. package/dist/date-entry.js.map +1 -0
  20. package/dist/document-resource.d.ts +104 -0
  21. package/dist/document-resource.d.ts.map +1 -0
  22. package/dist/document-resource.js +77 -0
  23. package/dist/document-resource.js.map +1 -0
  24. package/dist/effect-surface.d.ts +6 -1
  25. package/dist/effect-surface.d.ts.map +1 -1
  26. package/dist/effect-surface.js +5 -2
  27. package/dist/effect-surface.js.map +1 -1
  28. package/dist/field-group.d.ts +47 -0
  29. package/dist/field-group.d.ts.map +1 -0
  30. package/dist/field-group.js +92 -0
  31. package/dist/field-group.js.map +1 -0
  32. package/dist/image.d.ts +17 -0
  33. package/dist/image.d.ts.map +1 -1
  34. package/dist/image.js +19 -0
  35. package/dist/image.js.map +1 -1
  36. package/dist/internal/effect-noise.d.ts +2 -0
  37. package/dist/internal/effect-noise.d.ts.map +1 -0
  38. package/dist/internal/effect-noise.js +4 -0
  39. package/dist/internal/effect-noise.js.map +1 -0
  40. package/dist/progressive-blur.d.ts +32 -0
  41. package/dist/progressive-blur.d.ts.map +1 -0
  42. package/dist/progressive-blur.js +28 -0
  43. package/dist/progressive-blur.js.map +1 -0
  44. package/dist/reference-controls.d.ts +32 -0
  45. package/dist/reference-controls.d.ts.map +1 -0
  46. package/dist/reference-controls.js +28 -0
  47. package/dist/reference-controls.js.map +1 -0
  48. package/dist/scroll-progress.d.ts +7 -1
  49. package/dist/scroll-progress.d.ts.map +1 -1
  50. package/dist/scroll-progress.js +21 -2
  51. package/dist/scroll-progress.js.map +1 -1
  52. package/dist/text-annotation.d.ts +43 -0
  53. package/dist/text-annotation.d.ts.map +1 -0
  54. package/dist/text-annotation.js +137 -0
  55. package/dist/text-annotation.js.map +1 -0
  56. package/dist/version.d.ts +1 -1
  57. package/dist/version.js +1 -1
  58. package/dist/version.js.map +1 -1
  59. package/docs/agreement.md +14 -0
  60. package/docs/dialog.md +16 -1
  61. package/docs/effect-surface.md +19 -3
  62. package/docs/generated/component-maturity.md +1 -1
  63. package/docs/generated/renderer-evidence.json +3 -3
  64. package/docs/generated/renderer-evidence.md +1 -1
  65. package/docs/generated/showcase-manifest.json +1 -1
  66. package/docs/image.md +17 -0
  67. package/docs/optional-adapters.md +25 -0
  68. package/docs/popover.md +15 -0
  69. package/docs/rating.md +6 -1
  70. package/docs/reference-controls.md +40 -0
  71. package/docs/screen-patterns.md +2 -0
  72. package/docs/sheet.md +9 -0
  73. package/docs/task-list.md +15 -1
  74. package/docs/text-annotation.md +85 -0
  75. package/docs/toggle-group.md +6 -0
  76. package/docs/usage/README.md +18 -1
  77. package/docs/usage/components/agreement.md +13 -3
  78. package/docs/usage/components/alert-dialog.md +7 -1
  79. package/docs/usage/components/avatar.md +33 -1
  80. package/docs/usage/components/card.md +7 -1
  81. package/docs/usage/components/carousel.md +5 -1
  82. package/docs/usage/components/chat-message.md +11 -1
  83. package/docs/usage/components/chip.md +10 -2
  84. package/docs/usage/components/content-transition.md +28 -4
  85. package/docs/usage/components/dialog.md +18 -1
  86. package/docs/usage/components/effect-surface.md +4 -2
  87. package/docs/usage/components/empty-state.md +10 -2
  88. package/docs/usage/components/field.md +23 -0
  89. package/docs/usage/components/form.md +6 -1
  90. package/docs/usage/components/image-comparison.md +82 -0
  91. package/docs/usage/components/image.md +81 -2
  92. package/docs/usage/components/keyboard-avoiding.md +6 -1
  93. package/docs/usage/components/link.md +3 -1
  94. package/docs/usage/components/list-row.md +3 -1
  95. package/docs/usage/components/list.md +6 -1
  96. package/docs/usage/components/message-composer.md +5 -0
  97. package/docs/usage/components/popover.md +7 -1
  98. package/docs/usage/components/progress.md +28 -0
  99. package/docs/usage/components/progressive-blur.md +113 -0
  100. package/docs/usage/components/rating.md +74 -0
  101. package/docs/usage/components/search-field.md +10 -0
  102. package/docs/usage/components/search-screen.md +16 -5
  103. package/docs/usage/components/segmented-control.md +29 -4
  104. package/docs/usage/components/sheet.md +7 -1
  105. package/docs/usage/components/statistic.md +11 -0
  106. package/docs/usage/components/tags-input.md +5 -0
  107. package/docs/usage/components/toast.md +5 -0
  108. package/docs/usage/components/upload-item.md +3 -1
  109. package/docs/usage/compositions/action-feedback.md +77 -0
  110. package/docs/usage/compositions/adaptive-content.md +81 -0
  111. package/docs/usage/compositions/context-toolbar.md +85 -0
  112. package/docs/usage/compositions/date-entry.md +108 -0
  113. package/docs/usage/compositions/document-resource.md +124 -0
  114. package/docs/usage/compositions/field-group.md +104 -0
  115. package/docs/usage/compositions/illustrated-outcome.md +91 -0
  116. package/docs/usage/compositions/live-list.md +104 -0
  117. package/docs/usage/compositions/optional-adapters.md +1 -1
  118. package/docs/usage/compositions/origin-dialog.md +108 -0
  119. package/docs/usage/compositions/selection-motion.md +73 -0
  120. package/docs/usage/compositions/texture-comparison.md +86 -0
  121. package/docs/usage/compositions/upload-recovery.md +80 -0
  122. package/docs/usage/compositions/video-dialog.md +100 -0
  123. package/docs/usage/screens/common-search.md +7 -2
  124. package/docs/usage/screens/flow-onboarding.md +5 -3
  125. package/docs/usage/screens/product-bento.md +117 -0
  126. package/package.json +37 -1
@@ -0,0 +1,104 @@
1
+ # 관련 입력 묶음
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: [입력 그룹 조사](../../../../../docs/plans/field-group-experiment.md), `src/field-group.ts`
9
+ - 스토리북: `배포/구성/입력과 작성/관련 입력 묶음`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 주소·연락처처럼 여러 입력이 하나의 질문에 답할 때 쓴다. FieldGroup은 관련성·도움말·오류·잠금을
16
+ 소유하고 값·검증 시점·제출은 제품이 소유한다. Form 안에 여러 그룹을 둘 수 있다. 선택만 묶으면
17
+ CheckboxGroup/RadioGroup, 날짜 조각이면 DateEntry를 먼저 사용한다. 공통 제출 세션은 Form의 몫이다.
18
+
19
+ ## 구성 요소
20
+
21
+ | 컴포넌트 | 역할 | 지침 |
22
+ | --- | --- | --- |
23
+ | FieldGroup | 그룹 이름·설명·오류와 입력 연결 | 이 문서 |
24
+ | TextField 등 | renderField로 공급하는 개별 입력 | [Field](../components/field.md) |
25
+ | Form | 제품 제출 경계 | [Form](../components/form.md) |
26
+
27
+ ## 배치
28
+
29
+ ```text
30
+ 그룹 이름
31
+ 그룹 설명(선택)
32
+ 그룹 오류(선택, 한 번)
33
+ [입력 1]
34
+ 필드 설명 / 필드 오류
35
+ [입력 2]
36
+ 필드 설명 / 필드 오류
37
+ ```
38
+
39
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
40
+ | --- | --- | --- | --- |
41
+ | 바깥 틀 | Web fieldset / Native View | 기존 폼 안 | 테두리 없음, Web 최소 폭 0 |
42
+ | 그룹 이름 | Web legend / Native Text label | 맨 위 | Web 아래 spacing.sm 12, Native 그룹 간격 spacing.sm 12 |
43
+ | 입력 묶음 | renderField | descriptor.fields 순서 | 세로 간격 spacing.md 16 |
44
+ | 개별 도움말 | FieldGroup | 해당 입력 바로 아래 | Native 간격 spacing.xs 8, 글자 크기는 Text 기본 |
45
+
46
+ ## 흐름과 상태
47
+
48
+ 1. descriptor에 label과 fields를 넣는다. 필드 id는 그룹 안에서 고유하며 재정렬에도 유지한다.
49
+ 2. description·error는 이미 현지화한 문구다. 빈 문자열은 허용하지 않는다. 표시할 오류가 없으면 생략한다.
50
+ 3. 그룹 error는 message와 fieldIds를 받는다. 영향을 받는 필드만 invalid가 되고, 빈 배열은 그룹 전용 오류다.
51
+ 4. renderField의 controlProps를 개별 입력에 전달한다. FieldGroup이 도움말·오류를 이미 그리므로 같은 내용을
52
+ TextField description/error에 다시 넣지 않는다. Web id·aria 연결을 덮어쓰지 않는다.
53
+ 5. 값 변경에는 guardChange로 감싼 callback을 전달한다. 그룹 잠금·개별 잠금·필드 제거·언마운트 후
54
+ 남은 callback을 차단한다. 잠금 해제 시 원래 disabled인 필드는 계속 잠긴다. 제거가 commit된
55
+ 필드를 같은 id로 다시 추가해도 이전 callback은 복구하지 않는다. 새 renderField의 guardChange를
56
+ 사용한다. 2026-10-07 회귀에서 enabled id만 검사하면 이전 이벤트가 새 초안을 바꾸는 것을 확인했다.
57
+ 6. dynamic 필드 제거 시 해당 그룹 오류 대상도 함께 갱신한다. 사라진 id나 중복 오류 대상은 TypeError다.
58
+ 7. 제품이 값과 검증을 유지한다. 입력 순서·그룹 잠금 변경은 값을 초기화하지 않는다.
59
+
60
+ | 상태 | 모습 | 포커스·알림 |
61
+ | --- | --- | --- |
62
+ | 기본 | 그룹 설명과 독립 입력 | 자동 포커스 없음 |
63
+ | 진행 중 | 제품 값 편집 | 일반 Tab·터치 이동 |
64
+ | 실패 | 그룹 오류 한 번, 해당 입력 invalid, 개별 오류 보존 | Web 연결된 설명, Native hint에 그룹/개별 설명과 오류 |
65
+ | 비활성 | 각 입력 disabled | guardChange가 늦은 값 변경도 차단 |
66
+
67
+ ## 코드 골격
68
+
69
+ ```tsx
70
+ // Web
71
+ import { FieldGroup } from "@hjmds/react/field-group";
72
+ import { TextField } from "@hjmds/react/forms";
73
+ <FieldGroup descriptor={group} renderField={({ id, controlProps, guardChange }) =>
74
+ <TextField {...controlProps} value={values[id] ?? ""}
75
+ onValueChange={guardChange((value: string) => updateField(id, value))} />} />
76
+ ```
77
+
78
+ ```tsx
79
+ // Native
80
+ import { FieldGroup } from "@hjmds/react-native/field-group";
81
+ import { TextField } from "@hjmds/react-native/inputs";
82
+ <FieldGroup descriptor={group} renderField={({ id, controlProps, guardChange }) =>
83
+ <TextField {...controlProps} value={values[id] ?? ""}
84
+ onValueChange={guardChange((value: string) => updateField(id, value))} />} />
85
+ ```
86
+
87
+ 공통 descriptor/resolver/edit session은 `@hjmds/design-contracts/field-group`에 있다.
88
+ 사용자 정의 입력은 label·disabled·invalid·도움말을 자신의 실제 입력 host에 연결해야 한다.
89
+ controlProps는 TextField에 바로 연결되는 형태이며 Checkbox/Select 등 다른 공개 API와 호환되는지
90
+ 확인 없이 그대로 펼치지 않는다. renderField의 반환값 안에 독립 제출 버튼을 넣지 않는다.
91
+
92
+ ## 플랫폼 차이
93
+
94
+ | 항목 | Web | Native |
95
+ | --- | --- | --- |
96
+ | 그룹 의미 | fieldset의 첫 legend | 개별 입력 accessibilityLabel에 그룹 이름 포함, 부모 accessible=false |
97
+ | 필드 연결 | id·aria-invalid·aria-describedby | label·invalid·accessibilityLabel·accessibilityHint |
98
+ | 그룹 오류 | role=alert | assertive live region, iOS 별도 announcement |
99
+ | 화면 스크롤 | 제품 host | 제품 ScrollView·키보드 회피 host |
100
+
101
+ ## 함정
102
+
103
+ - 실제 스크린리더·Native 기기·제품 팔레트 검증은 아직 남았다. 자동 검사 통과를 승격 근거로 단독 사용하지 않는다.
104
+ - 국가·주소·연락처의 필드 순서와 autocomplete는 제품별로 정한다. 예제의 한국 주소 순서를 공통 규칙으로 복사하지 않는다.
@@ -0,0 +1,91 @@
1
+ # 그림과 시작 안내
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: `showcase/web/src/patterns/illustrated-outcome-preview.tsx`, `showcase/native/src/illustrated-outcome-preview.tsx`
9
+ - 스토리북: `배포/구성/피드백과 복구/그림과 시작 안내`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 빈 목록에서 시작을 안내하고 짧은 온보딩을 거쳐 결과를 보여 줄 때 쓴다. 3D 그림은 제품 자산이며
16
+ 공통 컴포넌트의 의미·초점·버튼을 대체하지 않는다. 별도 wrapper 없이 기존 공개 API를 사용한다.
17
+
18
+ ## 구성 요소
19
+
20
+ | 컴포넌트 | 역할 | 지침 |
21
+ | --- | --- | --- |
22
+ | EmptyState | 채울 수 있는 빈 목록과 첫 행동 | [빈 상태](../components/empty-state.md) |
23
+ | OnboardingScreen | 단계·이전·다음·완료 배치 | [시작 안내](../screens/flow-onboarding.md) |
24
+ | Result | 흐름이 끝난 결과와 다음 행동 | [결과](../components/result.md) |
25
+ | TextField | 제품이 소유하는 입력 초안 | [텍스트 필드](../components/field.md) |
26
+
27
+ ## 배치
28
+
29
+ ```text
30
+ [장식 그림]
31
+ [제목 + 설명]
32
+ [단계 본문: 그림 또는 입력]
33
+ [다음 / 완료]
34
+ [이전: 두 번째 단계부터]
35
+ ```
36
+
37
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
38
+ | --- | --- | --- | --- |
39
+ | 바깥 틀 | Stack | 본문 전체 | gap lg=20px |
40
+ | 빈 상태 그림 | EmptyState 제품 슬롯 | 제목 위 | 120×120, Asset xlarge와 같은 크기 |
41
+ | 단계 본문 | OnboardingScreen | 제목과 하단 행동 사이 | 그림 120×120 또는 TextField |
42
+ | 완료 | Result | 흐름 종료 자리 | 체크 32×32, 공통 56px 아이콘 영역 안 |
43
+ | 버튼 | 각 공개 컴포넌트 | 화면의 기존 footer 또는 action 영역 | 기존 recipe 최소 터치 크기 유지 |
44
+
45
+ ## 흐름과 상태
46
+
47
+ 1. EmptyState 행동으로 시작 안내를 연다. 화면의 현재 단계는 제품이 제어한다.
48
+ 2. 다음→입력→이전→다음에서도 초안은 상위 제품 상태에 남긴다.
49
+ 3. 예제는 제목이 공백이면 완료를 비활성화한다. 실제 앱의 저장 결과는 서버 응답으로만 확정한다.
50
+ 4. 그림 로딩 실패는 그림만 제거한다. 설명·입력·버튼은 유지하며 오류를 성공으로 바꾸지 않는다.
51
+ 5. 완료 후 다시 시작해도 초안을 유지한다. 초안 삭제는 제품의 명시적인 정책이 있을 때만 한다.
52
+
53
+ | 상태 | 모습 | 포커스·알림 |
54
+ | --- | --- | --- |
55
+ | 기본 | 빈 상태 제목·장식 그림·첫 행동 | 제목으로 의미를 전달하고 그림은 장식으로 숨김 |
56
+ | 진행 중 | 단계 안내·입력·이전/다음 행동 | 초안을 유지하며 실제 저장 중에는 제품 pending 연결 |
57
+ | 실패 | 그림 실패 시 본문·행동 유지 | 장식 실패를 저장 실패와 혼동하지 않음 |
58
+ | 완료 | 결과 제목·입력 내용·다시 시작 | Result의 기본 status 의미 유지 |
59
+
60
+ ## 코드 골격
61
+
62
+ ```tsx
63
+ // Web
64
+ import { EmptyState, Result } from "@hjmds/react/feedback";
65
+ import { OnboardingScreen } from "@hjmds/react/screen-flows";
66
+ <EmptyState title={title} description={description} icon={decorativeArtwork} action={startButton} />
67
+ <Result status="success" title={completedTitle} icon={decorativeCheck} actions={actions} />
68
+ ```
69
+
70
+ ```tsx
71
+ // Native
72
+ import { EmptyState, Result } from "@hjmds/react-native/feedback";
73
+ import { OnboardingScreen } from "@hjmds/react-native/screen-flows";
74
+ <EmptyState title={title} description={description} illustration={decorativeArtwork} action={startButton} />
75
+ <Result status="success" title={completedTitle} renderIcon={() => decorativeCheck} actions={actions} />
76
+ ```
77
+
78
+ OnboardingScreen에는 `steps`, `index`, `onIndexChange`, `nextLabel`, `backLabel`, `complete`,
79
+ `progressLabel`을 전달한다. 단계 구조·버튼 위치는 기존 화면 지침을 따른다. 제품 코드에서
80
+ Showcase를 import하지 않는다. 그림 호스트는 Web img의 빈 alt, Native Image의 accessible=false로 장식임을 명시한다.
81
+
82
+ ## 플랫폼 차이
83
+
84
+ | 항목 | Web | Native |
85
+ | --- | --- | --- |
86
+ | 파일 | 로컬 파일 URL | Metro 정적 require |
87
+ | 그림 접근성 | 빈 alt | accessible=false |
88
+ | 장식 슬롯 | EmptyState.icon / Result.icon | EmptyState.illustration / Result.renderIcon |
89
+ | 실패 | 그림만 숨김 | 그림만 숨김 |
90
+
91
+ 실제 기기의 자산 디코딩·큰 글자·접근성 검증은 타입 검사와 별도다.
@@ -0,0 +1,104 @@
1
+ # 추가해도 유지되는 목록
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: `showcase/web/src/patterns/live-list-preview.tsx`, `showcase/native/src/live-list-preview.tsx`
9
+ - 스토리북: `배포/구성/정보 표시/추가해도 유지되는 목록`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 입력 중인 목록에 새 데이터가 추가되거나 순서가 바뀌어도 기존 초안과 항목의 정체성을 유지할 때 쓴다.
16
+ Magic UI Animated List의 소개 페이지 순차 노출에서 새 항목의 등장 표현만 흡수한다. 이미 도착한
17
+ 데이터를 타이머로 늦추지 않고, 실제 알림 수신과 데모 재생을 혼동하지 않는다.
18
+
19
+ ## 구성 요소
20
+
21
+ | 컴포넌트 | 역할 | 지침 |
22
+ | --- | --- | --- |
23
+ | List · ListRow | stable key 항목의 목록 의미·제목·구분선 | [목록](../components/list.md), [행](../components/list-row.md) |
24
+ | ContentTransition | 새 항목 등장과 바깥 높이 전환 | [내용 전환](../components/content-transition.md) |
25
+ | TextField | 제품이 항목 id별로 보존하는 메모 | [필드](../components/field.md) |
26
+ | Button · Text | 명시적 데이터 변경과 변경 결과 | [버튼](../components/button.md), [글자](../components/text.md) |
27
+
28
+ ## 배치
29
+
30
+ ```text
31
+ [안내]
32
+ [기록 추가] [3개 함께 추가]
33
+ [순서 뒤집기] [첫 기록 삭제]
34
+ [움직임 멈추기]
35
+ [변경 결과]
36
+ ┌ 기록 목록 ────────────┐
37
+ │ 제목·설명 │
38
+ │ 메모 입력 │
39
+ ├───────────────────────┤
40
+ │ 다음 기록·메모 │
41
+ └───────────────────────┘
42
+ ```
43
+
44
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
45
+ | --- | --- | --- | --- |
46
+ | 바깥 틀 | Stack | 위에서 아래 | gap lg=20px |
47
+ | 변경 행동 | inline Stack · Button | 목록 위, 좁으면 감김 | 기본 Stack/Button recipe |
48
+ | 데이터 | List | 변경 결과 아래 | separator full, 공통 ListRow/TextField 크기 |
49
+ | 등장 | ContentTransition | 각 stable id 항목 | rise=12px, motion.normal, easing.enter |
50
+ | 높이 | ContentTransition animateHeight | 목록 바깥 | 같은 stateKey를 유지하며 실제 측정 높이만 전환 |
51
+
52
+ ## 흐름과 상태
53
+
54
+ 1. 초기 데이터는 바로 표시하고 최초 등장 모션은 끈다.
55
+ 2. 추가된 항목은 고유 id를 부여해 즉시 배열에 반영한다. 해당 항목만 enterOnMount를 켠다.
56
+ 3. 기존 id의 React key와 stateKey를 유지한다. 전체 목록 stateKey를 항목 개수로 바꾸지 않는다.
57
+ 4. 순서 변경은 데이터 배열 순서만 바꾸며 초안은 id에 연결한다. 순서 변경 자체의 이동 애니메이션은 제공하지 않는다.
58
+ 5. 삭제는 즉시 반영한다. 삭제된 입력·버튼을 담은 퇴장 사본을 남기지 않는다.
59
+ 6. 움직임 멈추기는 데이터 처리를 멈추지 않는다. 다시 켜도 기존 항목은 재생하지 않는다.
60
+
61
+ | 상태 | 모습 | 포커스·알림 |
62
+ | --- | --- | --- |
63
+ | 기본 | 기존 목록과 입력 | 초기 데이터 즉시 표시 |
64
+ | 진행 중 | 새 행만 짧게 등장 | 기존 입력 노드와 초안 유지 |
65
+ | 실패 | 서버 실패가 있는 제품은 기존 목록·초안을 보존하고 오류를 별도 표시 | 이 로컬 실험에는 네트워크 요청 없음 |
66
+ | 삭제 | 해당 행 즉시 제거 | 삭제 행동은 목록 밖에 두어 초점 유실 방지 |
67
+ | 비어 있음 | 기록 없음과 추가 안내 | 추가 버튼 유지 |
68
+ | 모션 감소·정지 | 같은 데이터, 공간 전환 없음 | 의미·입력 순서 동일 |
69
+
70
+ ## 코드 골격
71
+
72
+ ```tsx
73
+ // Web
74
+ import { ContentTransition } from "@hjmds/react/content-transition";
75
+ <ContentTransition stateKey="records" animateHeight motion={paused ? 'none' : 'system'}>
76
+ <List label={listLabel}>
77
+ {items.map(item => <ContentTransition key={item.id} stateKey={item.id}
78
+ enterOnMount={item.justAdded} preset="rise" motion={paused ? 'none' : 'system'}>
79
+ <RecordEditor item={item} onChange={changeById} />
80
+ </ContentTransition>)}
81
+ </List>
82
+ </ContentTransition>
83
+ ```
84
+
85
+ ```tsx
86
+ // Native
87
+ import { ContentTransition } from "@hjmds/react-native/content-transition";
88
+
89
+ <ContentTransition stateKey={item.id} enterOnMount={item.justAdded} preset="rise">
90
+ <RecordEditor item={item} onChange={changeById} />
91
+ </ContentTransition>
92
+ ```
93
+
94
+ ## 플랫폼 차이
95
+
96
+ | 항목 | Web | Native |
97
+ | --- | --- | --- |
98
+ | 등장 | 기존 framer-motion optional peer | 기존 RN Animated native driver |
99
+ | 높이 | ResizeObserver + WAAPI | onLayout + JS driver |
100
+ | 변경 알림 | status | accessibilityLiveRegion polite |
101
+ | 접근성 | 기존 입력 초점·list 의미 | 기존 입력과 Native list 의미 |
102
+
103
+ 데이터·중복 제거·정렬·서버 확정은 제품 소유다. 긴 목록은 VirtualList 선택을 먼저 검토하고
104
+ 재활용 mount를 새 데이터로 오인해 enterOnMount를 켜지 않는다. 큰 글자·제품 팔레트·성능은 승격 전 검증한다.
@@ -165,5 +165,5 @@ const gutter = resolveWindowClass(useWindowDimensions().width) === "compact" ? "
165
165
  - KeyboardDock은 window 기준 좌표를 쓴다. Storybook 캔버스처럼 아래에 다른 영역이 남는 host에서는 위치가 어긋나 스토리가 전체 화면 `Modal`로 띄운다. 제품도 앱 크기 host에서 쓴다.
166
166
  - NativeContextMenu 패치(Zeego 3.0.6 관련)는 HJM 설치로 적용되지 않는다. `@hjmds/react-native/docs/patches/`를 제품에 복사해 등록한다.
167
167
  - 필요한 optional peer(`react-native-zoom-toolkit` 5.1.1, `react-native-keyboard-controller` 1.22.5, `@gorhom/bottom-sheet` 5.2.14, `zeego` 3.0.6 등)가 없으면 기기 Metro 번들이 실패한다.
168
- - 현재 `ImageViewer`는 로딩 문구만 live region이고 실패 문구(`errorLabel`)는 알리지 않는다. 실패를 낭독해야 하면 제품이 따로 알린다(renderer 수정 후보).
168
+ - `ImageViewer` 실패는 Android assertive live region과 iOS announceForAccessibility로 알린다. 제품이 같은 오류를 다시 낭독하지 않는다. 제품 이미지 host와 상태 이벤트 연결은 [Image 사용 지침](../components/image.md)의 Native ImageViewer 절을 따른다.
169
169
  - 2026-10-06 예제 검수에서 머리·본문·하단의 좌우 여백을 Container compact로 맞추고 본문 간격은 Stack md로 옮겼다. 안전 영역·키보드 좌표는 바깥 host가 유지한다. 메뉴 결과는 상태 문구와 iOS/Android 알림을 함께 갱신한다.
@@ -0,0 +1,108 @@
1
+ # 버튼에서 이어지는 편집
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: `showcase/web/src/patterns/origin-dialog-preview.tsx`, `showcase/native/src/origin-dialog-preview.tsx`
9
+ - 스토리북: `배포/구성/입력과 작성/버튼에서 이어지는 편집`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 현재 화면의 항목을 짧게 편집하고 돌아올 때 출발 위치를 시각적으로 연결한다. 별도 상태
16
+ 엔진 없이 Dialog의 선택적 motionOrigin과 제품 소유 초안을 사용한다. 원본의 닫기 후
17
+ 초점·초안 손실을 그대로 가져오지 않기 위한 구성이다. 긴 편집은 전용 화면을 사용한다.
18
+
19
+ ## 구성 요소
20
+
21
+ | 컴포넌트 | 역할 | 지침 |
22
+ | --- | --- | --- |
23
+ | Button | 측정할 열기 버튼과 저장 | [버튼](../components/button.md) |
24
+ | Popover | Web에서 페이지를 계속 볼 수 있는 비모달 편집 변형 | [팝오버](../components/popover.md) |
25
+ | Dialog | 기존 모달 상태·닫기·초점 복귀와 선택적인 공간 전환 | [대화상자](../components/dialog.md) |
26
+ | TextField | 제품 상태에 보존하는 초안 | [필드](../components/field.md) |
27
+ | Text | 확정한 결과 표시 | [텍스트](../components/text.md) |
28
+
29
+ ## 배치
30
+
31
+ ```text
32
+ [안내]
33
+ [메모 편집] ──→ [여행 메모 닫기]
34
+ [저장 결과] [메모 입력]
35
+ [저장]
36
+ ```
37
+
38
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
39
+ | --- | --- | --- | --- |
40
+ | 바깥 틀 | Stack | 원래 화면 | gap lg=20px |
41
+ | 진행 중 | Dialog | 기본 overlay 중앙 | 기본 Dialog recipe |
42
+ | 입력 | TextField | Dialog 본문 | 공통 Field 크기·간격 |
43
+ | 확정 | Button | Web footer / Native primaryAction | 공통 Dialog 행동 영역 |
44
+
45
+ ## 흐름과 상태
46
+
47
+ 1. 열기 직전에 트리거를 실제 viewport/window 좌표로 측정한다. 측정 불가 시 일반 Dialog를 연다.
48
+ 2. 제품이 open·draft·saved를 각각 소유한다. 저장 전 닫기는 draft를 지우지 않는다.
49
+ 3. 저장 시 draft를 결과로 확정하고 닫는다. 실패 체험을 예약하면 첫 저장만 실패하고
50
+ 오류·초안을 유지한다. 다시 저장은 성공한다. 실제 서버가 있으면 busy와 확정 응답을 연결한다.
51
+ 4. 닫기 전환이 끝난 뒤 트리거로 초점을 돌린다. 빠른 재열기는 이전 종료 결과를 무시한다.
52
+ 5. 모션 감소·잘못된 측정에서는 공간 전환을 생략한다.
53
+
54
+ | 상태 | 모습 | 포커스·알림 |
55
+ | --- | --- | --- |
56
+ | 기본 | 열기 버튼과 마지막 저장 결과 | draft 유지 |
57
+ | 진행 중 | 제목·닫기·입력·저장 | 제품 상태로 갱신 |
58
+ | 결과 | 결과 갱신 후 닫기 | 트리거로 복귀 |
59
+ | 실패 | 오류 설명·입력·다시 저장 | 오류 알림, 초안 유지 |
60
+ | 측정 없음 | 일반 Dialog | 같음 |
61
+
62
+ ## 코드 골격
63
+
64
+ ```tsx
65
+ // Web: origin은 trigger.getBoundingClientRect()의 측정값
66
+ <Dialog open={open} onOpenChange={setOpen} title={title} closeLabel={closeLabel}
67
+ motionOrigin={origin} returnFocusRef={triggerRef} footer={saveButton}>
68
+ <TextField label={label} value={draft} onValueChange={setDraft} />
69
+ </Dialog>
70
+ ```
71
+
72
+ ```tsx
73
+ // Native: origin은 trigger.measureInWindow 콜백의 측정값
74
+ <Dialog open={open} onOpenChange={setOpen} title={title} closeLabel={closeLabel}
75
+ motionOrigin={origin} returnFocusRef={triggerRef} primaryAction={saveAction}>
76
+ <TextField label={label} value={draft} onValueChange={setDraft} />
77
+ </Dialog>
78
+ ```
79
+
80
+ 문구·초안·저장 로직·트리거 측정은 제품 소유다. Showcase를 앱에서 import하지 않는다.
81
+
82
+ ## 플랫폼 차이
83
+
84
+ | 항목 | Web | Native |
85
+ | --- | --- | --- |
86
+ | 원점 | getBoundingClientRect | measureInWindow |
87
+ | 전환 | WAAPI, 종료까지 한 subtree 유지 | 실제 modal 측정 + 기존 Animated progress |
88
+ | 모션 감소 | 공간 전환 없음 | 공간 전환 없음, 기존 opacity |
89
+ | 닫기 | Escape·바깥·닫기 | Back·바깥·닫기 |
90
+
91
+ 키보드·회전·스크롤에 의한 목적지 변화, 실제 기기와 제품 테마 비교는 승격 전 검증한다.
92
+
93
+
94
+ ### Web의 페이지 안에서 편집 변형
95
+
96
+ `Contextual`·`ContextualDark`·`ContextualLargeText`는 기존 Popover의 선택적인 `motionOrigin`을
97
+ 사용한다. 기본 360px 표면이 버튼 아래 8px 간격으로 열리고 viewport 충돌에 따라 배치가 바뀐다.
98
+ 본문은 Stack gap md=16px 안에 오류 → TextField → 저장 순서다. Native는 모달 편집 변형을 쓴다.
99
+
100
+ ```tsx
101
+ <Popover title={title} closeLabel={closeLabel} motionOrigin={origin}
102
+ trigger={<Button ref={triggerRef} onClick={() => setOrigin(triggerRef.current?.getBoundingClientRect())}>{openLabel}</Button>}>
103
+ {({ close }) => <Editor draft={draft} onDraftChange={setDraft} onSave={() => save(draft).then(close)} />}
104
+ </Popover>
105
+ ```
106
+
107
+ 비모달에서는 Escape/닫기 직후 기존 규칙대로 초점을 복귀한다. 바깥 클릭이나 Tab으로 이동한
108
+ 초점은 되가져오지 않는다. 저장 실패는 초안·열림을 유지하고, 닫았다 다시 열어도 제품 draft는 남는다.
@@ -0,0 +1,73 @@
1
+ # 선택 배경 이동
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: 공개 API를 사용하는 `showcase/*/reference-adoption-previews.tsx`
9
+ - 스토리북: `배포/구성/직접 조작과 모션/선택 배경 이동`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 짧은 단일 선택의 현재 항목을 이어지는 배경으로 보여 줄 때 쓴다. 선택 값·키보드·초점은 기존 SegmentedControl이 소유한다. 별도 신규 wrapper API가 아니라 아래 공개 컴포넌트의 조합 규격이다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | SegmentedControl · Text · TextField | 입력·표현·행동의 역할 분리 | [입력/동작](../components/segmented-control.md), [상태/전환](../components/content-transition.md) |
22
+
23
+ ## 배치
24
+
25
+ ```text
26
+ [제목 또는 현재 입력]
27
+ [상태/내용 영역]
28
+ [관련 행동과 결과]
29
+ ```
30
+
31
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
32
+ | --- | --- | --- | --- |
33
+ | 바깥 틀 | Stack | 화면의 본문 흐름 | gap lg=20px, 전체 폭 |
34
+ | 선택 | SegmentedControl 2개 | 제목 아래, 연결형 다음 필터형 | 기본 44px 터치 영역, 실제 글자에 따른 높이 |
35
+ | 결과·메모 | Text · TextField | 선택 컨트롤 아래 | 선택 변경 시 필드 재생성 없음 |
36
+ | Native 화면 host | ScreenLayout · KeyboardAvoiding | 본문 스크롤과 바깥 키보드 여백 | flex 1, 끝 여백 spacing.md=16 |
37
+
38
+ 2026-10-07 iOS 2배 글자에서 Stack만 사용한 예제의 메모가 키보드 뒤에 가려졌다.
39
+ 글자나 선택 항목을 줄이지 않고 위 화면 host를 연결해 입력·선택을 스크롤로 오가게 한다.
40
+ 선택 배경 이동은 입력 초점·스크롤을 소유하지 않으므로 제품 화면도 이 경계를 연결해야 한다.
41
+
42
+ ## 흐름과 상태
43
+
44
+ 1. 현재 선택 측정→사용자가 선택→배경만 이동. 메모 필드는 유지한다.
45
+ 2. 서버 응답·파일 권한·문구·브랜드는 제품이 전달한다. Showcase의 예제 응답과 고정 데이터를 가져오지 않는다.
46
+
47
+ | 상태 | 모습 | 포커스·알림 |
48
+ | --- | --- | --- |
49
+ | 기본 | 입력과 사용 가능한 행동 | 조작 이름은 제품 언어 |
50
+ | 진행 중 | 실제 작업 상태에 맞는 loading 또는 열린 도구 | 중복 요청 차단, 입력 유지 |
51
+ | 실패 | 문구와 가능한 재시도 | 원래 입력/파일 유지, 결과 안내 |
52
+
53
+ ## 코드 골격
54
+
55
+ ```tsx
56
+ // Web
57
+ <SegmentedControl label={t("period")} items={items} value={value} onValueChange={setValue} selectionMotion="slide" />
58
+ ```
59
+
60
+ ```tsx
61
+ // Native
62
+ <SegmentedControl label={t("period")} items={items} value={value} onValueChange={setValue} selectionMotion="slide" />
63
+ ```
64
+
65
+ Web은 `@hjmds/react`의 해당 granular entry, Native는 `@hjmds/react-native` entry를 쓴다.
66
+ 선택 콜백은 두 플랫폼 모두 `onValueChange`다. 위 골격의 번역·항목·값은 제품이 제공한다.
67
+
68
+ ## 플랫폼 차이
69
+
70
+ | 항목 | Web | Native |
71
+ | --- | --- | --- |
72
+ | 배치/테마 | Stack과 HjmProvider | Stack과 HjmNativeProvider |
73
+ | 큰 글자·좁은 폭 | 줄바꿈·단일 내용 | 같은 순서, OS 화면 검증은 별도 |
@@ -0,0 +1,86 @@
1
+ # 질감 비교
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: [EffectSurface 계약](../../effect-surface.md), `showcase/*/texture-comparison-preview.tsx`
9
+ - 스토리북: `배포/구성/정보 표시/질감 비교`
10
+
11
+ 승급: 2026-10-07 사용자 승인, [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md). Storybook 분류이며 제품 적용 증거는 별도다.
12
+
13
+ ## 언제 쓰나
14
+
15
+ 기존 반복 점 grain과 불규칙한 정적 noise를 같은 배경·강도로 비교할 때 쓴다.
16
+ 독립 NoiseTexture API를 추가하지 않고 기존 EffectSurface의 선택 레이어를 사용한다.
17
+ 일반 입력·데이터 패널은 불투명 Surface, 작업 진행·성공은 Progress·Result를 쓴다.
18
+
19
+ ## 구성 요소
20
+
21
+ | 컴포넌트 | 역할 | 지침 |
22
+ | --- | --- | --- |
23
+ | EffectSurface | 질감·테마·장식 수명주기 | [배경 효과](../components/effect-surface.md) |
24
+ | SegmentedControl | 두 영역의 강도를 함께 선택 | [단일 선택](../components/segmented-control.md) |
25
+ | TextField·Button·Text | 읽기·입력·행동·결과 확인 | [입력](../components/field.md) |
26
+
27
+ noise는 생성된64×64 PNG alpha mask이며 자체 런타임 픽셀 생성·외부 요청이 없다.
28
+ 기본 intensity0.22, 실험의 비교 강도0.6은 가독성 스트레스 조건이며 제품 기본 권장이 아니다.
29
+ seed는 고정 타일의 위상만 이동한다. 새 타일 픽셀을 생성하지 않는다.
30
+
31
+ ## 배치
32
+
33
+ ```text
34
+ [강도 선택: 두 영역에 함께 적용]
35
+ [grain: 제목 → 설명 → 입력 → 버튼]
36
+ [noise: 제목 → 설명 → 입력 → 버튼]
37
+ [선택 결과와 비교 한계]
38
+ ```
39
+
40
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
41
+ | --- | --- | --- | --- |
42
+ | 바깥 틀 | Stack | 화면 본문 세로 | gap lg20 |
43
+ | 비교 영역 | EffectSurface | grain 다음 noise | 내부 padding spacing.lg20, Stack gap md16 |
44
+ | 질감 | SVG mask | absolute 배경 | viewBox 밖64×64 CSS pixel/Native logical point 타일 반복 |
45
+ | 본문 | TextField·Button | 장식 앞 일반 레이아웃 | 기존 recipe |
46
+ | Native 화면 | ScreenLayout·KeyboardAvoiding | 본문 스크롤과 바깥 키보드 여백 | 긴 내용·큰 글자 접근용 |
47
+
48
+ ## 흐름과 상태
49
+
50
+ | 상태 | 모습 | 포커스·알림 |
51
+ | --- | --- | --- |
52
+ | 기본 | 두 질감과 기본22% 강도 | 입력·버튼 활성 |
53
+ | 진행 중 | 강도 선택을 두 질감에 함께 반영 | 입력 유지, 인위 대기 없음 |
54
+ | 실패 | 장식 host 실패는 기존 EffectSurface fallback | 제품 입력·행동은 유지 |
55
+
56
+ 1. 강도를 바꾸고 두 영역의 문장을 읽고 타이핑한다.
57
+ 2. 영역별 버튼을 눌러 결과 안내를 비교한다. 결과는 예제 내부 선택일 뿐 저장하지 않는다.
58
+
59
+ ## 코드 골격
60
+
61
+ ```tsx
62
+ // Web
63
+ import { EffectSurface } from "@hjmds/react/effect-surface";
64
+ <EffectSurface descriptor={{ layers: ["noise"], active: false }}><Content /></EffectSurface>
65
+ ```
66
+
67
+ ```tsx
68
+ // Native
69
+ import { EffectSurface } from "@hjmds/react-native/effect-surface";
70
+ <EffectSurface descriptor={{ layers: ["noise"], active: false }}><Content /></EffectSurface>
71
+ ```
72
+
73
+ ## 플랫폼 차이
74
+
75
+ | 항목 | Web | Native |
76
+ | --- | --- | --- |
77
+ | 장식 | SVG image·mask | react-native-svg Image·Mask |
78
+ | peer | 없음 | 기존 react-native-svg15.15.5 필요 |
79
+ | 동작 | 가시성·document visibility | visible·AppState |
80
+
81
+ Native FeTurbulence는 미구현이라 공유 PNG mask를 택했다. 새 GPU peer나 Native 무표시
82
+ fallback을 동일 구현으로 세지 않는다. noise는 theme.text 색의 alpha mask다. 임의 브랜드/강도에서
83
+ 대비를 보장하지 않는다. 기본은 정적이며 active를 켜면 기존 모션 감소·숨김 수명주기를 유지한다.
84
+ Web/RN rasterization·비율 차이로 픽셀 일치를 보장하지 않는다. 원본 SVG Perlin noise와 달리
85
+ 자체 생성한6 octave value noise이며 원본 자산을 복사하지 않았다. 실제 기기 표시·비용·VoiceOver
86
+ 검증은 따로 한다.
@@ -0,0 +1,80 @@
1
+ # 선택과 오류 복구
2
+
3
+ - 단계: 구성
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 1.14.0
7
+ - 검토일: 2026-10-07
8
+ - 근거: 공개 API를 사용하는 `showcase/*/reference-adoption-previews.tsx`
9
+ - 스토리북: `배포/구성/피드백과 복구/선택과 오류 복구`
10
+
11
+ 2026-10-07 사용자 승인으로 승급. [검토 결과](../../../../../docs/qa/2026-10-07-experiment-promotion-release.md).
12
+
13
+ ## 언제 쓰나
14
+
15
+ 파일 선택과 전송 상태의 취소·재시도를 연결할 때 쓴다. 별도 신규 wrapper API가 아니라 아래 공개 컴포넌트의 조합 규격이다.
16
+
17
+ ## 구성 요소
18
+
19
+ | 컴포넌트 | 역할 | 지침 |
20
+ | --- | --- | --- |
21
+ | FilePicker · UploadItem · Button | 입력·표현·행동의 역할 분리 | [입력/동작](../components/file-picker.md), [상태/전환](../components/upload-item.md) |
22
+
23
+ ## 배치
24
+
25
+ ```text
26
+ [제목 또는 현재 입력]
27
+ [상태/내용 영역]
28
+ [관련 행동과 결과]
29
+ ```
30
+
31
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
32
+ | --- | --- | --- | --- |
33
+ | 바깥 틀 | Stack | 화면의 본문 흐름 | gap md=16px, 전체 폭 |
34
+ | 내용 | 해당 공개 API | 입력과 행동 사이 | 자체 recipe 크기, 줄바꿈 허용 |
35
+ | 행동 | Button/기존 컨트롤 | 관련 항목 바로 뒤 | inline일 때 wrap, md=16px |
36
+
37
+ ## 흐름과 상태
38
+
39
+ 1. 선택→중복 제거→전송→성공 또는 실패→취소/재시도
40
+ 2. 서버 응답·파일 권한·문구·브랜드는 제품이 전달한다. Showcase의 예제 응답과 고정 데이터를 가져오지 않는다.
41
+
42
+ | 상태 | 모습 | 포커스·알림 |
43
+ | --- | --- | --- |
44
+ | 기본 | 입력과 사용 가능한 행동 | 조작 이름은 제품 언어 |
45
+ | 진행 중 | 실제 작업 상태에 맞는 loading 또는 열린 도구 | 중복 요청 차단, 입력 유지 |
46
+ | 실패 | 문구와 가능한 재시도 | 원래 입력/파일 유지, 결과 안내 |
47
+
48
+ ## 코드 골격
49
+
50
+ ```tsx
51
+ // Web
52
+ <UploadItem descriptor={item} labels={labels} onCancel={cancel} onRetry={retry} />
53
+ ```
54
+
55
+ ```tsx
56
+ // Native
57
+ <UploadItem descriptor={item} labels={labels} onCancel={cancel} onRetry={retry} />
58
+ ```
59
+
60
+ Web은 `@hjmds/react`의 해당 granular entry, Native는 `@hjmds/react-native` entry를 쓴다.
61
+ Button의 실행 콜백은 Web `onClick`, Native `onPress`로 연결한다. 위 골격의 도메인 함수·변수는 제품이 제공한다.
62
+
63
+ ## 플랫폼 차이
64
+
65
+ | 항목 | Web | Native |
66
+ | --- | --- | --- |
67
+ | 배치/테마 | Stack과 HjmProvider | Stack과 HjmNativeProvider |
68
+ | 큰 글자·좁은 폭 | 줄바꿈·단일 내용 | 같은 순서, OS 화면 검증은 별도 |
69
+
70
+ ### 예제의 제한과 초점 복구
71
+
72
+ 2026-10-07 실제 복구 흐름에서 상태 버튼이 사라질 때 Web 초점이 body로 빠졌다.
73
+ 예제는 상태 변경 전에 해당 파일의 이름 있는 영역으로 초점을 옮기고, 제거 후에는 예제 추가
74
+ 버튼으로 복귀한다. 이후 Tab으로 취소·재시도 등 현재 가능한 행동을 선택한다.
75
+ 제품도 비동기 상태 전환으로 사라지는 행동의 초점 목적지를 정한다.
76
+
77
+ 양쪽 예제는 이미지3개·파일당5MB 제한을 선언한다. Web은 실제 file chooser를 사용하며,
78
+ Native onPick은 예제 후보를 반환하는 fixture다. Native 시스템 사진 선택기 검증으로 세지 않는다.
79
+ 서버 비전송 안내는 성공·거부 상태와 별도로 항상 표시한다. 실제 제품은 서버 확정·취소 시
80
+ 늦게 도착하는 응답·전송 ticket의 소유권을 제품 업로드 계층에 연결해야 한다.