melta-app 0.1.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 (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/THIRD_PARTY_LICENSES.md +198 -0
  4. package/lib/module/components/ActionSheet.js +87 -0
  5. package/lib/module/components/ActionSheet.js.map +1 -0
  6. package/lib/module/components/Alert.js +93 -0
  7. package/lib/module/components/Alert.js.map +1 -0
  8. package/lib/module/components/Avatar.js +93 -0
  9. package/lib/module/components/Avatar.js.map +1 -0
  10. package/lib/module/components/BottomSheet.js +69 -0
  11. package/lib/module/components/BottomSheet.js.map +1 -0
  12. package/lib/module/components/Card.js +93 -0
  13. package/lib/module/components/Card.js.map +1 -0
  14. package/lib/module/components/Checkbox.js +115 -0
  15. package/lib/module/components/Checkbox.js.map +1 -0
  16. package/lib/module/components/EmptyState.js +70 -0
  17. package/lib/module/components/EmptyState.js.map +1 -0
  18. package/lib/module/components/Header.js +53 -0
  19. package/lib/module/components/Header.js.map +1 -0
  20. package/lib/module/components/Image.js +67 -0
  21. package/lib/module/components/Image.js.map +1 -0
  22. package/lib/module/components/Modal.js +95 -0
  23. package/lib/module/components/Modal.js.map +1 -0
  24. package/lib/module/components/Progress.js +113 -0
  25. package/lib/module/components/Progress.js.map +1 -0
  26. package/lib/module/components/Radio.js +134 -0
  27. package/lib/module/components/Radio.js.map +1 -0
  28. package/lib/module/components/Screen.js +57 -0
  29. package/lib/module/components/Screen.js.map +1 -0
  30. package/lib/module/components/Skeleton.js +169 -0
  31. package/lib/module/components/Skeleton.js.map +1 -0
  32. package/lib/module/components/Surface.js +49 -0
  33. package/lib/module/components/Surface.js.map +1 -0
  34. package/lib/module/components/TextField.js +84 -0
  35. package/lib/module/components/TextField.js.map +1 -0
  36. package/lib/module/components/Toast.js +105 -0
  37. package/lib/module/components/Toast.js.map +1 -0
  38. package/lib/module/components/Toggle.js +77 -0
  39. package/lib/module/components/Toggle.js.map +1 -0
  40. package/lib/module/components/action-sheet.styles.js +70 -0
  41. package/lib/module/components/action-sheet.styles.js.map +1 -0
  42. package/lib/module/components/alert.styles.js +80 -0
  43. package/lib/module/components/alert.styles.js.map +1 -0
  44. package/lib/module/components/avatar.styles.js +94 -0
  45. package/lib/module/components/avatar.styles.js.map +1 -0
  46. package/lib/module/components/bottom-sheet.styles.js +55 -0
  47. package/lib/module/components/bottom-sheet.styles.js.map +1 -0
  48. package/lib/module/components/card.styles.js +50 -0
  49. package/lib/module/components/card.styles.js.map +1 -0
  50. package/lib/module/components/checkbox.styles.js +55 -0
  51. package/lib/module/components/checkbox.styles.js.map +1 -0
  52. package/lib/module/components/empty-state.styles.js +60 -0
  53. package/lib/module/components/empty-state.styles.js.map +1 -0
  54. package/lib/module/components/header.styles.js +33 -0
  55. package/lib/module/components/header.styles.js.map +1 -0
  56. package/lib/module/components/image.styles.js +37 -0
  57. package/lib/module/components/image.styles.js.map +1 -0
  58. package/lib/module/components/index.js +26 -0
  59. package/lib/module/components/index.js.map +1 -0
  60. package/lib/module/components/modal.styles.js +68 -0
  61. package/lib/module/components/modal.styles.js.map +1 -0
  62. package/lib/module/components/progress.styles.js +55 -0
  63. package/lib/module/components/progress.styles.js.map +1 -0
  64. package/lib/module/components/radio.styles.js +96 -0
  65. package/lib/module/components/radio.styles.js.map +1 -0
  66. package/lib/module/components/screen.styles.js +38 -0
  67. package/lib/module/components/screen.styles.js.map +1 -0
  68. package/lib/module/components/skeleton.styles.js +114 -0
  69. package/lib/module/components/skeleton.styles.js.map +1 -0
  70. package/lib/module/components/surface.styles.js +37 -0
  71. package/lib/module/components/surface.styles.js.map +1 -0
  72. package/lib/module/components/textfield.styles.js +121 -0
  73. package/lib/module/components/textfield.styles.js.map +1 -0
  74. package/lib/module/components/toast.styles.js +81 -0
  75. package/lib/module/components/toast.styles.js.map +1 -0
  76. package/lib/module/components/toggle.styles.js +69 -0
  77. package/lib/module/components/toggle.styles.js.map +1 -0
  78. package/lib/module/contracts/contract-types.js +192 -0
  79. package/lib/module/contracts/contract-types.js.map +1 -0
  80. package/lib/module/icons/Icon.js +57 -0
  81. package/lib/module/icons/Icon.js.map +1 -0
  82. package/lib/module/icons/glyphs.js +257 -0
  83. package/lib/module/icons/glyphs.js.map +1 -0
  84. package/lib/module/icons/icon.styles.js +28 -0
  85. package/lib/module/icons/icon.styles.js.map +1 -0
  86. package/lib/module/icons/index.js +14 -0
  87. package/lib/module/icons/index.js.map +1 -0
  88. package/lib/module/index.js +20 -0
  89. package/lib/module/index.js.map +1 -0
  90. package/lib/module/package.json +1 -0
  91. package/lib/module/primitives/Button.js +105 -0
  92. package/lib/module/primitives/Button.js.map +1 -0
  93. package/lib/module/primitives/Metric.js +68 -0
  94. package/lib/module/primitives/Metric.js.map +1 -0
  95. package/lib/module/primitives/Row.js +43 -0
  96. package/lib/module/primitives/Row.js.map +1 -0
  97. package/lib/module/primitives/Stack.js +39 -0
  98. package/lib/module/primitives/Stack.js.map +1 -0
  99. package/lib/module/primitives/Tag.js +160 -0
  100. package/lib/module/primitives/Tag.js.map +1 -0
  101. package/lib/module/primitives/Text.js +54 -0
  102. package/lib/module/primitives/Text.js.map +1 -0
  103. package/lib/module/primitives/_internal/focus-ring.js +57 -0
  104. package/lib/module/primitives/_internal/focus-ring.js.map +1 -0
  105. package/lib/module/primitives/button.styles.js +88 -0
  106. package/lib/module/primitives/button.styles.js.map +1 -0
  107. package/lib/module/primitives/index.js +11 -0
  108. package/lib/module/primitives/index.js.map +1 -0
  109. package/lib/module/primitives/metric.styles.js +54 -0
  110. package/lib/module/primitives/metric.styles.js.map +1 -0
  111. package/lib/module/primitives/row.styles.js +53 -0
  112. package/lib/module/primitives/row.styles.js.map +1 -0
  113. package/lib/module/primitives/stack.styles.js +37 -0
  114. package/lib/module/primitives/stack.styles.js.map +1 -0
  115. package/lib/module/primitives/tag.styles.js +56 -0
  116. package/lib/module/primitives/tag.styles.js.map +1 -0
  117. package/lib/module/primitives/text.styles.js +33 -0
  118. package/lib/module/primitives/text.styles.js.map +1 -0
  119. package/lib/module/theme/ThemeProvider.js +41 -0
  120. package/lib/module/theme/ThemeProvider.js.map +1 -0
  121. package/lib/module/theme/index.js +19 -0
  122. package/lib/module/theme/index.js.map +1 -0
  123. package/lib/module/theme/native-theme.js +201 -0
  124. package/lib/module/theme/native-theme.js.map +1 -0
  125. package/lib/module/theme/types.js +4 -0
  126. package/lib/module/theme/types.js.map +1 -0
  127. package/lib/typescript/package.json +1 -0
  128. package/lib/typescript/src/components/ActionSheet.d.ts +45 -0
  129. package/lib/typescript/src/components/ActionSheet.d.ts.map +1 -0
  130. package/lib/typescript/src/components/Alert.d.ts +43 -0
  131. package/lib/typescript/src/components/Alert.d.ts.map +1 -0
  132. package/lib/typescript/src/components/Avatar.d.ts +45 -0
  133. package/lib/typescript/src/components/Avatar.d.ts.map +1 -0
  134. package/lib/typescript/src/components/BottomSheet.d.ts +36 -0
  135. package/lib/typescript/src/components/BottomSheet.d.ts.map +1 -0
  136. package/lib/typescript/src/components/Card.d.ts +46 -0
  137. package/lib/typescript/src/components/Card.d.ts.map +1 -0
  138. package/lib/typescript/src/components/Checkbox.d.ts +40 -0
  139. package/lib/typescript/src/components/Checkbox.d.ts.map +1 -0
  140. package/lib/typescript/src/components/EmptyState.d.ts +36 -0
  141. package/lib/typescript/src/components/EmptyState.d.ts.map +1 -0
  142. package/lib/typescript/src/components/Header.d.ts +34 -0
  143. package/lib/typescript/src/components/Header.d.ts.map +1 -0
  144. package/lib/typescript/src/components/Image.d.ts +41 -0
  145. package/lib/typescript/src/components/Image.d.ts.map +1 -0
  146. package/lib/typescript/src/components/Modal.d.ts +46 -0
  147. package/lib/typescript/src/components/Modal.d.ts.map +1 -0
  148. package/lib/typescript/src/components/Progress.d.ts +39 -0
  149. package/lib/typescript/src/components/Progress.d.ts.map +1 -0
  150. package/lib/typescript/src/components/Radio.d.ts +52 -0
  151. package/lib/typescript/src/components/Radio.d.ts.map +1 -0
  152. package/lib/typescript/src/components/Screen.d.ts +42 -0
  153. package/lib/typescript/src/components/Screen.d.ts.map +1 -0
  154. package/lib/typescript/src/components/Skeleton.d.ts +37 -0
  155. package/lib/typescript/src/components/Skeleton.d.ts.map +1 -0
  156. package/lib/typescript/src/components/Surface.d.ts +31 -0
  157. package/lib/typescript/src/components/Surface.d.ts.map +1 -0
  158. package/lib/typescript/src/components/TextField.d.ts +45 -0
  159. package/lib/typescript/src/components/TextField.d.ts.map +1 -0
  160. package/lib/typescript/src/components/Toast.d.ts +45 -0
  161. package/lib/typescript/src/components/Toast.d.ts.map +1 -0
  162. package/lib/typescript/src/components/Toggle.d.ts +39 -0
  163. package/lib/typescript/src/components/Toggle.d.ts.map +1 -0
  164. package/lib/typescript/src/components/action-sheet.styles.d.ts +65 -0
  165. package/lib/typescript/src/components/action-sheet.styles.d.ts.map +1 -0
  166. package/lib/typescript/src/components/alert.styles.d.ts +61 -0
  167. package/lib/typescript/src/components/alert.styles.d.ts.map +1 -0
  168. package/lib/typescript/src/components/avatar.styles.d.ts +60 -0
  169. package/lib/typescript/src/components/avatar.styles.d.ts.map +1 -0
  170. package/lib/typescript/src/components/bottom-sheet.styles.d.ts +50 -0
  171. package/lib/typescript/src/components/bottom-sheet.styles.d.ts.map +1 -0
  172. package/lib/typescript/src/components/card.styles.d.ts +27 -0
  173. package/lib/typescript/src/components/card.styles.d.ts.map +1 -0
  174. package/lib/typescript/src/components/checkbox.styles.d.ts +55 -0
  175. package/lib/typescript/src/components/checkbox.styles.d.ts.map +1 -0
  176. package/lib/typescript/src/components/empty-state.styles.d.ts +58 -0
  177. package/lib/typescript/src/components/empty-state.styles.d.ts.map +1 -0
  178. package/lib/typescript/src/components/header.styles.d.ts +30 -0
  179. package/lib/typescript/src/components/header.styles.d.ts.map +1 -0
  180. package/lib/typescript/src/components/image.styles.d.ts +28 -0
  181. package/lib/typescript/src/components/image.styles.d.ts.map +1 -0
  182. package/lib/typescript/src/components/index.d.ts +23 -0
  183. package/lib/typescript/src/components/index.d.ts.map +1 -0
  184. package/lib/typescript/src/components/modal.styles.d.ts +66 -0
  185. package/lib/typescript/src/components/modal.styles.d.ts.map +1 -0
  186. package/lib/typescript/src/components/progress.styles.d.ts +44 -0
  187. package/lib/typescript/src/components/progress.styles.d.ts.map +1 -0
  188. package/lib/typescript/src/components/radio.styles.d.ts +91 -0
  189. package/lib/typescript/src/components/radio.styles.d.ts.map +1 -0
  190. package/lib/typescript/src/components/screen.styles.d.ts +34 -0
  191. package/lib/typescript/src/components/screen.styles.d.ts.map +1 -0
  192. package/lib/typescript/src/components/skeleton.styles.d.ts +105 -0
  193. package/lib/typescript/src/components/skeleton.styles.d.ts.map +1 -0
  194. package/lib/typescript/src/components/surface.styles.d.ts +31 -0
  195. package/lib/typescript/src/components/surface.styles.d.ts.map +1 -0
  196. package/lib/typescript/src/components/textfield.styles.d.ts +75 -0
  197. package/lib/typescript/src/components/textfield.styles.d.ts.map +1 -0
  198. package/lib/typescript/src/components/toast.styles.d.ts +62 -0
  199. package/lib/typescript/src/components/toast.styles.d.ts.map +1 -0
  200. package/lib/typescript/src/components/toggle.styles.d.ts +53 -0
  201. package/lib/typescript/src/components/toggle.styles.d.ts.map +1 -0
  202. package/lib/typescript/src/contracts/contract-types.d.ts +193 -0
  203. package/lib/typescript/src/contracts/contract-types.d.ts.map +1 -0
  204. package/lib/typescript/src/icons/Icon.d.ts +40 -0
  205. package/lib/typescript/src/icons/Icon.d.ts.map +1 -0
  206. package/lib/typescript/src/icons/glyphs.d.ts +259 -0
  207. package/lib/typescript/src/icons/glyphs.d.ts.map +1 -0
  208. package/lib/typescript/src/icons/icon.styles.d.ts +20 -0
  209. package/lib/typescript/src/icons/icon.styles.d.ts.map +1 -0
  210. package/lib/typescript/src/icons/index.d.ts +12 -0
  211. package/lib/typescript/src/icons/index.d.ts.map +1 -0
  212. package/lib/typescript/src/index.d.ts +12 -0
  213. package/lib/typescript/src/index.d.ts.map +1 -0
  214. package/lib/typescript/src/primitives/Button.d.ts +48 -0
  215. package/lib/typescript/src/primitives/Button.d.ts.map +1 -0
  216. package/lib/typescript/src/primitives/Metric.d.ts +38 -0
  217. package/lib/typescript/src/primitives/Metric.d.ts.map +1 -0
  218. package/lib/typescript/src/primitives/Row.d.ts +37 -0
  219. package/lib/typescript/src/primitives/Row.d.ts.map +1 -0
  220. package/lib/typescript/src/primitives/Stack.d.ts +33 -0
  221. package/lib/typescript/src/primitives/Stack.d.ts.map +1 -0
  222. package/lib/typescript/src/primitives/Tag.d.ts +43 -0
  223. package/lib/typescript/src/primitives/Tag.d.ts.map +1 -0
  224. package/lib/typescript/src/primitives/Text.d.ts +45 -0
  225. package/lib/typescript/src/primitives/Text.d.ts.map +1 -0
  226. package/lib/typescript/src/primitives/_internal/focus-ring.d.ts +31 -0
  227. package/lib/typescript/src/primitives/_internal/focus-ring.d.ts.map +1 -0
  228. package/lib/typescript/src/primitives/button.styles.d.ts +29 -0
  229. package/lib/typescript/src/primitives/button.styles.d.ts.map +1 -0
  230. package/lib/typescript/src/primitives/index.d.ts +8 -0
  231. package/lib/typescript/src/primitives/index.d.ts.map +1 -0
  232. package/lib/typescript/src/primitives/metric.styles.d.ts +43 -0
  233. package/lib/typescript/src/primitives/metric.styles.d.ts.map +1 -0
  234. package/lib/typescript/src/primitives/row.styles.d.ts +50 -0
  235. package/lib/typescript/src/primitives/row.styles.d.ts.map +1 -0
  236. package/lib/typescript/src/primitives/stack.styles.d.ts +34 -0
  237. package/lib/typescript/src/primitives/stack.styles.d.ts.map +1 -0
  238. package/lib/typescript/src/primitives/tag.styles.d.ts +32 -0
  239. package/lib/typescript/src/primitives/tag.styles.d.ts.map +1 -0
  240. package/lib/typescript/src/primitives/text.styles.d.ts +26 -0
  241. package/lib/typescript/src/primitives/text.styles.d.ts.map +1 -0
  242. package/lib/typescript/src/theme/ThemeProvider.d.ts +32 -0
  243. package/lib/typescript/src/theme/ThemeProvider.d.ts.map +1 -0
  244. package/lib/typescript/src/theme/index.d.ts +15 -0
  245. package/lib/typescript/src/theme/index.d.ts.map +1 -0
  246. package/lib/typescript/src/theme/native-theme.d.ts +3 -0
  247. package/lib/typescript/src/theme/native-theme.d.ts.map +1 -0
  248. package/lib/typescript/src/theme/types.d.ts +121 -0
  249. package/lib/typescript/src/theme/types.d.ts.map +1 -0
  250. package/package.json +120 -0
  251. package/src/components/ActionSheet.tsx +112 -0
  252. package/src/components/Alert.tsx +84 -0
  253. package/src/components/Avatar.tsx +110 -0
  254. package/src/components/BottomSheet.tsx +81 -0
  255. package/src/components/Card.tsx +102 -0
  256. package/src/components/Checkbox.tsx +107 -0
  257. package/src/components/EmptyState.tsx +64 -0
  258. package/src/components/Header.tsx +51 -0
  259. package/src/components/Image.tsx +89 -0
  260. package/src/components/Modal.tsx +113 -0
  261. package/src/components/Progress.tsx +124 -0
  262. package/src/components/Radio.tsx +151 -0
  263. package/src/components/Screen.tsx +62 -0
  264. package/src/components/Skeleton.tsx +154 -0
  265. package/src/components/Surface.tsx +37 -0
  266. package/src/components/TextField.tsx +102 -0
  267. package/src/components/Toast.tsx +103 -0
  268. package/src/components/Toggle.tsx +94 -0
  269. package/src/components/action-sheet.styles.ts +102 -0
  270. package/src/components/alert.styles.ts +115 -0
  271. package/src/components/avatar.styles.ts +110 -0
  272. package/src/components/bottom-sheet.styles.ts +78 -0
  273. package/src/components/card.styles.ts +56 -0
  274. package/src/components/checkbox.styles.ts +83 -0
  275. package/src/components/empty-state.styles.ts +98 -0
  276. package/src/components/header.styles.ts +44 -0
  277. package/src/components/image.styles.ts +36 -0
  278. package/src/components/index.ts +23 -0
  279. package/src/components/modal.styles.ts +106 -0
  280. package/src/components/progress.styles.ts +74 -0
  281. package/src/components/radio.styles.ts +132 -0
  282. package/src/components/screen.styles.ts +44 -0
  283. package/src/components/skeleton.styles.ts +155 -0
  284. package/src/components/surface.styles.ts +50 -0
  285. package/src/components/textfield.styles.ts +135 -0
  286. package/src/components/toast.styles.ts +115 -0
  287. package/src/components/toggle.styles.ts +73 -0
  288. package/src/contracts/contract-types.ts +199 -0
  289. package/src/icons/Icon.tsx +65 -0
  290. package/src/icons/glyphs.ts +54 -0
  291. package/src/icons/icon.styles.ts +37 -0
  292. package/src/icons/index.ts +12 -0
  293. package/src/index.ts +24 -0
  294. package/src/primitives/Button.tsx +114 -0
  295. package/src/primitives/Metric.tsx +67 -0
  296. package/src/primitives/Row.tsx +54 -0
  297. package/src/primitives/Stack.tsx +42 -0
  298. package/src/primitives/Tag.tsx +126 -0
  299. package/src/primitives/Text.tsx +71 -0
  300. package/src/primitives/_internal/focus-ring.tsx +59 -0
  301. package/src/primitives/button.styles.ts +69 -0
  302. package/src/primitives/index.ts +8 -0
  303. package/src/primitives/metric.styles.ts +75 -0
  304. package/src/primitives/row.styles.ts +66 -0
  305. package/src/primitives/stack.styles.ts +47 -0
  306. package/src/primitives/tag.styles.ts +65 -0
  307. package/src/primitives/text.styles.ts +42 -0
  308. package/src/theme/ThemeProvider.tsx +60 -0
  309. package/src/theme/index.ts +18 -0
  310. package/src/theme/native-theme.ts +215 -0
  311. package/src/theme/types.ts +137 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Radio — 単一選択フォームコントロール。contract: radio。
3
+ *
4
+ * - 契約どおり fieldset 相当の RadioGroup として実装する(コンポーネント名は Radio、
5
+ * recipe description 参照)。groupLabel(= legend 相当の label prop)必須
6
+ * (FORM_FIELDSET_LEGEND_REQUIRED を props 型で強制)。
7
+ * - variant はレイアウト: vertical(標準)/ horizontal(wrap する横並び)/ card-style
8
+ * (各 option を枠付きカードにする。web の grid 2col は RN では縦積みに読み替え)。
9
+ * - RN にネイティブ radio が無いため各 option は Pressable + 描画(circle + 内側 dot)。
10
+ * - states(contract → prop の写像): selected/unselected → value との一致、
11
+ * error → error?: string(circle の borderColor 差分 + errorText 表示。selected と併用時は
12
+ * error を優先 = 契約 stateSpecs)、disabled → グループ全体 or option 個別(opacity 0.5 + 非活性)。
13
+ * focus は Phase1 非対応(recipe にも focus 差分なし)。
14
+ * - a11y: グループに accessibilityRole="radiogroup"、各 option に "radio" +
15
+ * accessibilityState={{ checked, disabled }}。
16
+ * - 色・寸法の決定は pure resolver(radio.styles.ts)に分離。
17
+ * recipes/app/radio.recipe.json との機械照合は scripts/lib/radio-conformance.test.ts が行う。
18
+ */
19
+
20
+ import { useMemo } from "react";
21
+ import { Pressable, View } from "react-native";
22
+ import { Text } from "../primitives/Text";
23
+ import { useTheme } from "../theme";
24
+ import { CONTRACTS } from "../contracts/contract-types";
25
+ import {
26
+ RADIO_SPEC,
27
+ resolveRadioCircleStyle,
28
+ resolveRadioGroupStyle,
29
+ type RadioVariant,
30
+ } from "./radio.styles";
31
+
32
+ export interface RadioOption {
33
+ label: string;
34
+ value: string;
35
+ /** label の下に出す補足(text-muted の小テキスト。card-style で使う想定)。 */
36
+ description?: string;
37
+ /** この option だけ操作不可にする。 */
38
+ disabled?: boolean;
39
+ }
40
+
41
+ export interface RadioProps {
42
+ /** グループラベル(fieldset の legend 相当。必須 = FORM_FIELDSET_LEGEND_REQUIRED)。 */
43
+ label: string;
44
+ options: RadioOption[];
45
+ /** 選択中の option の value(未選択は undefined)。 */
46
+ value: string | undefined;
47
+ onChange: (value: string) => void;
48
+ variant?: RadioVariant;
49
+ /** エラーメッセージ。指定時は circle が danger 枠になりメッセージを表示する。 */
50
+ error?: string;
51
+ /** グループ全体を操作不可にする。 */
52
+ disabled?: boolean;
53
+ testID?: string;
54
+ }
55
+
56
+ export function Radio({
57
+ label,
58
+ options,
59
+ value,
60
+ onChange,
61
+ variant = "vertical",
62
+ error,
63
+ disabled = false,
64
+ testID,
65
+ }: RadioProps) {
66
+ const { theme, mode } = useTheme();
67
+ const hasError = error != null && error !== "";
68
+ const group = useMemo(() => resolveRadioGroupStyle(theme, mode, variant), [theme, mode, variant]);
69
+ // circle は selected × error の 2 値。option ごとに再計算しないよう両方を先に解決しておく。
70
+ const circles = useMemo(
71
+ () => ({
72
+ unselected: resolveRadioCircleStyle(theme, mode, { selected: false, error: hasError }),
73
+ selected: resolveRadioCircleStyle(theme, mode, { selected: true, error: hasError }),
74
+ }),
75
+ [theme, mode, hasError],
76
+ );
77
+
78
+ return (
79
+ <View testID={testID} style={disabled ? { opacity: RADIO_SPEC.disabledOpacity } : undefined}>
80
+ {/* groupLabel(sm/medium/text-heading = recipe groupLabelStyle と同値)。 */}
81
+ <Text
82
+ variant="sm"
83
+ weight="medium"
84
+ color="text-heading"
85
+ style={{ marginBottom: group.groupLabelStyle.marginBottom }}
86
+ >
87
+ {label}
88
+ </Text>
89
+ <View accessibilityRole="radiogroup" style={group.containerStyle}>
90
+ {options.map((option) => {
91
+ const selected = option.value === value;
92
+ const optionDisabled = disabled || option.disabled === true;
93
+ return (
94
+ <Pressable
95
+ key={option.value}
96
+ onPress={() => onChange(option.value)}
97
+ disabled={optionDisabled}
98
+ accessibilityRole="radio"
99
+ accessibilityState={{ checked: selected, disabled: optionDisabled }}
100
+ accessibilityLabel={option.label}
101
+ style={[
102
+ variant === "card-style"
103
+ ? [group.cardStyle, selected ? group.cardSelectedStyle : null]
104
+ : null,
105
+ // グループ disabled は外側 View の opacity で表現済み。二重にしない。
106
+ option.disabled === true && !disabled
107
+ ? { opacity: RADIO_SPEC.disabledOpacity }
108
+ : null,
109
+ ]}
110
+ >
111
+ <View
112
+ style={{
113
+ flexDirection: "row",
114
+ // description 付きは複数行になるので circle を先頭行に寄せる。
115
+ alignItems: option.description != null ? "flex-start" : "center",
116
+ gap: group.optionGap,
117
+ }}
118
+ >
119
+ <View style={selected ? circles.selected : circles.unselected}>
120
+ {selected ? <View style={group.dotStyle} /> : null}
121
+ </View>
122
+ <View style={{ flexShrink: 1 }}>
123
+ <Text variant="base" color="text-default">
124
+ {option.label}
125
+ </Text>
126
+ {option.description != null ? (
127
+ <Text variant="sm" color="text-muted">
128
+ {option.description}
129
+ </Text>
130
+ ) : null}
131
+ </View>
132
+ </View>
133
+ </Pressable>
134
+ );
135
+ })}
136
+ </View>
137
+ {hasError ? (
138
+ // danger.text-light は SemanticColors 外なので style で上書き(Button の textColor と同じ扱い)。
139
+ <Text
140
+ variant="xs"
141
+ style={{ color: group.errorTextStyle.color, marginTop: group.errorTextStyle.marginTop }}
142
+ >
143
+ {error}
144
+ </Text>
145
+ ) : null}
146
+ </View>
147
+ );
148
+ }
149
+
150
+ // Phase 2 conformance test 用の contract メタ(§2 A-3)。
151
+ Radio.__contract = CONTRACTS.radio;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Screen — 画面骨格 primitive(SafeArea + bg-page + content padding)。
3
+ * contract: screen(app 先行定義、melta-contracts)。dogfood 不足-3(SafeAreaView + ScrollView +
4
+ * ヘッダーの毎画面手書き)の解消。
5
+ *
6
+ * - variant="scroll"(default)は content を ScrollView に、"fixed" は flex:1 の View に載せる。
7
+ * - header slot は scroll の外(SafeArea 直下)に固定する。
8
+ * - padding は spacing token キー限定 | "none"(default "4")。
9
+ * - 決定ロジックは pure resolver(screen.styles.ts)に分離 — recipe との機械照合対象。
10
+ *
11
+ * ⚠️ SafeArea は RN core の SafeAreaView(deprecated / iOS のみの最小対応)を意図的に使う。
12
+ * 依存ゼロ方針(P1)を安全域の精度より優先した判断。Android の edge-to-edge や notch 精度が
13
+ * 必要になったら react-native-safe-area-context への adapter 化を再検討する(optional peer の
14
+ * 前例は Icon × react-native-svg)。
15
+ */
16
+
17
+ import { useMemo, type ReactNode } from "react";
18
+ import { SafeAreaView, ScrollView, View, type StyleProp, type ViewStyle } from "react-native";
19
+ import { useTheme } from "../theme";
20
+ import { CONTRACTS, type VariantOf } from "../contracts/contract-types";
21
+ import { resolveScreenStyle, type ScreenPadding, type ScreenStyle } from "./screen.styles";
22
+
23
+ interface ScreenProps {
24
+ /** 画面の骨格(contract variant)。default "scroll"。 */
25
+ variant?: VariantOf<"screen">;
26
+ /** content の padding(spacing token キー限定 | "none")。default "4"。 */
27
+ padding?: ScreenPadding;
28
+ /** scroll 外(SafeArea 直下)に固定されるヘッダー slot(Header を差す想定)。 */
29
+ header?: ReactNode;
30
+ style?: StyleProp<ViewStyle>;
31
+ testID?: string;
32
+ children: ReactNode;
33
+ }
34
+
35
+ export function Screen({
36
+ variant = "scroll",
37
+ padding = "4",
38
+ header,
39
+ style,
40
+ testID,
41
+ children,
42
+ }: ScreenProps) {
43
+ const { theme, mode } = useTheme();
44
+
45
+ const { safeAreaStyle, contentStyle } = useMemo<ScreenStyle>(
46
+ () => resolveScreenStyle(theme, mode, { variant, padding }),
47
+ [theme, mode, variant, padding],
48
+ );
49
+
50
+ return (
51
+ <SafeAreaView style={[safeAreaStyle, style]} testID={testID}>
52
+ {header}
53
+ {variant === "scroll" ? (
54
+ <ScrollView contentContainerStyle={contentStyle}>{children}</ScrollView>
55
+ ) : (
56
+ <View style={contentStyle}>{children}</View>
57
+ )}
58
+ </SafeAreaView>
59
+ );
60
+ }
61
+
62
+ Screen.__contract = CONTRACTS.screen;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Skeleton — ローディングプレースホルダー(設計書 §1)。contract: skeleton。
3
+ *
4
+ * - variant: text(行バー)/ circle(アバター)/ card(Surface 土台 + 中にバー)。
5
+ * - a11y: accessibilityState={{busy:true}} + accessibilityLabel(default "読み込み中")。
6
+ * - 色・寸法の決定は pure resolver(skeleton.styles.ts)に分離。card の Surface props も
7
+ * CARD_SURFACE_SPEC(同ファイル)を SSOT として共有し、
8
+ * recipes/app/skeleton.recipe.json との機械照合は scripts/lib/skeleton-conformance.test.ts が行う。
9
+ * - bg: text/circle は border-default。card は Surface 土台(bg-surface+border+elevation.sm)
10
+ * の上に border-default のバーを置く(contract に忠実、§1 Agent m2)。
11
+ * - pulse の reduce-motion 順序ハザード(§1): Animated.Value を opacityFrom(= 非アニメ状態)で
12
+ * 初期化し、effect 内で AccessibilityInfo.isReduceMotionEnabled() を await した後にのみ loop 開始する。
13
+ * 同期的に loop を start すると reduce-motion ON でも一瞬光るため。ON 時は pulse 完全停止 + busy 維持。
14
+ */
15
+
16
+ import { useEffect, useMemo, useRef } from "react";
17
+ import {
18
+ AccessibilityInfo,
19
+ Animated,
20
+ View,
21
+ type DimensionValue,
22
+ type StyleProp,
23
+ type ViewStyle,
24
+ } from "react-native";
25
+ import { Surface } from "./Surface";
26
+ import { useTheme } from "../theme";
27
+ import { CONTRACTS } from "../contracts/contract-types";
28
+ import {
29
+ CARD_SURFACE_SPEC,
30
+ PULSE_OPACITY,
31
+ SKELETON_METRICS,
32
+ resolveSkeletonStates,
33
+ resolveSkeletonVariants,
34
+ type SkeletonVariant,
35
+ } from "./skeleton.styles";
36
+
37
+ interface SkeletonProps {
38
+ variant?: SkeletonVariant;
39
+ width?: DimensionValue;
40
+ /** text variant の行数。 */
41
+ lines?: number;
42
+ accessibilityLabel?: string;
43
+ style?: StyleProp<ViewStyle>;
44
+ testID?: string;
45
+ }
46
+
47
+ export function Skeleton({
48
+ variant = "text",
49
+ width = "100%",
50
+ lines = 1,
51
+ accessibilityLabel = "読み込み中",
52
+ style,
53
+ testID,
54
+ }: SkeletonProps) {
55
+ const { theme, mode } = useTheme();
56
+ const styles = useMemo(() => resolveSkeletonVariants(theme, mode), [theme, mode]);
57
+ // 初期値 opacityFrom = 非アニメ(reduce-motion でも光らない)。reduce-motion OFF を確認後にのみ loop で揺らす。
58
+ const opacity = useRef(new Animated.Value(PULSE_OPACITY.from)).current;
59
+
60
+ useEffect(() => {
61
+ let cancelled = false;
62
+ let loop: Animated.CompositeAnimation | null = null;
63
+ // pulse の値(opacity 振れ幅 / duration)は resolver 側が SSOT(recipe states.loading と照合される)。
64
+ const anim = resolveSkeletonStates(theme).loading.animation;
65
+ AccessibilityInfo.isReduceMotionEnabled().then((reduce) => {
66
+ if (cancelled || reduce) return;
67
+ loop = Animated.loop(
68
+ Animated.sequence([
69
+ Animated.timing(opacity, {
70
+ toValue: anim.opacityTo,
71
+ duration: anim.duration,
72
+ useNativeDriver: true,
73
+ }),
74
+ Animated.timing(opacity, {
75
+ toValue: anim.opacityFrom,
76
+ duration: anim.duration,
77
+ useNativeDriver: true,
78
+ }),
79
+ ]),
80
+ );
81
+ loop.start();
82
+ });
83
+ return () => {
84
+ cancelled = true;
85
+ loop?.stop();
86
+ };
87
+ }, [opacity, theme]);
88
+
89
+ // contract a11y role は web の "status"。RN の accessibilityRole に status は無いため、
90
+ // busy state + label で status 相当を表現する(§2 web→RN mapping、skeleton は busy で扱う)。
91
+ const a11y = {
92
+ accessible: true,
93
+ accessibilityState: { busy: true },
94
+ accessibilityLabel,
95
+ };
96
+
97
+ if (variant === "circle") {
98
+ return (
99
+ <Animated.View {...a11y} testID={testID} style={[styles.circle.style, { opacity }, style]} />
100
+ );
101
+ }
102
+
103
+ if (variant === "card") {
104
+ const { barStyle } = styles.card;
105
+ const { borderWidth, borderColor } = styles.card.style;
106
+ return (
107
+ <Surface
108
+ {...CARD_SURFACE_SPEC}
109
+ // contract skeleton.card は border-default を要求(Surface は border を出さないので Skeleton で付与)。
110
+ style={[{ borderWidth, borderColor }, style]}
111
+ testID={testID}
112
+ >
113
+ <Animated.View {...a11y} style={{ opacity, gap: theme.spacing["3"] }}>
114
+ <View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing["3"] }}>
115
+ <View
116
+ style={{
117
+ width: SKELETON_METRICS.circleSize,
118
+ height: SKELETON_METRICS.circleSize,
119
+ borderRadius: theme.radius.full,
120
+ backgroundColor: barStyle.backgroundColor,
121
+ }}
122
+ />
123
+ <View style={{ flex: 1, gap: theme.spacing["2"] }}>
124
+ <View style={[barStyle, { width: "60%" }]} />
125
+ <View style={[barStyle, { width: "40%" }]} />
126
+ </View>
127
+ </View>
128
+ <View style={barStyle} />
129
+ <View style={[barStyle, { width: "80%" }]} />
130
+ </Animated.View>
131
+ </Surface>
132
+ );
133
+ }
134
+
135
+ // text(行バー): recipe は 1 slot(style)に行バー + 行間 gap を併記 → gap はコンテナへ分離。
136
+ const { gap, ...textBar } = styles.text.style;
137
+ return (
138
+ <Animated.View {...a11y} testID={testID} style={[{ gap, opacity }, style]}>
139
+ {Array.from({ length: lines }, (_unused, i) => (
140
+ <View
141
+ key={i}
142
+ style={[
143
+ textBar,
144
+ // 最終行だけ短く(自然な見た目)。
145
+ { width: lines > 1 && i === lines - 1 ? "70%" : width },
146
+ ]}
147
+ />
148
+ ))}
149
+ </Animated.View>
150
+ );
151
+ }
152
+
153
+ // Phase 2 conformance test 用の contract メタ(§2 A-3)。
154
+ Skeleton.__contract = CONTRACTS.skeleton;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Surface — Card / Skeleton / EmptyState の共通土台となる「箱」(設計書 §1)。
3
+ * contract: surface(app 先行定義)。
4
+ *
5
+ * - 公開 P1 で正式 export に昇格(契約 surface.contract.json + recipe を持つ implemented のため)。
6
+ * セマンティクスを持つ箱が必要なら Card を優先し、Surface は素の土台が要る時だけ使う。
7
+ * - elevation[key] は iOS shadow* + Android elevation を 1 ViewStyle に同居させた値なので
8
+ * そのまま spread すれば両 OS に効く(types.ts ElevationStyle)。
9
+ * - 色・寸法の決定は pure resolver(surface.styles.ts)に分離。デフォルト値
10
+ * (bg=bg-surface / radius=lg / elevation=none)も resolver 側が持ち、
11
+ * recipes/app/surface.recipe.json との機械照合は scripts/lib/surface-conformance.test.ts が行う。
12
+ */
13
+
14
+ import type { ReactNode } from "react";
15
+ import { View, type StyleProp, type ViewStyle } from "react-native";
16
+ import { useTheme } from "../theme";
17
+ import { CONTRACTS } from "../contracts/contract-types";
18
+ import { resolveSurfaceStyle, type SurfaceStyleProps } from "./surface.styles";
19
+
20
+ export interface SurfaceProps extends SurfaceStyleProps {
21
+ style?: StyleProp<ViewStyle>;
22
+ testID?: string;
23
+ children?: ReactNode;
24
+ }
25
+
26
+ export function Surface({ bg, radius, elevation, padding, style, testID, children }: SurfaceProps) {
27
+ const { theme, mode } = useTheme();
28
+ const resolved = resolveSurfaceStyle(theme, mode, { bg, radius, elevation, padding });
29
+ return (
30
+ <View testID={testID} style={[resolved.base, resolved.elevation, style]}>
31
+ {children}
32
+ </View>
33
+ );
34
+ }
35
+
36
+ // Phase 2 conformance test 用の contract メタ(§2 A-3)。
37
+ Surface.__contract = CONTRACTS.surface;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * TextField — テキスト入力フィールド(contract: textfield)。RN TextInput の薄いラッパ。
3
+ *
4
+ * - label 必須(FORM_NO_LABEL_OMIT)。label(labelStyle)→ input → helperText / errorText の縦構成。
5
+ * - variant は検証状態(default/error/success)。disabled=true で variant を "disabled" に上書きし、
6
+ * editable=false + accessibilityState.disabled を併せて適用する(recipe description の規約)。
7
+ * - error 時は errorText を必ずテキストで表示する(FORM_NO_COLOR_ONLY_ERROR = 色だけで伝えない)。
8
+ * RN には aria-describedby 相当が無いため、accessibilityState ではなく Text の実表示で伝える。
9
+ * - focus は内部 state(onFocus/onBlur)で recipe states.focus の inputStyle 差分
10
+ * (borderColor=primary.500)を重ねる。disabled 時は focus させない。
11
+ * - 色・寸法の決定は pure resolver(textfield.styles.ts)に分離。
12
+ * recipes/app/textfield.recipe.json との機械照合は scripts/lib/textfield-conformance.test.ts が行う。
13
+ */
14
+
15
+ import { useMemo, useState } from "react";
16
+ import {
17
+ TextInput,
18
+ View,
19
+ Text as RNText,
20
+ type StyleProp,
21
+ type ViewStyle,
22
+ } from "react-native";
23
+ import { useTheme } from "../theme";
24
+ import { CONTRACTS } from "../contracts/contract-types";
25
+ import {
26
+ resolveTextFieldStyle,
27
+ resolveTextFieldFocusStyle,
28
+ type TextFieldSize,
29
+ } from "./textfield.styles";
30
+
31
+ interface TextFieldProps {
32
+ /** ラベル(必須。placeholder だけの入力欄を作らせない = FORM_NO_LABEL_OMIT)。 */
33
+ label: string;
34
+ value: string;
35
+ onChangeText?: (text: string) => void;
36
+ placeholder?: string;
37
+ /** 検証状態。disabled は prop(下の disabled)で表現するため variant には含めない。 */
38
+ variant?: "default" | "error" | "success";
39
+ /** true で variant を "disabled" に上書き + editable=false + accessibilityState.disabled。 */
40
+ disabled?: boolean;
41
+ size?: TextFieldSize;
42
+ /** 補足テキスト(error 表示中は errorText を優先して非表示)。 */
43
+ helperText?: string;
44
+ /** variant="error" 時に表示するエラーメッセージ(色だけでなくテキストで伝える)。 */
45
+ errorText?: string;
46
+ style?: StyleProp<ViewStyle>;
47
+ testID?: string;
48
+ }
49
+
50
+ export function TextField({
51
+ label,
52
+ value,
53
+ onChangeText,
54
+ placeholder,
55
+ variant = "default",
56
+ disabled = false,
57
+ size = "medium",
58
+ helperText,
59
+ errorText,
60
+ style,
61
+ testID,
62
+ }: TextFieldProps) {
63
+ const { theme, mode, colors } = useTheme();
64
+ const [focused, setFocused] = useState(false);
65
+
66
+ // disabled prop が variant を上書き(契約の variant 語彙 "disabled" への写像)。
67
+ const effectiveVariant = disabled ? "disabled" : variant;
68
+ const resolved = useMemo(
69
+ () => resolveTextFieldStyle(theme, mode, { variant: effectiveVariant, size }),
70
+ [theme, mode, effectiveVariant, size],
71
+ );
72
+ const focusStyle = useMemo(() => resolveTextFieldFocusStyle(theme), [theme]);
73
+
74
+ const showError = effectiveVariant === "error" && errorText != null;
75
+
76
+ return (
77
+ <View style={style} testID={testID}>
78
+ <RNText style={resolved.label}>{label}</RNText>
79
+ <TextInput
80
+ value={value}
81
+ onChangeText={onChangeText}
82
+ placeholder={placeholder}
83
+ placeholderTextColor={colors["text-muted"]}
84
+ editable={!disabled}
85
+ accessibilityLabel={label}
86
+ accessibilityState={{ disabled }}
87
+ onFocus={() => {
88
+ if (!disabled) setFocused(true);
89
+ }}
90
+ onBlur={() => setFocused(false)}
91
+ style={[resolved.input, focused && !disabled ? focusStyle : null]}
92
+ />
93
+ {helperText != null && !showError && (
94
+ <RNText style={resolved.helperText}>{helperText}</RNText>
95
+ )}
96
+ {showError && <RNText style={resolved.errorText}>{errorText}</RNText>}
97
+ </View>
98
+ );
99
+ }
100
+
101
+ // Phase 2 conformance test 用の contract メタ(§2 A-3)。
102
+ TextField.__contract = CONTRACTS.textfield;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Toast — 一時的な通知メッセージ(feedback)。contract: toast。
3
+ * 4タイプ(success/error/warning/info)。Alert と同系の bg subtle + text 色に elevation.sm を重ねる。
4
+ *
5
+ * - presentational component。表示位置・キュー・自動消滅はアプリ層の責務
6
+ * (entering/visible/dismissing のアニメ state は Phase 外、recipe description)。
7
+ * - closeButton は契約 anatomy で必須 → onClose を required prop にして型で強制。
8
+ * Pressable + テキスト ×(accessibilityLabel="閉じる" 必須)。
9
+ * - a11y(§2 web→RN mapping): web の role=status + aria-live=polite を
10
+ * accessibilityLiveRegion="polite"(Android のみ有効)に落とす。
11
+ * - elevation は theme.elevation.sm(iOS shadow* + Android elevation の複合値)を container に
12
+ * spread(Surface と同じ扱い)。
13
+ * - 色・寸法の決定は pure resolver(toast.styles.ts)に分離。dark mode は resolver が
14
+ * status.*.subtleDark / textDark を解決(info のみ primary 固定)。
15
+ * recipes/app/toast.recipe.json との機械照合は scripts/lib/toast-conformance.test.ts が行う。
16
+ */
17
+
18
+ import { useMemo } from "react";
19
+ import { Pressable, View, type StyleProp, type ViewStyle } from "react-native";
20
+ import { Text } from "../primitives/Text";
21
+ import { useTheme } from "../theme";
22
+ import { CONTRACTS } from "../contracts/contract-types";
23
+ import { TOAST_SPEC, resolveToastStyles, type ToastVariant } from "./toast.styles";
24
+
25
+ interface ToastProps {
26
+ /** 通知タイプ(contract variant)。 */
27
+ variant: ToastVariant;
28
+ /** 本文(必須)。 */
29
+ message: string;
30
+ /** 補足説明(任意。message の下に縦積み)。 */
31
+ description?: string;
32
+ /** 閉じるボタン(契約 anatomy で必須)。 */
33
+ onClose: () => void;
34
+ /** あれば message の後にアクションテキスト(onAction とセットで有効)。 */
35
+ actionLabel?: string;
36
+ onAction?: () => void;
37
+ style?: StyleProp<ViewStyle>;
38
+ testID?: string;
39
+ }
40
+
41
+ export function Toast({
42
+ variant,
43
+ message,
44
+ description,
45
+ onClose,
46
+ actionLabel,
47
+ onAction,
48
+ style,
49
+ testID,
50
+ }: ToastProps) {
51
+ const { theme, mode } = useTheme();
52
+ const styles = useMemo(() => resolveToastStyles(theme, mode, variant), [theme, mode, variant]);
53
+
54
+ return (
55
+ <View
56
+ accessibilityLiveRegion="polite"
57
+ testID={testID}
58
+ style={[styles.containerStyle, styles.elevation, style]}
59
+ >
60
+ <View style={{ flex: 1, gap: theme.spacing["1"] }}>
61
+ <Text
62
+ variant={TOAST_SPEC.messageFont}
63
+ weight={TOAST_SPEC.messageWeight}
64
+ style={{ color: styles.messageStyle.color }}
65
+ >
66
+ {message}
67
+ </Text>
68
+ {description != null && (
69
+ // description の style は recipe 外(anatomy の description? slot)。message と同色・同サイズ、weight 無し。
70
+ <Text variant={TOAST_SPEC.messageFont} style={{ color: styles.messageStyle.color }}>
71
+ {description}
72
+ </Text>
73
+ )}
74
+ </View>
75
+ {actionLabel != null && onAction != null && (
76
+ <Pressable onPress={onAction} accessibilityRole="button" hitSlop={10}>
77
+ <Text
78
+ variant={TOAST_SPEC.messageFont}
79
+ weight={TOAST_SPEC.messageWeight}
80
+ style={{ color: styles.messageStyle.color, textDecorationLine: "underline" }}
81
+ >
82
+ {actionLabel}
83
+ </Text>
84
+ </Pressable>
85
+ )}
86
+ {/* 最小タップターゲット確保: min 24 + hitSlop(Tag removable と同じ手当て)。 */}
87
+ <Pressable
88
+ onPress={onClose}
89
+ accessibilityRole="button"
90
+ accessibilityLabel="閉じる"
91
+ hitSlop={10}
92
+ style={{ minWidth: 24, minHeight: 24, alignItems: "center", justifyContent: "center" }}
93
+ >
94
+ <Text variant="xs" style={{ color: styles.messageStyle.color }}>
95
+ ×
96
+ </Text>
97
+ </Pressable>
98
+ </View>
99
+ );
100
+ }
101
+
102
+ // Phase 2 conformance test 用の contract メタ(§2 A-3)。
103
+ Toast.__contract = CONTRACTS.toast;