@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,164 @@
|
|
|
1
|
+
# Select — design
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-25
|
|
4
|
+
Package: `@studio-piot/native-ui`
|
|
5
|
+
Status: approved
|
|
6
|
+
Apps: Ribe create-activity Type field in the same pass
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Ship a presentational **Select**: Field-like trigger that opens a native bottom sheet of options (TrueSheet). Highlight the selected option; choosing one updates value and dismisses the sheet. Drag handle, dimmed backdrop, and snap heights feel like system UI (Strava-style grabber). Ribe wires Type of activity to Select with a glass close control.
|
|
11
|
+
|
|
12
|
+
## Decisions
|
|
13
|
+
|
|
14
|
+
| Topic | Choice |
|
|
15
|
+
|---|---|
|
|
16
|
+
| API shape | Single `Select`: trigger + internal sheet |
|
|
17
|
+
| Sheet library | `@lodev09/react-native-true-sheet` (native UIKit / Material) |
|
|
18
|
+
| Detents | Default `[0.25, 0.5, 0.75]`; present at `0.5` |
|
|
19
|
+
| Close control | `closeButton` slot; package default plain Cancel; Ribe passes glass |
|
|
20
|
+
| Colours | Field `field*` roles + `fieldRadioSelected` for selected check / emphasis |
|
|
21
|
+
| Trigger | Field-like chrome (label, description, error, chevron, radius) |
|
|
22
|
+
| Options | `{ value, label, disabled? }[]`; scrollable when long |
|
|
23
|
+
| Select behaviour | Tap option → `onChange(value)` + dismiss |
|
|
24
|
+
| Icons / search / grids | Out of scope |
|
|
25
|
+
| Optimum migration | Out of scope this pass (CustomPicker remains until a later PR) |
|
|
26
|
+
| Brand coupling | None in the package |
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Select
|
|
32
|
+
├── trigger (Field-like Pressable)
|
|
33
|
+
└── TrueSheet
|
|
34
|
+
├── grabber (system)
|
|
35
|
+
├── header (title + closeButton)
|
|
36
|
+
└── scrollable options
|
|
37
|
+
└── row (label + check when selected)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Public export: `Select` (+ types). Default close control may be a small internal component (plain Cancel), not required to be public.
|
|
41
|
+
- Controlled only: `value` + `onChange` + `options`.
|
|
42
|
+
- Peer dependency: `@lodev09/react-native-true-sheet` (document in usage docs; matching `devDependency` for types/build).
|
|
43
|
+
|
|
44
|
+
## Theme roles
|
|
45
|
+
|
|
46
|
+
| Usage | Theme role |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Trigger / sheet list ground | `fieldBackground` |
|
|
49
|
+
| Trigger value / option label | `fieldText` |
|
|
50
|
+
| Placeholder / chevron | `fieldPlaceholder` |
|
|
51
|
+
| Label above trigger | `fieldLabel` |
|
|
52
|
+
| Description under trigger | `fieldDescription` |
|
|
53
|
+
| Trigger border / dividers | `fieldBorder` |
|
|
54
|
+
| Error border + message | `fieldError` |
|
|
55
|
+
| Selected option emphasis / check | `fieldRadioSelected` |
|
|
56
|
+
|
|
57
|
+
Optional colour override props follow the Field pattern (explicit prop wins over theme role).
|
|
58
|
+
|
|
59
|
+
## Props
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
type SelectOption = {
|
|
63
|
+
value: string;
|
|
64
|
+
label: string;
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type SelectProps = {
|
|
69
|
+
value: string | null;
|
|
70
|
+
onChange: (value: string) => void;
|
|
71
|
+
options: SelectOption[];
|
|
72
|
+
placeholder?: string;
|
|
73
|
+
label?: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
error?: string;
|
|
76
|
+
sheetTitle?: string;
|
|
77
|
+
closeButton?: ReactNode;
|
|
78
|
+
detents?: number[];
|
|
79
|
+
initialDetent?: number;
|
|
80
|
+
radius?: ButtonRadius;
|
|
81
|
+
editable?: boolean;
|
|
82
|
+
// optional colour overrides…
|
|
83
|
+
style?: StyleProp<ViewStyle>;
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Prop | Notes |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `value` | Controlled; `null` / unmatched → show placeholder |
|
|
90
|
+
| `onChange` | Fired with option `value` on row press |
|
|
91
|
+
| `options` | Empty list: trigger still opens an empty sheet (or no-op open — prefer open with empty state text “No options”) |
|
|
92
|
+
| `sheetTitle` | Header title; default `label` or `"Select"` |
|
|
93
|
+
| `closeButton` | Render prop / node for header trailing close; default plain Cancel that dismisses |
|
|
94
|
+
| `detents` | Default `[0.25, 0.5, 0.75]` |
|
|
95
|
+
| `initialDetent` | Default `0.5` (must be one of `detents`) |
|
|
96
|
+
| `radius` | Trigger radius; default `"xl"` |
|
|
97
|
+
| `editable` | `false` → reduced opacity, no open |
|
|
98
|
+
|
|
99
|
+
## Trigger layout
|
|
100
|
+
|
|
101
|
+
Aligned with Field:
|
|
102
|
+
|
|
103
|
+
- Full width; optional label above, description / error below (error replaces description when set)
|
|
104
|
+
- Pressable row: value or placeholder, trailing chevron
|
|
105
|
+
- Resting border `fieldBorder`; error uses `fieldError`
|
|
106
|
+
- Radius via `buttonRadiusValue` (default `"xl"`)
|
|
107
|
+
|
|
108
|
+
## Sheet behaviour
|
|
109
|
+
|
|
110
|
+
- Present TrueSheet on trigger press (when `editable`)
|
|
111
|
+
- System grabber; dimmed / fading backdrop (library default)
|
|
112
|
+
- Header: title + `closeButton` (or default Cancel)
|
|
113
|
+
- Scrollable option list; row min height ~52; selected row uses `fieldRadioSelected` for check (and optional label tint)
|
|
114
|
+
- Row press: `onChange` + dismiss
|
|
115
|
+
- Dismiss also via grabber drag / backdrop / close control
|
|
116
|
+
- No search field, no category grids, no leading sport icons in this pass
|
|
117
|
+
|
|
118
|
+
## Accessibility
|
|
119
|
+
|
|
120
|
+
- Trigger: `accessibilityRole="button"`, label from `label` / placeholder; announce current value when set
|
|
121
|
+
- Options: `accessibilityRole="button"` (or `menuitem` if we use a menu pattern — prefer button + `accessibilityState.selected`)
|
|
122
|
+
- Close control: button with accessible name “Cancel” / “Close”
|
|
123
|
+
|
|
124
|
+
## App work (Ribe)
|
|
125
|
+
|
|
126
|
+
1. `npx expo install @lodev09/react-native-true-sheet` (or pnpm equivalent)
|
|
127
|
+
2. Create-activity: replace Type `FieldText` with `Select`
|
|
128
|
+
- Options: Ride, Run, Swim, Walk, Hike (values lowercase slug or same as label — pick stable string values e.g. `ride`, `run`, …)
|
|
129
|
+
- `error` when empty (same validation copy intent as today)
|
|
130
|
+
- `closeButton` that dismisses the sheet using glass `FormCloseButton` (needs a dismiss callback from Select — either `closeButton` as render prop `(dismiss) => ReactNode` **or** Select clones/passes dismiss via slot convention)
|
|
131
|
+
|
|
132
|
+
### Close slot + dismiss
|
|
133
|
+
|
|
134
|
+
Prefer **render prop** so the app can wire glass Cancel correctly:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
closeButton?: ReactNode | ((dismiss: () => void) => ReactNode);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If `ReactNode` only, the default Cancel works but glass Cancel cannot call dismiss unless Select exposes an imperative API. **Decision: support function form** `(dismiss) => ReactNode`, and also accept a static `ReactNode` for simple cases (static node cannot dismiss unless it uses an internal context — for static nodes, wrap with Pressable only in default Cancel).
|
|
141
|
+
|
|
142
|
+
Ribe usage:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
closeButton={(dismiss) => <FormCloseButton onPress={dismiss} />}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
3. Keep Name / Notes as Field; form submit still gated on name (and optionally require type — keep current “type empty → error” UX).
|
|
149
|
+
|
|
150
|
+
## Out of scope
|
|
151
|
+
|
|
152
|
+
- Strava “Top Sports” horizontal chips / search
|
|
153
|
+
- Per-option icons
|
|
154
|
+
- Migrating Optimum `CustomPicker` call sites
|
|
155
|
+
- Multi-select
|
|
156
|
+
- Liquid glass Cancel inside the package
|
|
157
|
+
- New `select*` theme roles
|
|
158
|
+
|
|
159
|
+
## Success criteria
|
|
160
|
+
|
|
161
|
+
- Package exports `Select` with usage docs and TrueSheet peer documented
|
|
162
|
+
- Sheet: grabber, fade backdrop, detents ¼ / ½ / ¾, open at ½, scrollable options, select-to-dismiss
|
|
163
|
+
- Ribe create-activity Type uses Select + glass close
|
|
164
|
+
- `npm test` / `npm run build` (package) and Ribe `pnpm typecheck` pass
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studio-piot/native-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared React Native components for Studio Piot apps.",
|
|
5
|
+
"homepage": "https://github.com/Studio-Piot/studio-native-ui#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/Studio-Piot/studio-native-ui/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Studio-Piot/studio-native-ui.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "Guillaume Piot",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"react-native": "./dist/index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"react-native": "./dist/index.js",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./components": {
|
|
27
|
+
"types": "./dist/components/index.d.ts",
|
|
28
|
+
"react-native": "./dist/components/index.js",
|
|
29
|
+
"import": "./dist/components/index.js",
|
|
30
|
+
"require": "./dist/components/index.js",
|
|
31
|
+
"default": "./dist/components/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"typesVersions": {
|
|
35
|
+
"*": {
|
|
36
|
+
"*": [
|
|
37
|
+
"./dist/*",
|
|
38
|
+
"./dist/index.d.ts"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"src",
|
|
45
|
+
"docs",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@lodev09/react-native-true-sheet": ">=3.7.0",
|
|
54
|
+
"react": ">=19.0.0",
|
|
55
|
+
"react-native": ">=0.76.0",
|
|
56
|
+
"react-native-keyboard-controller": ">=1.14.0",
|
|
57
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"test": "node --experimental-strip-types --test 'src/**/*.test.ts'",
|
|
61
|
+
"build": "tsc",
|
|
62
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
63
|
+
"dev": "tsc --watch",
|
|
64
|
+
"prepare": "tsc",
|
|
65
|
+
"prepublishOnly": "npm run build"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@lodev09/react-native-true-sheet": "3.11.15",
|
|
69
|
+
"@types/react": "^19.2.2",
|
|
70
|
+
"react-native": "^0.86.3",
|
|
71
|
+
"react-native-keyboard-controller": "1.21.9",
|
|
72
|
+
"react-native-safe-area-context": "~5.7.0",
|
|
73
|
+
"typescript": "^5.9.3"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActivityIndicator,
|
|
3
|
+
StyleSheet,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { buttonRadiusValue, type ButtonRadius } from "./buttonRadius";
|
|
11
|
+
|
|
12
|
+
export type ButtonProps = {
|
|
13
|
+
label: string;
|
|
14
|
+
onPress: () => void;
|
|
15
|
+
backgroundColor: string;
|
|
16
|
+
labelColor: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
variant?: "filled" | "outline";
|
|
20
|
+
radius?: ButtonRadius;
|
|
21
|
+
style?: StyleProp<ViewStyle>;
|
|
22
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function Button({
|
|
26
|
+
label,
|
|
27
|
+
onPress,
|
|
28
|
+
backgroundColor,
|
|
29
|
+
labelColor,
|
|
30
|
+
disabled = false,
|
|
31
|
+
loading = false,
|
|
32
|
+
variant = "filled",
|
|
33
|
+
radius = "xl",
|
|
34
|
+
style,
|
|
35
|
+
labelStyle,
|
|
36
|
+
}: ButtonProps) {
|
|
37
|
+
const blocked = disabled || loading;
|
|
38
|
+
const isOutline = variant === "outline";
|
|
39
|
+
const borderRadius = buttonRadiusValue(radius);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<TouchableOpacity
|
|
43
|
+
accessibilityLabel={label}
|
|
44
|
+
accessibilityRole="button"
|
|
45
|
+
accessibilityState={{ disabled: blocked, busy: loading }}
|
|
46
|
+
activeOpacity={0.75}
|
|
47
|
+
disabled={blocked}
|
|
48
|
+
onPress={onPress}
|
|
49
|
+
style={[
|
|
50
|
+
styles.shell,
|
|
51
|
+
{ borderRadius },
|
|
52
|
+
isOutline
|
|
53
|
+
? {
|
|
54
|
+
backgroundColor: "transparent",
|
|
55
|
+
borderWidth: 1,
|
|
56
|
+
borderColor: backgroundColor,
|
|
57
|
+
}
|
|
58
|
+
: { backgroundColor },
|
|
59
|
+
blocked && styles.blocked,
|
|
60
|
+
style,
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
{loading ? (
|
|
64
|
+
<ActivityIndicator
|
|
65
|
+
color={isOutline ? backgroundColor : labelColor}
|
|
66
|
+
size="small"
|
|
67
|
+
/>
|
|
68
|
+
) : (
|
|
69
|
+
<Text
|
|
70
|
+
style={[
|
|
71
|
+
styles.label,
|
|
72
|
+
{ color: isOutline ? backgroundColor : labelColor },
|
|
73
|
+
labelStyle,
|
|
74
|
+
]}
|
|
75
|
+
>
|
|
76
|
+
{label}
|
|
77
|
+
</Text>
|
|
78
|
+
)}
|
|
79
|
+
</TouchableOpacity>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const styles = StyleSheet.create({
|
|
84
|
+
shell: {
|
|
85
|
+
width: "100%",
|
|
86
|
+
minHeight: 52,
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
},
|
|
90
|
+
blocked: {
|
|
91
|
+
opacity: 0.4,
|
|
92
|
+
},
|
|
93
|
+
label: {
|
|
94
|
+
fontSize: 17,
|
|
95
|
+
fontWeight: "600",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { buttonRadiusValue } from "./buttonRadius.ts";
|
|
4
|
+
|
|
5
|
+
describe("buttonRadiusValue", () => {
|
|
6
|
+
it("maps Tailwind-style steps", () => {
|
|
7
|
+
assert.equal(buttonRadiusValue("none"), 0);
|
|
8
|
+
assert.equal(buttonRadiusValue("sm"), 2);
|
|
9
|
+
assert.equal(buttonRadiusValue("md"), 6);
|
|
10
|
+
assert.equal(buttonRadiusValue("lg"), 8);
|
|
11
|
+
assert.equal(buttonRadiusValue("xl"), 12);
|
|
12
|
+
assert.equal(buttonRadiusValue("2xl"), 16);
|
|
13
|
+
assert.equal(buttonRadiusValue("3xl"), 24);
|
|
14
|
+
assert.equal(buttonRadiusValue("full"), 999);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ButtonRadius =
|
|
2
|
+
| "none"
|
|
3
|
+
| "sm"
|
|
4
|
+
| "md"
|
|
5
|
+
| "lg"
|
|
6
|
+
| "xl"
|
|
7
|
+
| "2xl"
|
|
8
|
+
| "3xl"
|
|
9
|
+
| "full";
|
|
10
|
+
|
|
11
|
+
const RADIUS: Record<ButtonRadius, number> = {
|
|
12
|
+
none: 0,
|
|
13
|
+
sm: 2,
|
|
14
|
+
md: 6,
|
|
15
|
+
lg: 8,
|
|
16
|
+
xl: 12,
|
|
17
|
+
"2xl": 16,
|
|
18
|
+
"3xl": 24,
|
|
19
|
+
full: 999,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function buttonRadiusValue(radius: ButtonRadius): number {
|
|
23
|
+
return RADIUS[radius];
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FieldChrome } from "./fieldChrome";
|
|
2
|
+
import type { FieldTextareaProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export function FieldTextarea({
|
|
5
|
+
numberOfLines = 4,
|
|
6
|
+
...props
|
|
7
|
+
}: FieldTextareaProps) {
|
|
8
|
+
const minHeight = Math.max(100, numberOfLines * 24);
|
|
9
|
+
return (
|
|
10
|
+
<FieldChrome
|
|
11
|
+
{...props}
|
|
12
|
+
multiline
|
|
13
|
+
numberOfLines={numberOfLines}
|
|
14
|
+
minHeight={minHeight}
|
|
15
|
+
textAlignVertical="top"
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
StyleSheet,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
View,
|
|
8
|
+
type TextInputProps,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { buttonRadiusValue } from "../Button/buttonRadius";
|
|
11
|
+
import { FIELD_THEME_ROLES } from "./fieldThemeRoles";
|
|
12
|
+
import {
|
|
13
|
+
resolveFieldBorder,
|
|
14
|
+
shouldShowFieldClear,
|
|
15
|
+
} from "./resolveFieldBorder";
|
|
16
|
+
import type { FieldSharedProps } from "./types";
|
|
17
|
+
import { useResolvedFieldColor } from "./useResolvedFieldColor";
|
|
18
|
+
|
|
19
|
+
export type FieldChromeProps = FieldSharedProps & {
|
|
20
|
+
multiline?: boolean;
|
|
21
|
+
numberOfLines?: number;
|
|
22
|
+
minHeight?: number;
|
|
23
|
+
textAlignVertical?: TextInputProps["textAlignVertical"];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function FieldChrome({
|
|
27
|
+
value,
|
|
28
|
+
onChangeText,
|
|
29
|
+
borderColor: borderColorProp,
|
|
30
|
+
focusedBorderColor: focusedBorderColorProp,
|
|
31
|
+
errorColor: errorColorProp,
|
|
32
|
+
textColor: textColorProp,
|
|
33
|
+
labelColor: labelColorProp,
|
|
34
|
+
placeholderColor: placeholderColorProp,
|
|
35
|
+
backgroundColor: backgroundColorProp,
|
|
36
|
+
label,
|
|
37
|
+
description,
|
|
38
|
+
error,
|
|
39
|
+
placeholder,
|
|
40
|
+
radius = "xl",
|
|
41
|
+
editable = true,
|
|
42
|
+
clearIcon,
|
|
43
|
+
onClear,
|
|
44
|
+
style,
|
|
45
|
+
inputStyle,
|
|
46
|
+
multiline = false,
|
|
47
|
+
numberOfLines,
|
|
48
|
+
minHeight = 48,
|
|
49
|
+
textAlignVertical,
|
|
50
|
+
secureTextEntry,
|
|
51
|
+
keyboardType,
|
|
52
|
+
autoCapitalize,
|
|
53
|
+
autoComplete,
|
|
54
|
+
textContentType,
|
|
55
|
+
onFocus,
|
|
56
|
+
onBlur,
|
|
57
|
+
ref,
|
|
58
|
+
}: FieldChromeProps) {
|
|
59
|
+
const borderColor = useResolvedFieldColor(
|
|
60
|
+
borderColorProp,
|
|
61
|
+
FIELD_THEME_ROLES.borderColor,
|
|
62
|
+
);
|
|
63
|
+
const focusedBorderColor = useResolvedFieldColor(
|
|
64
|
+
focusedBorderColorProp,
|
|
65
|
+
FIELD_THEME_ROLES.focusedBorderColor,
|
|
66
|
+
);
|
|
67
|
+
const errorColor = useResolvedFieldColor(
|
|
68
|
+
errorColorProp,
|
|
69
|
+
FIELD_THEME_ROLES.errorColor,
|
|
70
|
+
);
|
|
71
|
+
const textColor = useResolvedFieldColor(
|
|
72
|
+
textColorProp,
|
|
73
|
+
FIELD_THEME_ROLES.textColor,
|
|
74
|
+
);
|
|
75
|
+
const labelColor = useResolvedFieldColor(
|
|
76
|
+
labelColorProp,
|
|
77
|
+
FIELD_THEME_ROLES.labelColor,
|
|
78
|
+
);
|
|
79
|
+
const placeholderColor = useResolvedFieldColor(
|
|
80
|
+
placeholderColorProp,
|
|
81
|
+
FIELD_THEME_ROLES.placeholderColor,
|
|
82
|
+
);
|
|
83
|
+
const backgroundColor = useResolvedFieldColor(
|
|
84
|
+
backgroundColorProp,
|
|
85
|
+
FIELD_THEME_ROLES.backgroundColor,
|
|
86
|
+
);
|
|
87
|
+
const descriptionColor = useResolvedFieldColor(
|
|
88
|
+
undefined,
|
|
89
|
+
FIELD_THEME_ROLES.descriptionColor,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
93
|
+
const hasError = Boolean(error);
|
|
94
|
+
const showClear = shouldShowFieldClear(value, clearIcon != null, editable);
|
|
95
|
+
const border = resolveFieldBorder({
|
|
96
|
+
isFocused,
|
|
97
|
+
hasError,
|
|
98
|
+
borderColor,
|
|
99
|
+
focusedBorderColor,
|
|
100
|
+
errorColor,
|
|
101
|
+
});
|
|
102
|
+
const borderRadius = buttonRadiusValue(radius);
|
|
103
|
+
|
|
104
|
+
const handleClear = () => {
|
|
105
|
+
if (onClear) onClear();
|
|
106
|
+
else onChangeText("");
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<View style={[styles.column, !editable && styles.blocked, style]}>
|
|
111
|
+
{label ? (
|
|
112
|
+
<Text style={[styles.label, { color: labelColor }]}>{label}</Text>
|
|
113
|
+
) : null}
|
|
114
|
+
<View style={styles.inputWrapper}>
|
|
115
|
+
<TextInput
|
|
116
|
+
ref={ref}
|
|
117
|
+
value={value}
|
|
118
|
+
onChangeText={onChangeText}
|
|
119
|
+
placeholder={placeholder}
|
|
120
|
+
placeholderTextColor={placeholderColor}
|
|
121
|
+
editable={editable}
|
|
122
|
+
multiline={multiline}
|
|
123
|
+
numberOfLines={numberOfLines}
|
|
124
|
+
textAlignVertical={textAlignVertical}
|
|
125
|
+
secureTextEntry={secureTextEntry}
|
|
126
|
+
keyboardType={keyboardType}
|
|
127
|
+
autoCapitalize={autoCapitalize}
|
|
128
|
+
autoComplete={autoComplete}
|
|
129
|
+
textContentType={textContentType}
|
|
130
|
+
accessibilityLabel={label}
|
|
131
|
+
accessibilityState={{ disabled: !editable }}
|
|
132
|
+
onFocus={(e) => {
|
|
133
|
+
setIsFocused(true);
|
|
134
|
+
onFocus?.(e);
|
|
135
|
+
}}
|
|
136
|
+
onBlur={(e) => {
|
|
137
|
+
setIsFocused(false);
|
|
138
|
+
onBlur?.(e);
|
|
139
|
+
}}
|
|
140
|
+
style={[
|
|
141
|
+
styles.input,
|
|
142
|
+
{
|
|
143
|
+
minHeight,
|
|
144
|
+
color: textColor,
|
|
145
|
+
backgroundColor,
|
|
146
|
+
borderRadius,
|
|
147
|
+
borderColor: border.borderColor,
|
|
148
|
+
borderWidth: border.borderWidth,
|
|
149
|
+
paddingHorizontal: border.paddingHorizontal,
|
|
150
|
+
paddingRight: showClear
|
|
151
|
+
? border.paddingHorizontal + 28
|
|
152
|
+
: border.paddingHorizontal,
|
|
153
|
+
},
|
|
154
|
+
multiline && styles.multiline,
|
|
155
|
+
inputStyle,
|
|
156
|
+
]}
|
|
157
|
+
/>
|
|
158
|
+
{showClear ? (
|
|
159
|
+
<TouchableOpacity
|
|
160
|
+
accessibilityLabel="Clear"
|
|
161
|
+
accessibilityRole="button"
|
|
162
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
163
|
+
onPress={handleClear}
|
|
164
|
+
style={styles.clearButton}
|
|
165
|
+
>
|
|
166
|
+
{clearIcon}
|
|
167
|
+
</TouchableOpacity>
|
|
168
|
+
) : null}
|
|
169
|
+
</View>
|
|
170
|
+
{error ? (
|
|
171
|
+
<Text style={[styles.helper, { color: errorColor }]}>{error}</Text>
|
|
172
|
+
) : description ? (
|
|
173
|
+
<Text style={[styles.helper, { color: descriptionColor }]}>
|
|
174
|
+
{description}
|
|
175
|
+
</Text>
|
|
176
|
+
) : null}
|
|
177
|
+
</View>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const styles = StyleSheet.create({
|
|
182
|
+
column: {
|
|
183
|
+
width: "100%",
|
|
184
|
+
gap: 6,
|
|
185
|
+
},
|
|
186
|
+
blocked: {
|
|
187
|
+
opacity: 0.4,
|
|
188
|
+
},
|
|
189
|
+
label: {
|
|
190
|
+
fontSize: 13,
|
|
191
|
+
fontWeight: "600",
|
|
192
|
+
},
|
|
193
|
+
inputWrapper: {
|
|
194
|
+
position: "relative",
|
|
195
|
+
width: "100%",
|
|
196
|
+
justifyContent: "center",
|
|
197
|
+
},
|
|
198
|
+
input: {
|
|
199
|
+
width: "100%",
|
|
200
|
+
paddingVertical: 12,
|
|
201
|
+
fontSize: 17,
|
|
202
|
+
},
|
|
203
|
+
multiline: {
|
|
204
|
+
paddingTop: 16,
|
|
205
|
+
paddingBottom: 16,
|
|
206
|
+
},
|
|
207
|
+
clearButton: {
|
|
208
|
+
position: "absolute",
|
|
209
|
+
right: 8,
|
|
210
|
+
top: 0,
|
|
211
|
+
bottom: 0,
|
|
212
|
+
justifyContent: "center",
|
|
213
|
+
alignItems: "center",
|
|
214
|
+
zIndex: 2,
|
|
215
|
+
},
|
|
216
|
+
helper: {
|
|
217
|
+
fontSize: 13,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Theme roles Field uses when colour props are omitted. */
|
|
2
|
+
export const FIELD_THEME_ROLES = {
|
|
3
|
+
borderColor: "fieldBorder",
|
|
4
|
+
focusedBorderColor: "fieldFocusedBorder",
|
|
5
|
+
errorColor: "fieldError",
|
|
6
|
+
textColor: "fieldText",
|
|
7
|
+
labelColor: "fieldLabel",
|
|
8
|
+
placeholderColor: "fieldPlaceholder",
|
|
9
|
+
backgroundColor: "fieldBackground",
|
|
10
|
+
descriptionColor: "fieldDescription",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type FieldThemeColorProp = keyof typeof FIELD_THEME_ROLES;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { FieldText } from "./FieldText";
|
|
2
|
+
export { FieldTextarea } from "./FieldTextarea";
|
|
3
|
+
export { FIELD_THEME_ROLES } from "./fieldThemeRoles";
|
|
4
|
+
export type { FieldThemeColorProp } from "./fieldThemeRoles";
|
|
5
|
+
export type { FieldSharedProps, FieldTextProps, FieldTextareaProps } from "./types";
|