@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,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
|
+
---
|