@studio-piot/native-ui 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 (247) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +91 -0
  3. package/dist/components/Button/Button.d.ts +16 -0
  4. package/dist/components/Button/Button.d.ts.map +1 -0
  5. package/dist/components/Button/Button.js +44 -0
  6. package/dist/components/Button/Button.js.map +1 -0
  7. package/dist/components/Button/buttonRadius.d.ts +3 -0
  8. package/dist/components/Button/buttonRadius.d.ts.map +1 -0
  9. package/dist/components/Button/buttonRadius.js +17 -0
  10. package/dist/components/Button/buttonRadius.js.map +1 -0
  11. package/dist/components/Button/index.d.ts +3 -0
  12. package/dist/components/Button/index.d.ts.map +1 -0
  13. package/dist/components/Button/index.js +8 -0
  14. package/dist/components/Button/index.js.map +1 -0
  15. package/dist/components/Field/FieldText.d.ts +3 -0
  16. package/dist/components/Field/FieldText.d.ts.map +1 -0
  17. package/dist/components/Field/FieldText.js +9 -0
  18. package/dist/components/Field/FieldText.js.map +1 -0
  19. package/dist/components/Field/FieldTextarea.d.ts +3 -0
  20. package/dist/components/Field/FieldTextarea.d.ts.map +1 -0
  21. package/dist/components/Field/FieldTextarea.js +10 -0
  22. package/dist/components/Field/FieldTextarea.js.map +1 -0
  23. package/dist/components/Field/fieldChrome.d.ts +10 -0
  24. package/dist/components/Field/fieldChrome.d.ts.map +1 -0
  25. package/dist/components/Field/fieldChrome.js +100 -0
  26. package/dist/components/Field/fieldChrome.js.map +1 -0
  27. package/dist/components/Field/fieldThemeRoles.d.ts +13 -0
  28. package/dist/components/Field/fieldThemeRoles.d.ts.map +1 -0
  29. package/dist/components/Field/fieldThemeRoles.js +15 -0
  30. package/dist/components/Field/fieldThemeRoles.js.map +1 -0
  31. package/dist/components/Field/index.d.ts +6 -0
  32. package/dist/components/Field/index.d.ts.map +1 -0
  33. package/dist/components/Field/index.js +10 -0
  34. package/dist/components/Field/index.js.map +1 -0
  35. package/dist/components/Field/resolveFieldBorder.d.ts +16 -0
  36. package/dist/components/Field/resolveFieldBorder.d.ts.map +1 -0
  37. package/dist/components/Field/resolveFieldBorder.js +20 -0
  38. package/dist/components/Field/resolveFieldBorder.js.map +1 -0
  39. package/dist/components/Field/resolveFieldColor.d.ts +7 -0
  40. package/dist/components/Field/resolveFieldColor.d.ts.map +1 -0
  41. package/dist/components/Field/resolveFieldColor.js +21 -0
  42. package/dist/components/Field/resolveFieldColor.js.map +1 -0
  43. package/dist/components/Field/types.d.ts +46 -0
  44. package/dist/components/Field/types.d.ts.map +1 -0
  45. package/dist/components/Field/types.js +3 -0
  46. package/dist/components/Field/types.js.map +1 -0
  47. package/dist/components/Field/useResolvedFieldColor.d.ts +2 -0
  48. package/dist/components/Field/useResolvedFieldColor.d.ts.map +1 -0
  49. package/dist/components/Field/useResolvedFieldColor.js +15 -0
  50. package/dist/components/Field/useResolvedFieldColor.js.map +1 -0
  51. package/dist/components/Form/Form.d.ts +3 -0
  52. package/dist/components/Form/Form.d.ts.map +1 -0
  53. package/dist/components/Form/Form.js +100 -0
  54. package/dist/components/Form/Form.js.map +1 -0
  55. package/dist/components/Form/FormCloseButton.d.ts +4 -0
  56. package/dist/components/Form/FormCloseButton.d.ts.map +1 -0
  57. package/dist/components/Form/FormCloseButton.js +17 -0
  58. package/dist/components/Form/FormCloseButton.js.map +1 -0
  59. package/dist/components/Form/index.d.ts +4 -0
  60. package/dist/components/Form/index.d.ts.map +1 -0
  61. package/dist/components/Form/index.js +8 -0
  62. package/dist/components/Form/index.js.map +1 -0
  63. package/dist/components/Form/resolveThemeColor.d.ts +11 -0
  64. package/dist/components/Form/resolveThemeColor.d.ts.map +1 -0
  65. package/dist/components/Form/resolveThemeColor.js +21 -0
  66. package/dist/components/Form/resolveThemeColor.js.map +1 -0
  67. package/dist/components/Form/types.d.ts +30 -0
  68. package/dist/components/Form/types.d.ts.map +1 -0
  69. package/dist/components/Form/types.js +3 -0
  70. package/dist/components/Form/types.js.map +1 -0
  71. package/dist/components/Form/useResolvedThemeColor.d.ts +2 -0
  72. package/dist/components/Form/useResolvedThemeColor.d.ts.map +1 -0
  73. package/dist/components/Form/useResolvedThemeColor.js +15 -0
  74. package/dist/components/Form/useResolvedThemeColor.js.map +1 -0
  75. package/dist/components/Form/withAlpha.d.ts +2 -0
  76. package/dist/components/Form/withAlpha.d.ts.map +1 -0
  77. package/dist/components/Form/withAlpha.js +12 -0
  78. package/dist/components/Form/withAlpha.js.map +1 -0
  79. package/dist/components/LinkBlocks/LinkBlocks.d.ts +3 -0
  80. package/dist/components/LinkBlocks/LinkBlocks.d.ts.map +1 -0
  81. package/dist/components/LinkBlocks/LinkBlocks.js +75 -0
  82. package/dist/components/LinkBlocks/LinkBlocks.js.map +1 -0
  83. package/dist/components/LinkBlocks/filterGroups.d.ts +3 -0
  84. package/dist/components/LinkBlocks/filterGroups.d.ts.map +1 -0
  85. package/dist/components/LinkBlocks/filterGroups.js +7 -0
  86. package/dist/components/LinkBlocks/filterGroups.js.map +1 -0
  87. package/dist/components/LinkBlocks/index.d.ts +4 -0
  88. package/dist/components/LinkBlocks/index.d.ts.map +1 -0
  89. package/dist/components/LinkBlocks/index.js +8 -0
  90. package/dist/components/LinkBlocks/index.js.map +1 -0
  91. package/dist/components/LinkBlocks/types.d.ts +26 -0
  92. package/dist/components/LinkBlocks/types.d.ts.map +1 -0
  93. package/dist/components/LinkBlocks/types.js +3 -0
  94. package/dist/components/LinkBlocks/types.js.map +1 -0
  95. package/dist/components/RadioField/RadioField.d.ts +3 -0
  96. package/dist/components/RadioField/RadioField.d.ts.map +1 -0
  97. package/dist/components/RadioField/RadioField.js +76 -0
  98. package/dist/components/RadioField/RadioField.js.map +1 -0
  99. package/dist/components/RadioField/RadioMark.d.ts +7 -0
  100. package/dist/components/RadioField/RadioMark.d.ts.map +1 -0
  101. package/dist/components/RadioField/RadioMark.js +32 -0
  102. package/dist/components/RadioField/RadioMark.js.map +1 -0
  103. package/dist/components/RadioField/index.d.ts +4 -0
  104. package/dist/components/RadioField/index.d.ts.map +1 -0
  105. package/dist/components/RadioField/index.js +8 -0
  106. package/dist/components/RadioField/index.js.map +1 -0
  107. package/dist/components/RadioField/radioThemeRoles.d.ts +11 -0
  108. package/dist/components/RadioField/radioThemeRoles.d.ts.map +1 -0
  109. package/dist/components/RadioField/radioThemeRoles.js +13 -0
  110. package/dist/components/RadioField/radioThemeRoles.js.map +1 -0
  111. package/dist/components/RadioField/types.d.ts +24 -0
  112. package/dist/components/RadioField/types.d.ts.map +1 -0
  113. package/dist/components/RadioField/types.js +3 -0
  114. package/dist/components/RadioField/types.js.map +1 -0
  115. package/dist/components/Select/Select.d.ts +3 -0
  116. package/dist/components/Select/Select.d.ts.map +1 -0
  117. package/dist/components/Select/Select.js +137 -0
  118. package/dist/components/Select/Select.js.map +1 -0
  119. package/dist/components/Select/SelectCloseButton.d.ts +4 -0
  120. package/dist/components/Select/SelectCloseButton.d.ts.map +1 -0
  121. package/dist/components/Select/SelectCloseButton.js +17 -0
  122. package/dist/components/Select/SelectCloseButton.js.map +1 -0
  123. package/dist/components/Select/index.d.ts +6 -0
  124. package/dist/components/Select/index.d.ts.map +1 -0
  125. package/dist/components/Select/index.js +14 -0
  126. package/dist/components/Select/index.js.map +1 -0
  127. package/dist/components/Select/resolveSelectDetentIndex.d.ts +9 -0
  128. package/dist/components/Select/resolveSelectDetentIndex.d.ts.map +1 -0
  129. package/dist/components/Select/resolveSelectDetentIndex.js +16 -0
  130. package/dist/components/Select/resolveSelectDetentIndex.js.map +1 -0
  131. package/dist/components/Select/selectThemeRoles.d.ts +12 -0
  132. package/dist/components/Select/selectThemeRoles.d.ts.map +1 -0
  133. package/dist/components/Select/selectThemeRoles.js +14 -0
  134. package/dist/components/Select/selectThemeRoles.js.map +1 -0
  135. package/dist/components/Select/types.d.ts +35 -0
  136. package/dist/components/Select/types.d.ts.map +1 -0
  137. package/dist/components/Select/types.js +3 -0
  138. package/dist/components/Select/types.js.map +1 -0
  139. package/dist/components/index.d.ts +7 -0
  140. package/dist/components/index.d.ts.map +1 -0
  141. package/dist/components/index.js +27 -0
  142. package/dist/components/index.js.map +1 -0
  143. package/dist/index.d.ts +10 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +66 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/layout/index.d.ts +2 -0
  148. package/dist/layout/index.d.ts.map +1 -0
  149. package/dist/layout/index.js +6 -0
  150. package/dist/layout/index.js.map +1 -0
  151. package/dist/layout/screenPadding.d.ts +3 -0
  152. package/dist/layout/screenPadding.d.ts.map +1 -0
  153. package/dist/layout/screenPadding.js +6 -0
  154. package/dist/layout/screenPadding.js.map +1 -0
  155. package/dist/theme/ThemeProvider.d.ts +11 -0
  156. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  157. package/dist/theme/ThemeProvider.js +11 -0
  158. package/dist/theme/ThemeProvider.js.map +1 -0
  159. package/dist/theme/index.d.ts +6 -0
  160. package/dist/theme/index.d.ts.map +1 -0
  161. package/dist/theme/index.js +12 -0
  162. package/dist/theme/index.js.map +1 -0
  163. package/dist/theme/resolveColorScheme.d.ts +2 -0
  164. package/dist/theme/resolveColorScheme.d.ts.map +1 -0
  165. package/dist/theme/resolveColorScheme.js +7 -0
  166. package/dist/theme/resolveColorScheme.js.map +1 -0
  167. package/dist/theme/types.d.ts +8 -0
  168. package/dist/theme/types.d.ts.map +1 -0
  169. package/dist/theme/types.js +3 -0
  170. package/dist/theme/types.js.map +1 -0
  171. package/dist/theme/useColorScheme.d.ts +2 -0
  172. package/dist/theme/useColorScheme.d.ts.map +1 -0
  173. package/dist/theme/useColorScheme.js +16 -0
  174. package/dist/theme/useColorScheme.js.map +1 -0
  175. package/dist/theme/useThemeColor.d.ts +5 -0
  176. package/dist/theme/useThemeColor.d.ts.map +1 -0
  177. package/dist/theme/useThemeColor.js +25 -0
  178. package/dist/theme/useThemeColor.js.map +1 -0
  179. package/docs/components/button.md +162 -0
  180. package/docs/components/field.md +128 -0
  181. package/docs/components/form.md +86 -0
  182. package/docs/components/link-blocks.md +120 -0
  183. package/docs/components/radio-field.md +100 -0
  184. package/docs/components/select.md +59 -0
  185. package/docs/superpowers/plans/2026-09-24-field.md +583 -0
  186. package/docs/superpowers/plans/2026-09-24-link-blocks.md +414 -0
  187. package/docs/superpowers/plans/2026-09-24-theme-provider-button.md +493 -0
  188. package/docs/superpowers/plans/2026-09-25-radio-field.md +516 -0
  189. package/docs/superpowers/plans/2026-09-25-select.md +22 -0
  190. package/docs/superpowers/specs/2026-09-24-field-design.md +205 -0
  191. package/docs/superpowers/specs/2026-09-24-link-blocks-design.md +167 -0
  192. package/docs/superpowers/specs/2026-09-24-theme-provider-button-design.md +178 -0
  193. package/docs/superpowers/specs/2026-09-25-radio-field-design.md +136 -0
  194. package/docs/superpowers/specs/2026-09-25-select-design.md +164 -0
  195. package/package.json +75 -0
  196. package/src/components/Button/Button.tsx +97 -0
  197. package/src/components/Button/buttonRadius.test.ts +16 -0
  198. package/src/components/Button/buttonRadius.ts +24 -0
  199. package/src/components/Button/index.ts +2 -0
  200. package/src/components/Field/FieldText.tsx +6 -0
  201. package/src/components/Field/FieldTextarea.tsx +18 -0
  202. package/src/components/Field/fieldChrome.tsx +219 -0
  203. package/src/components/Field/fieldThemeRoles.ts +13 -0
  204. package/src/components/Field/index.ts +5 -0
  205. package/src/components/Field/resolveFieldBorder.test.ts +76 -0
  206. package/src/components/Field/resolveFieldBorder.ts +40 -0
  207. package/src/components/Field/resolveFieldColor.test.ts +92 -0
  208. package/src/components/Field/resolveFieldColor.ts +26 -0
  209. package/src/components/Field/types.ts +53 -0
  210. package/src/components/Field/useResolvedFieldColor.ts +15 -0
  211. package/src/components/Form/Form.tsx +205 -0
  212. package/src/components/Form/FormCloseButton.tsx +19 -0
  213. package/src/components/Form/index.ts +3 -0
  214. package/src/components/Form/resolveThemeColor.ts +24 -0
  215. package/src/components/Form/types.ts +30 -0
  216. package/src/components/Form/useResolvedThemeColor.ts +15 -0
  217. package/src/components/Form/withAlpha.test.ts +13 -0
  218. package/src/components/Form/withAlpha.ts +9 -0
  219. package/src/components/LinkBlocks/LinkBlocks.tsx +165 -0
  220. package/src/components/LinkBlocks/filterGroups.test.ts +37 -0
  221. package/src/components/LinkBlocks/filterGroups.ts +7 -0
  222. package/src/components/LinkBlocks/index.ts +7 -0
  223. package/src/components/LinkBlocks/types.ts +28 -0
  224. package/src/components/RadioField/RadioField.tsx +168 -0
  225. package/src/components/RadioField/RadioMark.tsx +54 -0
  226. package/src/components/RadioField/index.ts +3 -0
  227. package/src/components/RadioField/radioThemeRoles.test.ts +15 -0
  228. package/src/components/RadioField/radioThemeRoles.ts +11 -0
  229. package/src/components/RadioField/types.ts +25 -0
  230. package/src/components/Select/Select.tsx +279 -0
  231. package/src/components/Select/SelectCloseButton.tsx +19 -0
  232. package/src/components/Select/index.ts +13 -0
  233. package/src/components/Select/resolveSelectDetentIndex.test.ts +20 -0
  234. package/src/components/Select/resolveSelectDetentIndex.ts +16 -0
  235. package/src/components/Select/selectThemeRoles.ts +12 -0
  236. package/src/components/Select/types.ts +39 -0
  237. package/src/components/index.ts +42 -0
  238. package/src/index.ts +50 -0
  239. package/src/layout/index.ts +1 -0
  240. package/src/layout/screenPadding.ts +2 -0
  241. package/src/theme/ThemeProvider.tsx +20 -0
  242. package/src/theme/index.ts +5 -0
  243. package/src/theme/resolveColorScheme.test.ts +16 -0
  244. package/src/theme/resolveColorScheme.ts +5 -0
  245. package/src/theme/types.ts +5 -0
  246. package/src/theme/useColorScheme.ts +13 -0
  247. package/src/theme/useThemeColor.ts +25 -0
