@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.
- package/LICENSE +15 -0
- package/README.md +91 -0
- package/dist/components/Button/Button.d.ts +16 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +44 -0
- package/dist/components/Button/Button.js.map +1 -0
- package/dist/components/Button/buttonRadius.d.ts +3 -0
- package/dist/components/Button/buttonRadius.d.ts.map +1 -0
- package/dist/components/Button/buttonRadius.js +17 -0
- package/dist/components/Button/buttonRadius.js.map +1 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Button/index.js +8 -0
- package/dist/components/Button/index.js.map +1 -0
- package/dist/components/Field/FieldText.d.ts +3 -0
- package/dist/components/Field/FieldText.d.ts.map +1 -0
- package/dist/components/Field/FieldText.js +9 -0
- package/dist/components/Field/FieldText.js.map +1 -0
- package/dist/components/Field/FieldTextarea.d.ts +3 -0
- package/dist/components/Field/FieldTextarea.d.ts.map +1 -0
- package/dist/components/Field/FieldTextarea.js +10 -0
- package/dist/components/Field/FieldTextarea.js.map +1 -0
- package/dist/components/Field/fieldChrome.d.ts +10 -0
- package/dist/components/Field/fieldChrome.d.ts.map +1 -0
- package/dist/components/Field/fieldChrome.js +100 -0
- package/dist/components/Field/fieldChrome.js.map +1 -0
- package/dist/components/Field/fieldThemeRoles.d.ts +13 -0
- package/dist/components/Field/fieldThemeRoles.d.ts.map +1 -0
- package/dist/components/Field/fieldThemeRoles.js +15 -0
- package/dist/components/Field/fieldThemeRoles.js.map +1 -0
- package/dist/components/Field/index.d.ts +6 -0
- package/dist/components/Field/index.d.ts.map +1 -0
- package/dist/components/Field/index.js +10 -0
- package/dist/components/Field/index.js.map +1 -0
- package/dist/components/Field/resolveFieldBorder.d.ts +16 -0
- package/dist/components/Field/resolveFieldBorder.d.ts.map +1 -0
- package/dist/components/Field/resolveFieldBorder.js +20 -0
- package/dist/components/Field/resolveFieldBorder.js.map +1 -0
- package/dist/components/Field/resolveFieldColor.d.ts +7 -0
- package/dist/components/Field/resolveFieldColor.d.ts.map +1 -0
- package/dist/components/Field/resolveFieldColor.js +21 -0
- package/dist/components/Field/resolveFieldColor.js.map +1 -0
- package/dist/components/Field/types.d.ts +46 -0
- package/dist/components/Field/types.d.ts.map +1 -0
- package/dist/components/Field/types.js +3 -0
- package/dist/components/Field/types.js.map +1 -0
- package/dist/components/Field/useResolvedFieldColor.d.ts +2 -0
- package/dist/components/Field/useResolvedFieldColor.d.ts.map +1 -0
- package/dist/components/Field/useResolvedFieldColor.js +15 -0
- package/dist/components/Field/useResolvedFieldColor.js.map +1 -0
- package/dist/components/Form/Form.d.ts +3 -0
- package/dist/components/Form/Form.d.ts.map +1 -0
- package/dist/components/Form/Form.js +100 -0
- package/dist/components/Form/Form.js.map +1 -0
- package/dist/components/Form/FormCloseButton.d.ts +4 -0
- package/dist/components/Form/FormCloseButton.d.ts.map +1 -0
- package/dist/components/Form/FormCloseButton.js +17 -0
- package/dist/components/Form/FormCloseButton.js.map +1 -0
- package/dist/components/Form/index.d.ts +4 -0
- package/dist/components/Form/index.d.ts.map +1 -0
- package/dist/components/Form/index.js +8 -0
- package/dist/components/Form/index.js.map +1 -0
- package/dist/components/Form/resolveThemeColor.d.ts +11 -0
- package/dist/components/Form/resolveThemeColor.d.ts.map +1 -0
- package/dist/components/Form/resolveThemeColor.js +21 -0
- package/dist/components/Form/resolveThemeColor.js.map +1 -0
- package/dist/components/Form/types.d.ts +30 -0
- package/dist/components/Form/types.d.ts.map +1 -0
- package/dist/components/Form/types.js +3 -0
- package/dist/components/Form/types.js.map +1 -0
- package/dist/components/Form/useResolvedThemeColor.d.ts +2 -0
- package/dist/components/Form/useResolvedThemeColor.d.ts.map +1 -0
- package/dist/components/Form/useResolvedThemeColor.js +15 -0
- package/dist/components/Form/useResolvedThemeColor.js.map +1 -0
- package/dist/components/Form/withAlpha.d.ts +2 -0
- package/dist/components/Form/withAlpha.d.ts.map +1 -0
- package/dist/components/Form/withAlpha.js +12 -0
- package/dist/components/Form/withAlpha.js.map +1 -0
- package/dist/components/LinkBlocks/LinkBlocks.d.ts +3 -0
- package/dist/components/LinkBlocks/LinkBlocks.d.ts.map +1 -0
- package/dist/components/LinkBlocks/LinkBlocks.js +75 -0
- package/dist/components/LinkBlocks/LinkBlocks.js.map +1 -0
- package/dist/components/LinkBlocks/filterGroups.d.ts +3 -0
- package/dist/components/LinkBlocks/filterGroups.d.ts.map +1 -0
- package/dist/components/LinkBlocks/filterGroups.js +7 -0
- package/dist/components/LinkBlocks/filterGroups.js.map +1 -0
- package/dist/components/LinkBlocks/index.d.ts +4 -0
- package/dist/components/LinkBlocks/index.d.ts.map +1 -0
- package/dist/components/LinkBlocks/index.js +8 -0
- package/dist/components/LinkBlocks/index.js.map +1 -0
- package/dist/components/LinkBlocks/types.d.ts +26 -0
- package/dist/components/LinkBlocks/types.d.ts.map +1 -0
- package/dist/components/LinkBlocks/types.js +3 -0
- package/dist/components/LinkBlocks/types.js.map +1 -0
- package/dist/components/RadioField/RadioField.d.ts +3 -0
- package/dist/components/RadioField/RadioField.d.ts.map +1 -0
- package/dist/components/RadioField/RadioField.js +76 -0
- package/dist/components/RadioField/RadioField.js.map +1 -0
- package/dist/components/RadioField/RadioMark.d.ts +7 -0
- package/dist/components/RadioField/RadioMark.d.ts.map +1 -0
- package/dist/components/RadioField/RadioMark.js +32 -0
- package/dist/components/RadioField/RadioMark.js.map +1 -0
- package/dist/components/RadioField/index.d.ts +4 -0
- package/dist/components/RadioField/index.d.ts.map +1 -0
- package/dist/components/RadioField/index.js +8 -0
- package/dist/components/RadioField/index.js.map +1 -0
- package/dist/components/RadioField/radioThemeRoles.d.ts +11 -0
- package/dist/components/RadioField/radioThemeRoles.d.ts.map +1 -0
- package/dist/components/RadioField/radioThemeRoles.js +13 -0
- package/dist/components/RadioField/radioThemeRoles.js.map +1 -0
- package/dist/components/RadioField/types.d.ts +24 -0
- package/dist/components/RadioField/types.d.ts.map +1 -0
- package/dist/components/RadioField/types.js +3 -0
- package/dist/components/RadioField/types.js.map +1 -0
- package/dist/components/Select/Select.d.ts +3 -0
- package/dist/components/Select/Select.d.ts.map +1 -0
- package/dist/components/Select/Select.js +137 -0
- package/dist/components/Select/Select.js.map +1 -0
- package/dist/components/Select/SelectCloseButton.d.ts +4 -0
- package/dist/components/Select/SelectCloseButton.d.ts.map +1 -0
- package/dist/components/Select/SelectCloseButton.js +17 -0
- package/dist/components/Select/SelectCloseButton.js.map +1 -0
- package/dist/components/Select/index.d.ts +6 -0
- package/dist/components/Select/index.d.ts.map +1 -0
- package/dist/components/Select/index.js +14 -0
- package/dist/components/Select/index.js.map +1 -0
- package/dist/components/Select/resolveSelectDetentIndex.d.ts +9 -0
- package/dist/components/Select/resolveSelectDetentIndex.d.ts.map +1 -0
- package/dist/components/Select/resolveSelectDetentIndex.js +16 -0
- package/dist/components/Select/resolveSelectDetentIndex.js.map +1 -0
- package/dist/components/Select/selectThemeRoles.d.ts +12 -0
- package/dist/components/Select/selectThemeRoles.d.ts.map +1 -0
- package/dist/components/Select/selectThemeRoles.js +14 -0
- package/dist/components/Select/selectThemeRoles.js.map +1 -0
- package/dist/components/Select/types.d.ts +35 -0
- package/dist/components/Select/types.d.ts.map +1 -0
- package/dist/components/Select/types.js +3 -0
- package/dist/components/Select/types.js.map +1 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +27 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +6 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/screenPadding.d.ts +3 -0
- package/dist/layout/screenPadding.d.ts.map +1 -0
- package/dist/layout/screenPadding.js +6 -0
- package/dist/layout/screenPadding.js.map +1 -0
- package/dist/theme/ThemeProvider.d.ts +11 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +11 -0
- package/dist/theme/ThemeProvider.js.map +1 -0
- package/dist/theme/index.d.ts +6 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +12 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/resolveColorScheme.d.ts +2 -0
- package/dist/theme/resolveColorScheme.d.ts.map +1 -0
- package/dist/theme/resolveColorScheme.js +7 -0
- package/dist/theme/resolveColorScheme.js.map +1 -0
- package/dist/theme/types.d.ts +8 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +3 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/theme/useColorScheme.d.ts +2 -0
- package/dist/theme/useColorScheme.d.ts.map +1 -0
- package/dist/theme/useColorScheme.js +16 -0
- package/dist/theme/useColorScheme.js.map +1 -0
- package/dist/theme/useThemeColor.d.ts +5 -0
- package/dist/theme/useThemeColor.d.ts.map +1 -0
- package/dist/theme/useThemeColor.js +25 -0
- package/dist/theme/useThemeColor.js.map +1 -0
- package/docs/components/button.md +162 -0
- package/docs/components/field.md +128 -0
- package/docs/components/form.md +86 -0
- package/docs/components/link-blocks.md +120 -0
- package/docs/components/radio-field.md +100 -0
- package/docs/components/select.md +59 -0
- package/docs/superpowers/plans/2026-09-24-field.md +583 -0
- package/docs/superpowers/plans/2026-09-24-link-blocks.md +414 -0
- package/docs/superpowers/plans/2026-09-24-theme-provider-button.md +493 -0
- package/docs/superpowers/plans/2026-09-25-radio-field.md +516 -0
- package/docs/superpowers/plans/2026-09-25-select.md +22 -0
- package/docs/superpowers/specs/2026-09-24-field-design.md +205 -0
- package/docs/superpowers/specs/2026-09-24-link-blocks-design.md +167 -0
- package/docs/superpowers/specs/2026-09-24-theme-provider-button-design.md +178 -0
- package/docs/superpowers/specs/2026-09-25-radio-field-design.md +136 -0
- package/docs/superpowers/specs/2026-09-25-select-design.md +164 -0
- package/package.json +75 -0
- package/src/components/Button/Button.tsx +97 -0
- package/src/components/Button/buttonRadius.test.ts +16 -0
- package/src/components/Button/buttonRadius.ts +24 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Field/FieldText.tsx +6 -0
- package/src/components/Field/FieldTextarea.tsx +18 -0
- package/src/components/Field/fieldChrome.tsx +219 -0
- package/src/components/Field/fieldThemeRoles.ts +13 -0
- package/src/components/Field/index.ts +5 -0
- package/src/components/Field/resolveFieldBorder.test.ts +76 -0
- package/src/components/Field/resolveFieldBorder.ts +40 -0
- package/src/components/Field/resolveFieldColor.test.ts +92 -0
- package/src/components/Field/resolveFieldColor.ts +26 -0
- package/src/components/Field/types.ts +53 -0
- package/src/components/Field/useResolvedFieldColor.ts +15 -0
- package/src/components/Form/Form.tsx +205 -0
- package/src/components/Form/FormCloseButton.tsx +19 -0
- package/src/components/Form/index.ts +3 -0
- package/src/components/Form/resolveThemeColor.ts +24 -0
- package/src/components/Form/types.ts +30 -0
- package/src/components/Form/useResolvedThemeColor.ts +15 -0
- package/src/components/Form/withAlpha.test.ts +13 -0
- package/src/components/Form/withAlpha.ts +9 -0
- package/src/components/LinkBlocks/LinkBlocks.tsx +165 -0
- package/src/components/LinkBlocks/filterGroups.test.ts +37 -0
- package/src/components/LinkBlocks/filterGroups.ts +7 -0
- package/src/components/LinkBlocks/index.ts +7 -0
- package/src/components/LinkBlocks/types.ts +28 -0
- package/src/components/RadioField/RadioField.tsx +168 -0
- package/src/components/RadioField/RadioMark.tsx +54 -0
- package/src/components/RadioField/index.ts +3 -0
- package/src/components/RadioField/radioThemeRoles.test.ts +15 -0
- package/src/components/RadioField/radioThemeRoles.ts +11 -0
- package/src/components/RadioField/types.ts +25 -0
- package/src/components/Select/Select.tsx +279 -0
- package/src/components/Select/SelectCloseButton.tsx +19 -0
- package/src/components/Select/index.ts +13 -0
- package/src/components/Select/resolveSelectDetentIndex.test.ts +20 -0
- package/src/components/Select/resolveSelectDetentIndex.ts +16 -0
- package/src/components/Select/selectThemeRoles.ts +12 -0
- package/src/components/Select/types.ts +39 -0
- package/src/components/index.ts +42 -0
- package/src/index.ts +50 -0
- package/src/layout/index.ts +1 -0
- package/src/layout/screenPadding.ts +2 -0
- package/src/theme/ThemeProvider.tsx +20 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/resolveColorScheme.test.ts +16 -0
- package/src/theme/resolveColorScheme.ts +5 -0
- package/src/theme/types.ts +5 -0
- package/src/theme/useColorScheme.ts +13 -0
- 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`.
|