@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,583 @@
1
+ # FieldText / FieldTextarea 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 presentational `FieldText` and `FieldTextarea` with shared chrome (label, focus/error border, clear, error text), colour props from the parent, and Button-style radius.
6
+
7
+ **Architecture:** Shared internal `FieldChrome` owns layout and border/focus/clear/error. Thin `FieldText` / `FieldTextarea` pass multiline vs single-line input options. Pure `resolveFieldBorder` (and `shouldShowFieldClear`) keep focus/error rules testable without a React Native renderer. Replace the app-coupled example files under `src/components/Field/`.
8
+
9
+ **Tech Stack:** TypeScript, React 19+, React Native, `tsc` → `dist`, Node test runner for pure helpers.
10
+
11
+ ## Global Constraints
12
+
13
+ - No brand names, palettes, or app role catalogues in the package
14
+ - No icon library dependencies; clear icon is optional `React.ReactNode`
15
+ - Field components do not call `useThemeColor` — colours are required props
16
+ - Controlled only: `value` + `onChangeText` required
17
+ - Width always `100%` of parent; radius reuses `ButtonRadius` (default `"xl"`)
18
+ - Focus: `borderWidth` 1 → 2 with padding compensation; error colour wins over focused colour; double stroke still applies when focused + error
19
+ - Do not invert `editable` (pass through to `TextInput` as given)
20
+ - Spec: `docs/superpowers/specs/2026-09-24-field-design.md`
21
+ - Commit only when the user asks
22
+
23
+ ## File map
24
+
25
+ | File | Responsibility |
26
+ |---|---|
27
+ | `src/components/Field/types.ts` | Shared props types |
28
+ | `src/components/Field/resolveFieldBorder.ts` | Pure border colour / width / padding |
29
+ | `src/components/Field/resolveFieldBorder.test.ts` | Node tests for border + clear visibility |
30
+ | `src/components/Field/fieldChrome.tsx` | Shared UI chrome (internal) |
31
+ | `src/components/Field/FieldText.tsx` | Single-line public component |
32
+ | `src/components/Field/FieldTextarea.tsx` | Multiline public component |
33
+ | `src/components/Field/index.ts` | Barrel |
34
+ | Delete | `Field.tsx`, `FieldLabel.tsx`, `FieldInput.tsx`, `FieldError.tsx` (example leftovers) |
35
+ | `src/components/index.ts` | Re-export |
36
+ | `src/index.ts` | Public export |
37
+ | `docs/components/field.md` | Usage |
38
+ | `README.md` | Link to usage + design |
39
+
40
+ ---
41
+
42
+ ### Task 1: Types, border helper, FieldChrome, FieldText, FieldTextarea, exports
43
+
44
+ **Files:**
45
+ - Create: `src/components/Field/types.ts`
46
+ - Create: `src/components/Field/resolveFieldBorder.ts`
47
+ - Create: `src/components/Field/resolveFieldBorder.test.ts`
48
+ - Create: `src/components/Field/fieldChrome.tsx`
49
+ - Create: `src/components/Field/FieldText.tsx`
50
+ - Create: `src/components/Field/FieldTextarea.tsx`
51
+ - Create: `src/components/Field/index.ts`
52
+ - Delete: `src/components/Field/Field.tsx`, `FieldLabel.tsx`, `FieldInput.tsx`, `FieldError.tsx` (and overwrite old example `FieldTextarea.tsx`)
53
+ - Modify: `src/components/index.ts`
54
+ - Modify: `src/index.ts`
55
+
56
+ **Interfaces:**
57
+ - Consumes: `buttonRadiusValue`, `ButtonRadius` from `../Button/buttonRadius`
58
+ - Produces: `FieldText`, `FieldTextarea`, `FieldTextProps`, `FieldTextareaProps`, `FieldSharedProps`, `resolveFieldBorder`, `shouldShowFieldClear`
59
+
60
+ - [ ] **Step 1: Write failing border / clear tests**
61
+
62
+ ```ts
63
+ // src/components/Field/resolveFieldBorder.test.ts
64
+ import assert from "node:assert/strict";
65
+ import { describe, it } from "node:test";
66
+ import {
67
+ resolveFieldBorder,
68
+ shouldShowFieldClear,
69
+ } from "./resolveFieldBorder.ts";
70
+
71
+ const colors = {
72
+ borderColor: "#aaa",
73
+ focusedBorderColor: "#00f",
74
+ errorColor: "#f00",
75
+ };
76
+
77
+ describe("resolveFieldBorder", () => {
78
+ it("resting: width 1 and borderColor", () => {
79
+ const next = resolveFieldBorder({
80
+ isFocused: false,
81
+ hasError: false,
82
+ ...colors,
83
+ });
84
+ assert.equal(next.borderWidth, 1);
85
+ assert.equal(next.borderColor, "#aaa");
86
+ assert.equal(next.paddingHorizontal, 14);
87
+ });
88
+
89
+ it("focused without error: width 2, focused colour, padding -1", () => {
90
+ const next = resolveFieldBorder({
91
+ isFocused: true,
92
+ hasError: false,
93
+ ...colors,
94
+ });
95
+ assert.equal(next.borderWidth, 2);
96
+ assert.equal(next.borderColor, "#00f");
97
+ assert.equal(next.paddingHorizontal, 13);
98
+ });
99
+
100
+ it("error without focus: width 1, error colour", () => {
101
+ const next = resolveFieldBorder({
102
+ isFocused: false,
103
+ hasError: true,
104
+ ...colors,
105
+ });
106
+ assert.equal(next.borderWidth, 1);
107
+ assert.equal(next.borderColor, "#f00");
108
+ assert.equal(next.paddingHorizontal, 14);
109
+ });
110
+
111
+ it("focused + error: width 2, error colour wins", () => {
112
+ const next = resolveFieldBorder({
113
+ isFocused: true,
114
+ hasError: true,
115
+ ...colors,
116
+ });
117
+ assert.equal(next.borderWidth, 2);
118
+ assert.equal(next.borderColor, "#f00");
119
+ assert.equal(next.paddingHorizontal, 13);
120
+ });
121
+ });
122
+
123
+ describe("shouldShowFieldClear", () => {
124
+ it("false when value empty", () => {
125
+ assert.equal(shouldShowFieldClear("", true), false);
126
+ });
127
+
128
+ it("false when no clear icon", () => {
129
+ assert.equal(shouldShowFieldClear("hi", false), false);
130
+ });
131
+
132
+ it("true when value and clear icon", () => {
133
+ assert.equal(shouldShowFieldClear("hi", true), true);
134
+ });
135
+ });
136
+ ```
137
+
138
+ - [ ] **Step 2: Run — expect fail**
139
+
140
+ ```bash
141
+ node --experimental-strip-types --test src/components/Field/resolveFieldBorder.test.ts
142
+ ```
143
+
144
+ Expected: FAIL (module not found).
145
+
146
+ - [ ] **Step 3: Add types + resolve helpers**
147
+
148
+ ```ts
149
+ // src/components/Field/types.ts
150
+ import type { ReactNode, Ref } from "react";
151
+ import type {
152
+ StyleProp,
153
+ TextInputProps,
154
+ TextStyle,
155
+ ViewStyle,
156
+ } from "react-native";
157
+ import type { ButtonRadius } from "../Button/buttonRadius";
158
+
159
+ export type FieldSharedProps = {
160
+ value: string;
161
+ onChangeText: (text: string) => void;
162
+ borderColor: string;
163
+ focusedBorderColor: string;
164
+ errorColor: string;
165
+ textColor: string;
166
+ labelColor: string;
167
+ placeholderColor: string;
168
+ backgroundColor: string;
169
+ label?: string;
170
+ error?: string;
171
+ placeholder?: string;
172
+ radius?: ButtonRadius;
173
+ editable?: boolean;
174
+ clearIcon?: ReactNode;
175
+ onClear?: () => void;
176
+ style?: StyleProp<ViewStyle>;
177
+ inputStyle?: StyleProp<TextStyle>;
178
+ /** Forwarded to TextInput; do not invert. */
179
+ secureTextEntry?: boolean;
180
+ keyboardType?: TextInputProps["keyboardType"];
181
+ autoCapitalize?: TextInputProps["autoCapitalize"];
182
+ autoComplete?: TextInputProps["autoComplete"];
183
+ textContentType?: TextInputProps["textContentType"];
184
+ onFocus?: TextInputProps["onFocus"];
185
+ onBlur?: TextInputProps["onBlur"];
186
+ ref?: Ref<import("react-native").TextInput>;
187
+ };
188
+
189
+ export type FieldTextProps = FieldSharedProps;
190
+
191
+ export type FieldTextareaProps = FieldSharedProps & {
192
+ numberOfLines?: number;
193
+ };
194
+ ```
195
+
196
+ ```ts
197
+ // src/components/Field/resolveFieldBorder.ts
198
+ const RESTING_PADDING_HORIZONTAL = 14;
199
+
200
+ export type ResolveFieldBorderInput = {
201
+ isFocused: boolean;
202
+ hasError: boolean;
203
+ borderColor: string;
204
+ focusedBorderColor: string;
205
+ errorColor: string;
206
+ restingPaddingHorizontal?: number;
207
+ };
208
+
209
+ export type ResolveFieldBorderResult = {
210
+ borderColor: string;
211
+ borderWidth: number;
212
+ paddingHorizontal: number;
213
+ };
214
+
215
+ export function resolveFieldBorder(
216
+ input: ResolveFieldBorderInput,
217
+ ): ResolveFieldBorderResult {
218
+ const resting =
219
+ input.restingPaddingHorizontal ?? RESTING_PADDING_HORIZONTAL;
220
+ const borderWidth = input.isFocused ? 2 : 1;
221
+ const borderColor = input.hasError
222
+ ? input.errorColor
223
+ : input.isFocused
224
+ ? input.focusedBorderColor
225
+ : input.borderColor;
226
+ const paddingHorizontal = input.isFocused ? resting - 1 : resting;
227
+
228
+ return { borderColor, borderWidth, paddingHorizontal };
229
+ }
230
+
231
+ export function shouldShowFieldClear(
232
+ value: string,
233
+ hasClearIcon: boolean,
234
+ ): boolean {
235
+ return value.length > 0 && hasClearIcon;
236
+ }
237
+ ```
238
+
239
+ - [ ] **Step 4: Run tests — expect pass**
240
+
241
+ ```bash
242
+ node --experimental-strip-types --test src/components/Field/resolveFieldBorder.test.ts
243
+ ```
244
+
245
+ Expected: PASS (all resolve + clear cases).
246
+
247
+ - [ ] **Step 5: Implement FieldChrome (internal)**
248
+
249
+ ```tsx
250
+ // src/components/Field/fieldChrome.tsx
251
+ import { useState, type ReactNode } from "react";
252
+ import {
253
+ StyleSheet,
254
+ Text,
255
+ TextInput,
256
+ TouchableOpacity,
257
+ View,
258
+ type StyleProp,
259
+ type TextInputProps,
260
+ type TextStyle,
261
+ type ViewStyle,
262
+ } from "react-native";
263
+ import { buttonRadiusValue, type ButtonRadius } from "../Button/buttonRadius";
264
+ import {
265
+ resolveFieldBorder,
266
+ shouldShowFieldClear,
267
+ } from "./resolveFieldBorder";
268
+ import type { FieldSharedProps } from "./types";
269
+
270
+ export type FieldChromeProps = FieldSharedProps & {
271
+ multiline?: boolean;
272
+ numberOfLines?: number;
273
+ minHeight?: number;
274
+ textAlignVertical?: TextInputProps["textAlignVertical"];
275
+ };
276
+
277
+ export function FieldChrome({
278
+ value,
279
+ onChangeText,
280
+ borderColor,
281
+ focusedBorderColor,
282
+ errorColor,
283
+ textColor,
284
+ labelColor,
285
+ placeholderColor,
286
+ backgroundColor,
287
+ label,
288
+ error,
289
+ placeholder,
290
+ radius = "xl",
291
+ editable = true,
292
+ clearIcon,
293
+ onClear,
294
+ style,
295
+ inputStyle,
296
+ multiline = false,
297
+ numberOfLines,
298
+ minHeight = 48,
299
+ textAlignVertical,
300
+ secureTextEntry,
301
+ keyboardType,
302
+ autoCapitalize,
303
+ autoComplete,
304
+ textContentType,
305
+ onFocus,
306
+ onBlur,
307
+ ref,
308
+ }: FieldChromeProps) {
309
+ const [isFocused, setIsFocused] = useState(false);
310
+ const hasError = Boolean(error);
311
+ const showClear = shouldShowFieldClear(value, clearIcon != null);
312
+ const border = resolveFieldBorder({
313
+ isFocused,
314
+ hasError,
315
+ borderColor,
316
+ focusedBorderColor,
317
+ errorColor,
318
+ });
319
+ const borderRadius = buttonRadiusValue(radius);
320
+
321
+ const handleClear = () => {
322
+ if (onClear) onClear();
323
+ else onChangeText("");
324
+ };
325
+
326
+ return (
327
+ <View style={[styles.column, !editable && styles.blocked, style]}>
328
+ {label ? (
329
+ <Text style={[styles.label, { color: labelColor }]}>{label}</Text>
330
+ ) : null}
331
+ <View style={styles.inputWrapper}>
332
+ <TextInput
333
+ ref={ref}
334
+ value={value}
335
+ onChangeText={onChangeText}
336
+ placeholder={placeholder}
337
+ placeholderTextColor={placeholderColor}
338
+ editable={editable}
339
+ multiline={multiline}
340
+ numberOfLines={numberOfLines}
341
+ textAlignVertical={textAlignVertical}
342
+ secureTextEntry={secureTextEntry}
343
+ keyboardType={keyboardType}
344
+ autoCapitalize={autoCapitalize}
345
+ autoComplete={autoComplete}
346
+ textContentType={textContentType}
347
+ accessibilityLabel={label}
348
+ accessibilityState={{ disabled: !editable }}
349
+ onFocus={(e) => {
350
+ setIsFocused(true);
351
+ onFocus?.(e);
352
+ }}
353
+ onBlur={(e) => {
354
+ setIsFocused(false);
355
+ onBlur?.(e);
356
+ }}
357
+ style={[
358
+ styles.input,
359
+ {
360
+ minHeight,
361
+ color: textColor,
362
+ backgroundColor,
363
+ borderRadius,
364
+ borderColor: border.borderColor,
365
+ borderWidth: border.borderWidth,
366
+ paddingHorizontal: border.paddingHorizontal,
367
+ paddingRight: showClear
368
+ ? border.paddingHorizontal + 28
369
+ : border.paddingHorizontal,
370
+ },
371
+ multiline && styles.multiline,
372
+ inputStyle,
373
+ ]}
374
+ />
375
+ {showClear ? (
376
+ <TouchableOpacity
377
+ accessibilityLabel="Clear"
378
+ accessibilityRole="button"
379
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
380
+ onPress={handleClear}
381
+ style={styles.clearButton}
382
+ >
383
+ {clearIcon}
384
+ </TouchableOpacity>
385
+ ) : null}
386
+ </View>
387
+ {error ? (
388
+ <Text style={[styles.error, { color: errorColor }]}>{error}</Text>
389
+ ) : null}
390
+ </View>
391
+ );
392
+ }
393
+
394
+ const styles = StyleSheet.create({
395
+ column: {
396
+ width: "100%",
397
+ gap: 6,
398
+ },
399
+ blocked: {
400
+ opacity: 0.4,
401
+ },
402
+ label: {
403
+ fontSize: 13,
404
+ fontWeight: "600",
405
+ },
406
+ inputWrapper: {
407
+ position: "relative",
408
+ width: "100%",
409
+ justifyContent: "center",
410
+ },
411
+ input: {
412
+ width: "100%",
413
+ paddingVertical: 12,
414
+ fontSize: 17,
415
+ },
416
+ multiline: {
417
+ paddingTop: 16,
418
+ paddingBottom: 16,
419
+ },
420
+ clearButton: {
421
+ position: "absolute",
422
+ right: 8,
423
+ top: 0,
424
+ bottom: 0,
425
+ justifyContent: "center",
426
+ alignItems: "center",
427
+ zIndex: 2,
428
+ },
429
+ error: {
430
+ fontSize: 13,
431
+ },
432
+ });
433
+ ```
434
+
435
+ - [ ] **Step 6: Implement FieldText and FieldTextarea; barrel; delete leftovers**
436
+
437
+ ```tsx
438
+ // src/components/Field/FieldText.tsx
439
+ import { FieldChrome } from "./fieldChrome";
440
+ import type { FieldTextProps } from "./types";
441
+
442
+ export function FieldText(props: FieldTextProps) {
443
+ return <FieldChrome {...props} multiline={false} minHeight={48} />;
444
+ }
445
+ ```
446
+
447
+ ```tsx
448
+ // src/components/Field/FieldTextarea.tsx
449
+ import { FieldChrome } from "./fieldChrome";
450
+ import type { FieldTextareaProps } from "./types";
451
+
452
+ export function FieldTextarea({
453
+ numberOfLines = 4,
454
+ ...props
455
+ }: FieldTextareaProps) {
456
+ const minHeight = Math.max(100, numberOfLines * 24);
457
+ return (
458
+ <FieldChrome
459
+ {...props}
460
+ multiline
461
+ numberOfLines={numberOfLines}
462
+ minHeight={minHeight}
463
+ textAlignVertical="top"
464
+ />
465
+ );
466
+ }
467
+ ```
468
+
469
+ ```ts
470
+ // src/components/Field/index.ts
471
+ export { FieldText } from "./FieldText";
472
+ export { FieldTextarea } from "./FieldTextarea";
473
+ export type { FieldSharedProps, FieldTextProps, FieldTextareaProps } from "./types";
474
+ ```
475
+
476
+ Delete these example files if still present:
477
+
478
+ - `src/components/Field/Field.tsx`
479
+ - `src/components/Field/FieldLabel.tsx`
480
+ - `src/components/Field/FieldInput.tsx`
481
+ - `src/components/Field/FieldError.tsx`
482
+
483
+ Update exports:
484
+
485
+ ```ts
486
+ // src/components/index.ts — add alongside existing exports
487
+ export {
488
+ FieldText,
489
+ FieldTextarea,
490
+ type FieldSharedProps,
491
+ type FieldTextProps,
492
+ type FieldTextareaProps,
493
+ } from "./Field";
494
+ ```
495
+
496
+ ```ts
497
+ // src/index.ts — add alongside existing exports
498
+ export {
499
+ FieldText,
500
+ FieldTextarea,
501
+ type FieldSharedProps,
502
+ type FieldTextProps,
503
+ type FieldTextareaProps,
504
+ } from "./components/Field";
505
+ ```
506
+
507
+ - [ ] **Step 7: Run full tests + build**
508
+
509
+ ```bash
510
+ npm test
511
+ npm run build
512
+ ```
513
+
514
+ Expected: all existing tests + new Field tests pass; `dist/components/Field/` emitted with `FieldText`, `FieldTextarea`, helpers; no leftover example modules required by the build.
515
+
516
+ - [ ] **Step 8: Commit** (only if the user asked)
517
+
518
+ ```bash
519
+ git add src/components/Field src/components/index.ts src/index.ts
520
+ git commit -m "Add FieldText and FieldTextarea with shared chrome."
521
+ ```
522
+
523
+ ---
524
+
525
+ ### Task 2: Usage docs and README
526
+
527
+ **Files:**
528
+ - Create: `docs/components/field.md`
529
+ - Modify: `README.md`
530
+
531
+ **Interfaces:**
532
+ - Consumes: public API from Task 1
533
+ - Produces: usage doc linked from README
534
+
535
+ - [ ] **Step 1: Write usage doc**
536
+
537
+ Create `docs/components/field.md` covering:
538
+
539
+ - Presentational contract (colours in, no theme lookup inside Field)
540
+ - Example for `FieldText` with label, error, `clearIcon`
541
+ - Example for `FieldTextarea`
542
+ - Colour prop table
543
+ - Focus / error / clear behaviour summary
544
+ - Link to design spec `../superpowers/specs/2026-09-24-field-design.md`
545
+ - Link to theme/Button usage for `useThemeColor`
546
+
547
+ Use the app example from the design spec as the primary snippet (adapt imports to `@studio-piot/native-ui`).
548
+
549
+ - [ ] **Step 2: Link from README**
550
+
551
+ Under Docs, add:
552
+
553
+ ```markdown
554
+ - [Field usage](./docs/components/field.md)
555
+ - [Design: Field](./docs/superpowers/specs/2026-09-24-field-design.md)
556
+ ```
557
+
558
+ - [ ] **Step 3: Commit** (only if the user asked)
559
+
560
+ ```bash
561
+ git add docs/components/field.md README.md
562
+ git commit -m "Document FieldText and FieldTextarea usage."
563
+ ```
564
+
565
+ ---
566
+
567
+ ## Self-review (plan vs spec)
568
+
569
+ | Spec requirement | Task |
570
+ |---|---|
571
+ | FieldText + FieldTextarea single-control API | Task 1 |
572
+ | Shared chrome | Task 1 (`fieldChrome.tsx`) |
573
+ | Required colour props | Task 1 (`types.ts`) |
574
+ | Focus double stroke + padding compensation | Task 1 (`resolveFieldBorder`) |
575
+ | Error under field; error colour; focused+error | Task 1 |
576
+ | Clear when value + clearIcon; onClear default | Task 1 |
577
+ | ButtonRadius default xl | Task 1 |
578
+ | Full width; disabled opacity; editable not inverted | Task 1 |
579
+ | Remove example leftovers | Task 1 |
580
+ | Usage docs + README | Task 2 |
581
+ | No uncontrolled / validation / icon libs | Out of scope (not in tasks) |
582
+
583
+ No placeholders remaining. Types named consistently: `FieldSharedProps`, `FieldTextProps`, `FieldTextareaProps`, `resolveFieldBorder`, `shouldShowFieldClear`.