@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,516 @@
1
+ # RadioField Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Ship a controlled `RadioField` (LinkBlocks-style card, Field colours + `fieldRadioSelected`) and migrate Ribe Appearance to it.
6
+
7
+ **Architecture:** One public `RadioField` component resolves theme colours like Field, renders a rounded card of option rows (label + optional description left, `RadioMark` right). Pure helpers stay unit-tested without an RN renderer. Ribe adds `fieldRadioSelected` and swaps Appearance’s checkmark list for `RadioField`.
8
+
9
+ **Tech Stack:** TypeScript, React 19+, React Native, `tsc` → `dist`, Node `--test` for pure helpers; Ribe Expo Router + existing `ThemeProvider` / `Colors`.
10
+
11
+ ## Global Constraints
12
+
13
+ - Spec: `docs/superpowers/specs/2026-09-25-radio-field-design.md`
14
+ - No brand names or palettes in the package
15
+ - Controlled only: `value` + `onChange` + `options` required
16
+ - Theme defaults from Field roles + new `fieldRadioSelected`; optional colour props override
17
+ - Layout metrics match LinkBlocks (row minHeight 52, pad H 16, radius default `"xl"`)
18
+ - No multi-group RadioBlocks, icons, multi-select, or liquid-glass radios
19
+ - Commit only when the user asks (unless they explicitly request commits in the execution session)
20
+
21
+ ## File map
22
+
23
+ | File | Responsibility |
24
+ |---|---|
25
+ | `src/components/RadioField/radioThemeRoles.ts` | Role map for defaults |
26
+ | `src/components/RadioField/types.ts` | `RadioOption`, `RadioFieldProps` |
27
+ | `src/components/RadioField/RadioMark.tsx` | Selected / unselected circle |
28
+ | `src/components/RadioField/RadioField.tsx` | Card + rows |
29
+ | `src/components/RadioField/index.ts` | Barrel |
30
+ | `src/components/RadioField/radioThemeRoles.test.ts` | Role map assertions |
31
+ | `src/components/index.ts` / `src/index.ts` | Public exports |
32
+ | `docs/components/radio-field.md` | Usage |
33
+ | `README.md` | Link to usage + design |
34
+ | Ribe `src/config/colors.ts` (+ test) | `fieldRadioSelected` |
35
+ | Ribe `src/app/(app)/(tabs)/profile/appearance.tsx` | Use `RadioField` |
36
+
37
+ ---
38
+
39
+ ### Task 1: RadioField package component, tests, docs, exports
40
+
41
+ **Files:**
42
+ - Create: `src/components/RadioField/radioThemeRoles.ts`
43
+ - Create: `src/components/RadioField/radioThemeRoles.test.ts`
44
+ - Create: `src/components/RadioField/types.ts`
45
+ - Create: `src/components/RadioField/RadioMark.tsx`
46
+ - Create: `src/components/RadioField/RadioField.tsx`
47
+ - Create: `src/components/RadioField/index.ts`
48
+ - Create: `docs/components/radio-field.md`
49
+ - Modify: `src/components/index.ts`
50
+ - Modify: `src/index.ts`
51
+ - Modify: `README.md`
52
+
53
+ **Interfaces:**
54
+ - Consumes: `buttonRadiusValue`, `ButtonRadius` from `../Button/buttonRadius`; `useResolvedFieldColor` from `../Field/useResolvedFieldColor` (same resolve path as Field)
55
+ - Produces: `RadioField`, `RADIO_THEME_ROLES`, `RadioOption`, `RadioFieldProps`
56
+
57
+ - [ ] **Step 1: Write failing role-map test**
58
+
59
+ ```ts
60
+ // src/components/RadioField/radioThemeRoles.test.ts
61
+ import assert from "node:assert/strict";
62
+ import { describe, it } from "node:test";
63
+ import { RADIO_THEME_ROLES } from "./radioThemeRoles.ts";
64
+
65
+ describe("RADIO_THEME_ROLES", () => {
66
+ it("maps colour props to Field roles plus fieldRadioSelected", () => {
67
+ assert.equal(RADIO_THEME_ROLES.backgroundColor, "fieldBackground");
68
+ assert.equal(RADIO_THEME_ROLES.labelColor, "fieldText");
69
+ assert.equal(RADIO_THEME_ROLES.descriptionColor, "fieldDescription");
70
+ assert.equal(RADIO_THEME_ROLES.dividerColor, "fieldBorder");
71
+ assert.equal(RADIO_THEME_ROLES.radioColor, "fieldBorder");
72
+ assert.equal(RADIO_THEME_ROLES.selectedColor, "fieldRadioSelected");
73
+ assert.equal(RADIO_THEME_ROLES.titleColor, "fieldLabel");
74
+ });
75
+ });
76
+ ```
77
+
78
+ - [ ] **Step 2: Run test to verify it fails**
79
+
80
+ Run: `cd libraries/@studio-piot/studio-native-ui && node --experimental-strip-types --test src/components/RadioField/radioThemeRoles.test.ts`
81
+
82
+ Expected: FAIL (module not found)
83
+
84
+ - [ ] **Step 3: Add roles, types, RadioMark, RadioField**
85
+
86
+ ```ts
87
+ // src/components/RadioField/radioThemeRoles.ts
88
+ export const RADIO_THEME_ROLES = {
89
+ backgroundColor: "fieldBackground",
90
+ labelColor: "fieldText",
91
+ descriptionColor: "fieldDescription",
92
+ dividerColor: "fieldBorder",
93
+ radioColor: "fieldBorder",
94
+ selectedColor: "fieldRadioSelected",
95
+ titleColor: "fieldLabel",
96
+ } as const;
97
+
98
+ export type RadioThemeColorProp = keyof typeof RADIO_THEME_ROLES;
99
+ ```
100
+
101
+ ```ts
102
+ // src/components/RadioField/types.ts
103
+ import type { StyleProp, ViewStyle } from "react-native";
104
+ import type { ButtonRadius } from "../Button/buttonRadius";
105
+
106
+ export type RadioOption<T extends string = string> = {
107
+ value: T;
108
+ label: string;
109
+ description?: string;
110
+ disabled?: boolean;
111
+ };
112
+
113
+ export type RadioFieldProps<T extends string = string> = {
114
+ value: T | null;
115
+ onChange: (value: T) => void;
116
+ options: RadioOption<T>[];
117
+ label?: string;
118
+ radius?: ButtonRadius;
119
+ backgroundColor?: string;
120
+ labelColor?: string;
121
+ descriptionColor?: string;
122
+ dividerColor?: string;
123
+ radioColor?: string;
124
+ selectedColor?: string;
125
+ titleColor?: string;
126
+ style?: StyleProp<ViewStyle>;
127
+ };
128
+ ```
129
+
130
+ ```tsx
131
+ // src/components/RadioField/RadioMark.tsx
132
+ import { StyleSheet, View } from "react-native";
133
+
134
+ const SIZE = 22;
135
+ const DOT = 8;
136
+
137
+ export function RadioMark({
138
+ selected,
139
+ radioColor,
140
+ selectedColor,
141
+ holeColor,
142
+ }: {
143
+ selected: boolean;
144
+ radioColor: string;
145
+ selectedColor: string;
146
+ holeColor: string;
147
+ }) {
148
+ if (selected) {
149
+ return (
150
+ <View
151
+ accessibilityElementsHidden
152
+ importantForAccessibility="no"
153
+ style={[
154
+ styles.outer,
155
+ { backgroundColor: selectedColor, borderColor: selectedColor },
156
+ ]}
157
+ >
158
+ <View style={[styles.dot, { backgroundColor: holeColor }]} />
159
+ </View>
160
+ );
161
+ }
162
+ return (
163
+ <View
164
+ accessibilityElementsHidden
165
+ importantForAccessibility="no"
166
+ style={[styles.outer, { borderColor: radioColor }]}
167
+ />
168
+ );
169
+ }
170
+
171
+ const styles = StyleSheet.create({
172
+ outer: {
173
+ width: SIZE,
174
+ height: SIZE,
175
+ borderRadius: SIZE / 2,
176
+ borderWidth: 2,
177
+ alignItems: "center",
178
+ justifyContent: "center",
179
+ },
180
+ dot: {
181
+ width: DOT,
182
+ height: DOT,
183
+ borderRadius: DOT / 2,
184
+ },
185
+ });
186
+ ```
187
+
188
+ ```tsx
189
+ // src/components/RadioField/RadioField.tsx
190
+ import {
191
+ StyleSheet,
192
+ Text,
193
+ TouchableOpacity,
194
+ View,
195
+ } from "react-native";
196
+ import { buttonRadiusValue } from "../Button/buttonRadius";
197
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
198
+ import { RadioMark } from "./RadioMark";
199
+ import { RADIO_THEME_ROLES } from "./radioThemeRoles";
200
+ import type { RadioFieldProps } from "./types";
201
+
202
+ const ROW_PAD_H = 16;
203
+
204
+ export function RadioField<T extends string = string>({
205
+ value,
206
+ onChange,
207
+ options,
208
+ label,
209
+ radius = "xl",
210
+ backgroundColor: backgroundColorProp,
211
+ labelColor: labelColorProp,
212
+ descriptionColor: descriptionColorProp,
213
+ dividerColor: dividerColorProp,
214
+ radioColor: radioColorProp,
215
+ selectedColor: selectedColorProp,
216
+ titleColor: titleColorProp,
217
+ style,
218
+ }: RadioFieldProps<T>) {
219
+ const backgroundColor = useResolvedFieldColor(
220
+ backgroundColorProp,
221
+ RADIO_THEME_ROLES.backgroundColor,
222
+ );
223
+ const labelColor = useResolvedFieldColor(
224
+ labelColorProp,
225
+ RADIO_THEME_ROLES.labelColor,
226
+ );
227
+ const descriptionColor = useResolvedFieldColor(
228
+ descriptionColorProp,
229
+ RADIO_THEME_ROLES.descriptionColor,
230
+ );
231
+ const dividerColor = useResolvedFieldColor(
232
+ dividerColorProp,
233
+ RADIO_THEME_ROLES.dividerColor,
234
+ );
235
+ const radioColor = useResolvedFieldColor(
236
+ radioColorProp,
237
+ RADIO_THEME_ROLES.radioColor,
238
+ );
239
+ const selectedColor = useResolvedFieldColor(
240
+ selectedColorProp,
241
+ RADIO_THEME_ROLES.selectedColor,
242
+ );
243
+ const titleColor = useResolvedFieldColor(
244
+ titleColorProp,
245
+ RADIO_THEME_ROLES.titleColor,
246
+ );
247
+ const borderRadius = buttonRadiusValue(radius);
248
+
249
+ if (options.length === 0) {
250
+ return null;
251
+ }
252
+
253
+ return (
254
+ <View style={[styles.group, style]}>
255
+ {label ? (
256
+ <Text style={[styles.title, { color: titleColor }]}>{label}</Text>
257
+ ) : null}
258
+ <View
259
+ accessibilityRole="radiogroup"
260
+ style={[
261
+ styles.surface,
262
+ { backgroundColor, borderRadius },
263
+ ]}
264
+ >
265
+ {options.map((option, index) => {
266
+ const selected = value === option.value;
267
+ return (
268
+ <View key={option.value}>
269
+ {index > 0 ? (
270
+ <View
271
+ style={[styles.divider, { backgroundColor: dividerColor }]}
272
+ />
273
+ ) : null}
274
+ <TouchableOpacity
275
+ accessibilityLabel={
276
+ option.description
277
+ ? `${option.label}. ${option.description}`
278
+ : option.label
279
+ }
280
+ accessibilityRole="radio"
281
+ accessibilityState={{
282
+ selected,
283
+ disabled: !!option.disabled,
284
+ }}
285
+ activeOpacity={0.75}
286
+ disabled={option.disabled}
287
+ onPress={() => onChange(option.value)}
288
+ style={[styles.row, option.disabled && styles.blocked]}
289
+ >
290
+ <View style={styles.textCol}>
291
+ <Text style={[styles.label, { color: labelColor }]}>
292
+ {option.label}
293
+ </Text>
294
+ {option.description ? (
295
+ <Text
296
+ style={[styles.description, { color: descriptionColor }]}
297
+ >
298
+ {option.description}
299
+ </Text>
300
+ ) : null}
301
+ </View>
302
+ <RadioMark
303
+ holeColor={backgroundColor}
304
+ radioColor={radioColor}
305
+ selected={selected}
306
+ selectedColor={selectedColor}
307
+ />
308
+ </TouchableOpacity>
309
+ </View>
310
+ );
311
+ })}
312
+ </View>
313
+ </View>
314
+ );
315
+ }
316
+
317
+ const styles = StyleSheet.create({
318
+ group: {
319
+ width: "100%",
320
+ gap: 8,
321
+ },
322
+ title: {
323
+ fontSize: 13,
324
+ fontWeight: "600",
325
+ paddingHorizontal: 4,
326
+ },
327
+ surface: {
328
+ width: "100%",
329
+ overflow: "hidden",
330
+ },
331
+ row: {
332
+ minHeight: 52,
333
+ paddingHorizontal: ROW_PAD_H,
334
+ paddingVertical: 10,
335
+ flexDirection: "row",
336
+ alignItems: "center",
337
+ gap: 12,
338
+ },
339
+ blocked: {
340
+ opacity: 0.4,
341
+ },
342
+ textCol: {
343
+ flex: 1,
344
+ gap: 2,
345
+ },
346
+ label: {
347
+ fontSize: 17,
348
+ },
349
+ description: {
350
+ fontSize: 13,
351
+ },
352
+ divider: {
353
+ height: StyleSheet.hairlineWidth,
354
+ },
355
+ });
356
+ ```
357
+
358
+ ```ts
359
+ // src/components/RadioField/index.ts
360
+ export { RadioField } from "./RadioField";
361
+ export { RADIO_THEME_ROLES, type RadioThemeColorProp } from "./radioThemeRoles";
362
+ export type { RadioOption, RadioFieldProps } from "./types";
363
+ ```
364
+
365
+ Wire exports in `src/components/index.ts` and `src/index.ts` the same way as Field/Form.
366
+
367
+ - [ ] **Step 4: Run role test + full package tests and build**
368
+
369
+ Run:
370
+
371
+ ```bash
372
+ cd libraries/@studio-piot/studio-native-ui
373
+ npm test
374
+ npm run build
375
+ ```
376
+
377
+ Expected: all tests pass; `tsc` succeeds; `dist/components/RadioField/` present.
378
+
379
+ - [ ] **Step 5: Write usage docs and README links**
380
+
381
+ Create `docs/components/radio-field.md` covering: peer ThemeProvider, required theme roles (including `fieldRadioSelected`), props table, Appearance-style example with `value` / `onChange` / `options`.
382
+
383
+ Add to `README.md` Docs list:
384
+
385
+ - `[RadioField usage](./docs/components/radio-field.md)`
386
+ - `[Design: RadioField](./docs/superpowers/specs/2026-09-25-radio-field-design.md)`
387
+
388
+ - [ ] **Step 6: Commit** (only if the user asked to commit)
389
+
390
+ ```bash
391
+ git add src/components/RadioField src/components/index.ts src/index.ts docs/components/radio-field.md README.md
392
+ git commit -m "Add RadioField with Field theme roles and selected mark."
393
+ ```
394
+
395
+ ---
396
+
397
+ ### Task 2: Ribe theme role + Appearance migration
398
+
399
+ **Files:**
400
+ - Modify: `projects/ribe/ribe-mobile-app/src/config/colors.ts`
401
+ - Modify: `projects/ribe/ribe-mobile-app/src/config/colors.test.ts`
402
+ - Modify: `projects/ribe/ribe-mobile-app/src/app/(app)/(tabs)/profile/appearance.tsx`
403
+
404
+ **Interfaces:**
405
+ - Consumes: `RadioField` from `@studio-piot/native-ui`; existing `useColorSchemePreference`, `ColorSchemePreference`
406
+ - Produces: Appearance screen driven by `RadioField`; `Colors.*.fieldRadioSelected`
407
+
408
+ - [ ] **Step 1: Add `fieldRadioSelected` to colours + test**
409
+
410
+ In `ThemeColors` and both maps:
411
+
412
+ ```ts
413
+ fieldRadioSelected: string;
414
+ // light:
415
+ fieldRadioSelected: charcoal, // or palette.neutral[900]
416
+ // dark:
417
+ fieldRadioSelected: stone, // or palette.neutral[100]
418
+ ```
419
+
420
+ Update `colors.test.ts` `deepEqual` objects to include the new key with the exact hex (read `charcoal` / `stone` constants — currently `#22201D` / `#EDE7DE`).
421
+
422
+ Run: `cd projects/ribe/ribe-mobile-app && pnpm exec node --experimental-strip-types --test src/config/colors.test.ts`
423
+
424
+ Expected: PASS after both file and test updated together.
425
+
426
+ - [ ] **Step 2: Replace Appearance list with RadioField**
427
+
428
+ ```tsx
429
+ import { styles } from "@/config/styles";
430
+ import {
431
+ useColorSchemePreference,
432
+ type ColorSchemePreference,
433
+ } from "@/hooks/useThemeColor";
434
+ import { RadioField } from "@studio-piot/native-ui";
435
+ import { ScrollView } from "react-native";
436
+
437
+ const OPTIONS: {
438
+ value: ColorSchemePreference;
439
+ label: string;
440
+ description?: string;
441
+ }[] = [
442
+ {
443
+ value: "automatic",
444
+ label: "Automatic",
445
+ description: "Match the appearance settings on your device.",
446
+ },
447
+ { value: "light", label: "Light" },
448
+ { value: "dark", label: "Dark" },
449
+ ];
450
+
451
+ export default function AppearanceScreen() {
452
+ const { preference, setPreference } = useColorSchemePreference();
453
+ // keep parseColorSchemePreference / openedAs behaviour from the current file
454
+ const current = preference || openedAs;
455
+
456
+ return (
457
+ <ScrollView
458
+ contentContainerStyle={styles.content}
459
+ contentInsetAdjustmentBehavior="automatic"
460
+ style={[styles.screen, { backgroundColor: /* useThemeColor("background") */ }]}
461
+ >
462
+ <RadioField
463
+ onChange={setPreference}
464
+ options={OPTIONS}
465
+ value={current}
466
+ />
467
+ </ScrollView>
468
+ );
469
+ }
470
+ ```
471
+
472
+ Preserve existing preference / search-param resolution from the current `appearance.tsx`. Drop the checkmark `Pressable` list and the summary “Automatic” line above it (the radio card is the UI). Ensure `ThemeProvider` already wraps the app (it does).
473
+
474
+ After editing, run package link if needed:
475
+
476
+ ```bash
477
+ cd projects/ribe/ribe-mobile-app
478
+ pnpm install
479
+ pnpm typecheck
480
+ ```
481
+
482
+ Expected: `tsc --noEmit` passes.
483
+
484
+ - [ ] **Step 3: Manual check**
485
+
486
+ - Open Profile → Appearance; three rows in one rounded card; Automatic selected by default when preference is automatic; tap Light/Dark updates scheme and selected radio.
487
+ - Light/dark maps: selected fill uses `fieldRadioSelected`; unselected rings use `fieldBorder`.
488
+
489
+ - [ ] **Step 4: Commit** (only if the user asked)
490
+
491
+ ```bash
492
+ # in ribe-mobile-app
493
+ git add src/config/colors.ts src/config/colors.test.ts src/app/(app)/(tabs)/profile/appearance.tsx
494
+ git commit -m "Use RadioField on Appearance and add fieldRadioSelected."
495
+ ```
496
+
497
+ ---
498
+
499
+ ## Spec coverage check
500
+
501
+ | Spec item | Task |
502
+ |---|---|
503
+ | Single controlled group API | Task 1 |
504
+ | LinkBlocks card layout + metrics | Task 1 |
505
+ | Field roles + `fieldRadioSelected` | Task 1 roles + Task 2 colours |
506
+ | RadioMark Views | Task 1 |
507
+ | a11y radiogroup / radio | Task 1 |
508
+ | Docs / exports | Task 1 |
509
+ | Ribe Appearance migration | Task 2 |
510
+ | Out of scope (multi-group, icons, …) | Not implemented |
511
+
512
+ ## Placeholder / consistency scan
513
+
514
+ - No TBD / “similar to Task N” leftovers
515
+ - Role names match the spec table
516
+ - Props match the approved design (`value: T | null`, `onChange`, `options`, optional `label`)
@@ -0,0 +1,22 @@
1
+ # Select Implementation Plan
2
+
3
+ > **For agentic workers:** Inline execution in session; checkboxes track progress.
4
+
5
+ **Goal:** Ship `Select` (Field-like trigger + TrueSheet options) and wire Ribe create-activity Type.
6
+
7
+ **Architecture:** Controlled `Select` presents `@lodev09/react-native-true-sheet` at detents `[0.25, 0.5, 0.75]` (open index for `0.5`). `closeButton` accepts `ReactNode | ((dismiss) => ReactNode)`. Field theme roles + `fieldRadioSelected` for selected check.
8
+
9
+ **Tech Stack:** TypeScript, RN, TrueSheet peer, Expo Ribe.
10
+
11
+ ## Global Constraints
12
+
13
+ - Spec: `docs/superpowers/specs/2026-09-25-select-design.md`
14
+ - Peer `@lodev09/react-native-true-sheet` (v3.x for Expo 57)
15
+ - No brand in package; no search/icons/grids
16
+
17
+ ## Tasks
18
+
19
+ 1. [x] Package Select + peers + docs + exports + unit test for detent index helper
20
+ 2. [x] Ribe: install TrueSheet, create-activity Type → Select + glass close
21
+
22
+ ---