@@ -0,0 +1,165 @@
1
+ import {
2
+ StyleSheet,
3
+ Text,
4
+ TouchableOpacity,
5
+ View,
6
+ } from "react-native";
7
+ import { buttonRadiusValue } from "../Button/buttonRadius";
8
+ import { filterLinkBlockGroups } from "./filterGroups";
9
+ import type { LinkBlockGroup, LinkBlocksProps } from "./types";
10
+
11
+ const ICON_SLOT = 28;
12
+ const ROW_PAD_H = 16;
13
+
14
+ export function LinkBlocks({
15
+ blocks,
16
+ surfaceColor,
17
+ labelColor,
18
+ dividerColor,
19
+ chevronColor,
20
+ titleColor,
21
+ radius = "xl",
22
+ style,
23
+ }: LinkBlocksProps) {
24
+ const visible = filterLinkBlockGroups(blocks);
25
+ const borderRadius = buttonRadiusValue(radius);
26
+ const resolvedTitleColor = titleColor ?? labelColor;
27
+
28
+ return (
29
+ <View style={[styles.stack, style]}>
30
+ {visible.map((group) => (
31
+ <Group
32
+ key={group.key}
33
+ group={group}
34
+ surfaceColor={surfaceColor}
35
+ labelColor={labelColor}
36
+ dividerColor={dividerColor}
37
+ chevronColor={chevronColor}
38
+ titleColor={resolvedTitleColor}
39
+ borderRadius={borderRadius}
40
+ />
41
+ ))}
42
+ </View>
43
+ );
44
+ }
45
+
46
+ function Group({
47
+ group,
48
+ surfaceColor,
49
+ labelColor,
50
+ dividerColor,
51
+ chevronColor,
52
+ titleColor,
53
+ borderRadius,
54
+ }: {
55
+ group: LinkBlockGroup;
56
+ surfaceColor: string;
57
+ labelColor: string;
58
+ dividerColor: string;
59
+ chevronColor: string;
60
+ titleColor: string;
61
+ borderRadius: number;
62
+ }) {
63
+ const showIconSlot = group.items.some((item) => item.icon != null);
64
+ const dividerInset = ROW_PAD_H + (showIconSlot ? ICON_SLOT + 12 : 0);
65
+
66
+ return (
67
+ <View style={styles.group}>
68
+ {group.title ? (
69
+ <Text style={[styles.title, { color: titleColor }]}>{group.title}</Text>
70
+ ) : null}
71
+ <View
72
+ style={[
73
+ styles.surface,
74
+ { backgroundColor: surfaceColor, borderRadius },
75
+ ]}
76
+ >
77
+ {group.items.map((item, index) => (
78
+ <View key={item.key}>
79
+ {index > 0 ? (
80
+ <View
81
+ style={[
82
+ styles.divider,
83
+ {
84
+ backgroundColor: dividerColor,
85
+ marginLeft: dividerInset,
86
+ },
87
+ ]}
88
+ />
89
+ ) : null}
90
+ <TouchableOpacity
91
+ accessibilityLabel={item.label}
92
+ accessibilityRole="button"
93
+ accessibilityState={{ disabled: !!item.disabled }}
94
+ activeOpacity={0.75}
95
+ disabled={item.disabled}
96
+ onPress={item.onPress}
97
+ style={[styles.row, item.disabled && styles.blocked]}
98
+ >
99
+ {showIconSlot ? (
100
+ <View style={styles.iconSlot}>{item.icon ?? null}</View>
101
+ ) : null}
102
+ <Text style={[styles.label, { color: labelColor, flex: 1 }]}>
103
+ {item.label}
104
+ </Text>
105
+ <Text
106
+ accessibilityElementsHidden
107
+ importantForAccessibility="no"
108
+ selectable={false}
109
+ style={[styles.chevron, { color: chevronColor }]}
110
+ >
111
+ ›
112
+ </Text>
113
+ </TouchableOpacity>
114
+ </View>
115
+ ))}
116
+ </View>
117
+ </View>
118
+ );
119
+ }
120
+
121
+ const styles = StyleSheet.create({
122
+ stack: {
123
+ width: "100%",
124
+ gap: 16,
125
+ },
126
+ group: {
127
+ width: "100%",
128
+ gap: 8,
129
+ },
130
+ title: {
131
+ fontSize: 13,
132
+ fontWeight: "600",
133
+ paddingHorizontal: 4,
134
+ },
135
+ surface: {
136
+ width: "100%",
137
+ overflow: "hidden",
138
+ },
139
+ row: {
140
+ minHeight: 52,
141
+ paddingHorizontal: ROW_PAD_H,
142
+ flexDirection: "row",
143
+ alignItems: "center",
144
+ gap: 12,
145
+ },
146
+ blocked: {
147
+ opacity: 0.4,
148
+ },
149
+ iconSlot: {
150
+ width: ICON_SLOT,
151
+ alignItems: "center",
152
+ justifyContent: "center",
153
+ },
154
+ label: {
155
+ fontSize: 17,
156
+ },
157
+ chevron: {
158
+ fontSize: 22,
159
+ fontWeight: "300",
160
+ lineHeight: 24,
161
+ },
162
+ divider: {
163
+ height: StyleSheet.hairlineWidth,
164
+ },
165
+ });
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { filterLinkBlockGroups } from "./filterGroups.ts";
4
+ import type { LinkBlockGroup } from "./types.ts";
5
+
6
+ describe("filterLinkBlockGroups", () => {
7
+ it("drops groups with empty items", () => {
8
+ const blocks: LinkBlockGroup[] = [
9
+ { key: "a", title: "A", items: [] },
10
+ {
11
+ key: "b",
12
+ items: [{ key: "1", label: "One", onPress: () => undefined }],
13
+ },
14
+ ];
15
+ const next = filterLinkBlockGroups(blocks);
16
+ assert.equal(next.length, 1);
17
+ assert.equal(next[0]?.key, "b");
18
+ });
19
+
20
+ it("keeps titled and untitled non-empty groups in order", () => {
21
+ const blocks: LinkBlockGroup[] = [
22
+ {
23
+ key: "t",
24
+ title: "Settings",
25
+ items: [{ key: "1", label: "One", onPress: () => undefined }],
26
+ },
27
+ {
28
+ key: "u",
29
+ items: [{ key: "2", label: "Two", onPress: () => undefined }],
30
+ },
31
+ ];
32
+ assert.deepEqual(
33
+ filterLinkBlockGroups(blocks).map((g) => g.key),
34
+ ["t", "u"],
35
+ );
36
+ });
37
+ });
@@ -0,0 +1,7 @@
1
+ import type { LinkBlockGroup } from "./types";
2
+
3
+ export function filterLinkBlockGroups(
4
+ blocks: LinkBlockGroup[],
5
+ ): LinkBlockGroup[] {
6
+ return blocks.filter((group) => group.items.length > 0);
7
+ }
@@ -0,0 +1,7 @@
1
+ export { LinkBlocks } from "./LinkBlocks";
2
+ export type {
3
+ LinkBlockGroup,
4
+ LinkBlockItem,
5
+ LinkBlocksProps,
6
+ } from "./types";
7
+ export { filterLinkBlockGroups } from "./filterGroups";
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import type { StyleProp, ViewStyle } from "react-native";
3
+ import type { ButtonRadius } from "../Button/buttonRadius";
4
+
5
+ export type LinkBlockItem = {
6
+ key: string;
7
+ label: string;
8
+ onPress: () => void;
9
+ icon?: ReactNode;
10
+ disabled?: boolean;
11
+ };
12
+
13
+ export type LinkBlockGroup = {
14
+ key: string;
15
+ title?: string;
16
+ items: LinkBlockItem[];
17
+ };
18
+
19
+ export type LinkBlocksProps = {
20
+ blocks: LinkBlockGroup[];
21
+ surfaceColor: string;
22
+ labelColor: string;
23
+ dividerColor: string;
24
+ chevronColor: string;
25
+ titleColor?: string;
26
+ radius?: ButtonRadius;
27
+ style?: StyleProp<ViewStyle>;
28
+ };
@@ -0,0 +1,168 @@
1
+ import {
2
+ StyleSheet,
3
+ Text,
4
+ TouchableOpacity,
5
+ View,
6
+ } from "react-native";
7
+ import { buttonRadiusValue } from "../Button/buttonRadius";
8
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
9
+ import { RadioMark } from "./RadioMark";
10
+ import { RADIO_THEME_ROLES } from "./radioThemeRoles";
11
+ import type { RadioFieldProps } from "./types";
12
+
13
+ const ROW_PAD_H = 16;
14
+
15
+ export function RadioField<T extends string = string>({
16
+ value,
17
+ onChange,
18
+ options,
19
+ label,
20
+ radius = "xl",
21
+ backgroundColor: backgroundColorProp,
22
+ labelColor: labelColorProp,
23
+ descriptionColor: descriptionColorProp,
24
+ dividerColor: dividerColorProp,
25
+ radioColor: radioColorProp,
26
+ selectedColor: selectedColorProp,
27
+ titleColor: titleColorProp,
28
+ style,
29
+ }: RadioFieldProps<T>) {
30
+ const backgroundColor = useResolvedFieldColor(
31
+ backgroundColorProp,
32
+ RADIO_THEME_ROLES.backgroundColor,
33
+ );
34
+ const labelColor = useResolvedFieldColor(
35
+ labelColorProp,
36
+ RADIO_THEME_ROLES.labelColor,
37
+ );
38
+ const descriptionColor = useResolvedFieldColor(
39
+ descriptionColorProp,
40
+ RADIO_THEME_ROLES.descriptionColor,
41
+ );
42
+ const dividerColor = useResolvedFieldColor(
43
+ dividerColorProp,
44
+ RADIO_THEME_ROLES.dividerColor,
45
+ );
46
+ const radioColor = useResolvedFieldColor(
47
+ radioColorProp,
48
+ RADIO_THEME_ROLES.radioColor,
49
+ );
50
+ const selectedColor = useResolvedFieldColor(
51
+ selectedColorProp,
52
+ RADIO_THEME_ROLES.selectedColor,
53
+ );
54
+ const titleColor = useResolvedFieldColor(
55
+ titleColorProp,
56
+ RADIO_THEME_ROLES.titleColor,
57
+ );
58
+ const borderRadius = buttonRadiusValue(radius);
59
+
60
+ if (options.length === 0) {
61
+ return null;
62
+ }
63
+
64
+ return (
65
+ <View style={[styles.group, style]}>
66
+ {label ? (
67
+ <Text style={[styles.title, { color: titleColor }]}>{label}</Text>
68
+ ) : null}
69
+ <View
70
+ accessibilityRole="radiogroup"
71
+ {...(label ? { accessibilityLabel: label } : {})}
72
+ style={[
73
+ styles.surface,
74
+ { backgroundColor, borderRadius },
75
+ ]}
76
+ >
77
+ {options.map((option, index) => {
78
+ const selected = value === option.value;
79
+ return (
80
+ <View key={option.value}>
81
+ {index > 0 ? (
82
+ <View
83
+ style={[styles.divider, { backgroundColor: dividerColor }]}
84
+ />
85
+ ) : null}
86
+ <TouchableOpacity
87
+ accessibilityLabel={
88
+ option.description
89
+ ? `${option.label}. ${option.description}`
90
+ : option.label
91
+ }
92
+ accessibilityRole="radio"
93
+ accessibilityState={{
94
+ selected,
95
+ checked: selected,
96
+ disabled: !!option.disabled,
97
+ }}
98
+ activeOpacity={0.75}
99
+ disabled={option.disabled}
100
+ onPress={() => onChange(option.value)}
101
+ style={[styles.row, option.disabled && styles.blocked]}
102
+ >
103
+ <View style={styles.textCol}>
104
+ <Text style={[styles.label, { color: labelColor }]}>
105
+ {option.label}
106
+ </Text>
107
+ {option.description ? (
108
+ <Text
109
+ style={[styles.description, { color: descriptionColor }]}
110
+ >
111
+ {option.description}
112
+ </Text>
113
+ ) : null}
114
+ </View>
115
+ <RadioMark
116
+ holeColor={backgroundColor}
117
+ radioColor={radioColor}
118
+ selected={selected}
119
+ selectedColor={selectedColor}
120
+ />
121
+ </TouchableOpacity>
122
+ </View>
123
+ );
124
+ })}
125
+ </View>
126
+ </View>
127
+ );
128
+ }
129
+
130
+ const styles = StyleSheet.create({
131
+ group: {
132
+ width: "100%",
133
+ gap: 8,
134
+ },
135
+ title: {
136
+ fontSize: 13,
137
+ fontWeight: "600",
138
+ paddingHorizontal: 4,
139
+ },
140
+ surface: {
141
+ width: "100%",
142
+ overflow: "hidden",
143
+ },
144
+ row: {
145
+ minHeight: 52,
146
+ paddingHorizontal: ROW_PAD_H,
147
+ paddingVertical: 10,
148
+ flexDirection: "row",
149
+ alignItems: "center",
150
+ gap: 12,
151
+ },
152
+ blocked: {
153
+ opacity: 0.4,
154
+ },
155
+ textCol: {
156
+ flex: 1,
157
+ gap: 2,
158
+ },
159
+ label: {
160
+ fontSize: 17,
161
+ },
162
+ description: {
163
+ fontSize: 13,
164
+ },
165
+ divider: {
166
+ height: StyleSheet.hairlineWidth,
167
+ },
168
+ });
@@ -0,0 +1,54 @@
1
+ import { StyleSheet, View } from "react-native";
2
+
3
+ const SIZE = 22;
4
+ const DOT = 8;
5
+
6
+ export function RadioMark({
7
+ selected,
8
+ radioColor,
9
+ selectedColor,
10
+ holeColor,
11
+ }: {
12
+ selected: boolean;
13
+ radioColor: string;
14
+ selectedColor: string;
15
+ holeColor: string;
16
+ }) {
17
+ if (selected) {
18
+ return (
19
+ <View
20
+ accessibilityElementsHidden
21
+ importantForAccessibility="no"
22
+ style={[
23
+ styles.outer,
24
+ { backgroundColor: selectedColor, borderColor: selectedColor },
25
+ ]}
26
+ >
27
+ <View style={[styles.dot, { backgroundColor: holeColor }]} />
28
+ </View>
29
+ );
30
+ }
31
+ return (
32
+ <View
33
+ accessibilityElementsHidden
34
+ importantForAccessibility="no"
35
+ style={[styles.outer, { borderColor: radioColor }]}
36
+ />
37
+ );
38
+ }
39
+
40
+ const styles = StyleSheet.create({
41
+ outer: {
42
+ width: SIZE,
43
+ height: SIZE,
44
+ borderRadius: SIZE / 2,
45
+ borderWidth: 2,
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ },
49
+ dot: {
50
+ width: DOT,
51
+ height: DOT,
52
+ borderRadius: DOT / 2,
53
+ },
54
+ });
@@ -0,0 +1,3 @@
1
+ export { RadioField } from "./RadioField";
2
+ export { RADIO_THEME_ROLES, type RadioThemeColorProp } from "./radioThemeRoles";
3
+ export type { RadioOption, RadioFieldProps } from "./types";
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { RADIO_THEME_ROLES } from "./radioThemeRoles.ts";
4
+
5
+ describe("RADIO_THEME_ROLES", () => {
6
+ it("maps colour props to Field roles plus fieldRadioSelected", () => {
7
+ assert.equal(RADIO_THEME_ROLES.backgroundColor, "fieldBackground");
8
+ assert.equal(RADIO_THEME_ROLES.labelColor, "fieldText");
9
+ assert.equal(RADIO_THEME_ROLES.descriptionColor, "fieldDescription");
10
+ assert.equal(RADIO_THEME_ROLES.dividerColor, "fieldBorder");
11
+ assert.equal(RADIO_THEME_ROLES.radioColor, "fieldBorder");
12
+ assert.equal(RADIO_THEME_ROLES.selectedColor, "fieldRadioSelected");
13
+ assert.equal(RADIO_THEME_ROLES.titleColor, "fieldLabel");
14
+ });
15
+ });
@@ -0,0 +1,11 @@
1
+ export const RADIO_THEME_ROLES = {
2
+ backgroundColor: "fieldBackground",
3
+ labelColor: "fieldText",
4
+ descriptionColor: "fieldDescription",
5
+ dividerColor: "fieldBorder",
6
+ radioColor: "fieldBorder",
7
+ selectedColor: "fieldRadioSelected",
8
+ titleColor: "fieldLabel",
9
+ } as const;
10
+
11
+ export type RadioThemeColorProp = keyof typeof RADIO_THEME_ROLES;
@@ -0,0 +1,25 @@
1
+ import type { StyleProp, ViewStyle } from "react-native";
2
+ import type { ButtonRadius } from "../Button/buttonRadius";
3
+
4
+ export type RadioOption<T extends string = string> = {
5
+ value: T;
6
+ label: string;
7
+ description?: string;
8
+ disabled?: boolean;
9
+ };
10
+
11
+ export type RadioFieldProps<T extends string = string> = {
12
+ value: T | null;
13
+ onChange: (value: T) => void;
14
+ options: RadioOption<T>[];
15
+ label?: string;
16
+ radius?: ButtonRadius;
17
+ backgroundColor?: string;
18
+ labelColor?: string;
19
+ descriptionColor?: string;
20
+ dividerColor?: string;
21
+ radioColor?: string;
22
+ selectedColor?: string;
23
+ titleColor?: string;
24
+ style?: StyleProp<ViewStyle>;
25
+ };