@studio-piot/native-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +91 -0
  3. package/dist/components/Button/Button.d.ts +16 -0
  4. package/dist/components/Button/Button.d.ts.map +1 -0
  5. package/dist/components/Button/Button.js +44 -0
  6. package/dist/components/Button/Button.js.map +1 -0
  7. package/dist/components/Button/buttonRadius.d.ts +3 -0
  8. package/dist/components/Button/buttonRadius.d.ts.map +1 -0
  9. package/dist/components/Button/buttonRadius.js +17 -0
  10. package/dist/components/Button/buttonRadius.js.map +1 -0
  11. package/dist/components/Button/index.d.ts +3 -0
  12. package/dist/components/Button/index.d.ts.map +1 -0
  13. package/dist/components/Button/index.js +8 -0
  14. package/dist/components/Button/index.js.map +1 -0
  15. package/dist/components/Field/FieldText.d.ts +3 -0
  16. package/dist/components/Field/FieldText.d.ts.map +1 -0
  17. package/dist/components/Field/FieldText.js +9 -0
  18. package/dist/components/Field/FieldText.js.map +1 -0
  19. package/dist/components/Field/FieldTextarea.d.ts +3 -0
  20. package/dist/components/Field/FieldTextarea.d.ts.map +1 -0
  21. package/dist/components/Field/FieldTextarea.js +10 -0
  22. package/dist/components/Field/FieldTextarea.js.map +1 -0
  23. package/dist/components/Field/fieldChrome.d.ts +10 -0
  24. package/dist/components/Field/fieldChrome.d.ts.map +1 -0
  25. package/dist/components/Field/fieldChrome.js +100 -0
  26. package/dist/components/Field/fieldChrome.js.map +1 -0
  27. package/dist/components/Field/fieldThemeRoles.d.ts +13 -0
  28. package/dist/components/Field/fieldThemeRoles.d.ts.map +1 -0
  29. package/dist/components/Field/fieldThemeRoles.js +15 -0
  30. package/dist/components/Field/fieldThemeRoles.js.map +1 -0
  31. package/dist/components/Field/index.d.ts +6 -0
  32. package/dist/components/Field/index.d.ts.map +1 -0
  33. package/dist/components/Field/index.js +10 -0
  34. package/dist/components/Field/index.js.map +1 -0
  35. package/dist/components/Field/resolveFieldBorder.d.ts +16 -0
  36. package/dist/components/Field/resolveFieldBorder.d.ts.map +1 -0
  37. package/dist/components/Field/resolveFieldBorder.js +20 -0
  38. package/dist/components/Field/resolveFieldBorder.js.map +1 -0
  39. package/dist/components/Field/resolveFieldColor.d.ts +7 -0
  40. package/dist/components/Field/resolveFieldColor.d.ts.map +1 -0
  41. package/dist/components/Field/resolveFieldColor.js +21 -0
  42. package/dist/components/Field/resolveFieldColor.js.map +1 -0
  43. package/dist/components/Field/types.d.ts +46 -0
  44. package/dist/components/Field/types.d.ts.map +1 -0
  45. package/dist/components/Field/types.js +3 -0
  46. package/dist/components/Field/types.js.map +1 -0
  47. package/dist/components/Field/useResolvedFieldColor.d.ts +2 -0
  48. package/dist/components/Field/useResolvedFieldColor.d.ts.map +1 -0
  49. package/dist/components/Field/useResolvedFieldColor.js +15 -0
  50. package/dist/components/Field/useResolvedFieldColor.js.map +1 -0
  51. package/dist/components/Form/Form.d.ts +3 -0
  52. package/dist/components/Form/Form.d.ts.map +1 -0
  53. package/dist/components/Form/Form.js +100 -0
  54. package/dist/components/Form/Form.js.map +1 -0
  55. package/dist/components/Form/FormCloseButton.d.ts +4 -0
  56. package/dist/components/Form/FormCloseButton.d.ts.map +1 -0
  57. package/dist/components/Form/FormCloseButton.js +17 -0
  58. package/dist/components/Form/FormCloseButton.js.map +1 -0
  59. package/dist/components/Form/index.d.ts +4 -0
  60. package/dist/components/Form/index.d.ts.map +1 -0
  61. package/dist/components/Form/index.js +8 -0
  62. package/dist/components/Form/index.js.map +1 -0
  63. package/dist/components/Form/resolveThemeColor.d.ts +11 -0
  64. package/dist/components/Form/resolveThemeColor.d.ts.map +1 -0
  65. package/dist/components/Form/resolveThemeColor.js +21 -0
  66. package/dist/components/Form/resolveThemeColor.js.map +1 -0
  67. package/dist/components/Form/types.d.ts +30 -0
  68. package/dist/components/Form/types.d.ts.map +1 -0
  69. package/dist/components/Form/types.js +3 -0
  70. package/dist/components/Form/types.js.map +1 -0
  71. package/dist/components/Form/useResolvedThemeColor.d.ts +2 -0
  72. package/dist/components/Form/useResolvedThemeColor.d.ts.map +1 -0
  73. package/dist/components/Form/useResolvedThemeColor.js +15 -0
  74. package/dist/components/Form/useResolvedThemeColor.js.map +1 -0
  75. package/dist/components/Form/withAlpha.d.ts +2 -0
  76. package/dist/components/Form/withAlpha.d.ts.map +1 -0
  77. package/dist/components/Form/withAlpha.js +12 -0
  78. package/dist/components/Form/withAlpha.js.map +1 -0
  79. package/dist/components/LinkBlocks/LinkBlocks.d.ts +3 -0
  80. package/dist/components/LinkBlocks/LinkBlocks.d.ts.map +1 -0
  81. package/dist/components/LinkBlocks/LinkBlocks.js +75 -0
  82. package/dist/components/LinkBlocks/LinkBlocks.js.map +1 -0
  83. package/dist/components/LinkBlocks/filterGroups.d.ts +3 -0
  84. package/dist/components/LinkBlocks/filterGroups.d.ts.map +1 -0
  85. package/dist/components/LinkBlocks/filterGroups.js +7 -0
  86. package/dist/components/LinkBlocks/filterGroups.js.map +1 -0
  87. package/dist/components/LinkBlocks/index.d.ts +4 -0
  88. package/dist/components/LinkBlocks/index.d.ts.map +1 -0
  89. package/dist/components/LinkBlocks/index.js +8 -0
  90. package/dist/components/LinkBlocks/index.js.map +1 -0
  91. package/dist/components/LinkBlocks/types.d.ts +26 -0
  92. package/dist/components/LinkBlocks/types.d.ts.map +1 -0
  93. package/dist/components/LinkBlocks/types.js +3 -0
  94. package/dist/components/LinkBlocks/types.js.map +1 -0
  95. package/dist/components/RadioField/RadioField.d.ts +3 -0
  96. package/dist/components/RadioField/RadioField.d.ts.map +1 -0
  97. package/dist/components/RadioField/RadioField.js +76 -0
  98. package/dist/components/RadioField/RadioField.js.map +1 -0
  99. package/dist/components/RadioField/RadioMark.d.ts +7 -0
  100. package/dist/components/RadioField/RadioMark.d.ts.map +1 -0
  101. package/dist/components/RadioField/RadioMark.js +32 -0
  102. package/dist/components/RadioField/RadioMark.js.map +1 -0
  103. package/dist/components/RadioField/index.d.ts +4 -0
  104. package/dist/components/RadioField/index.d.ts.map +1 -0
  105. package/dist/components/RadioField/index.js +8 -0
  106. package/dist/components/RadioField/index.js.map +1 -0
  107. package/dist/components/RadioField/radioThemeRoles.d.ts +11 -0
  108. package/dist/components/RadioField/radioThemeRoles.d.ts.map +1 -0
  109. package/dist/components/RadioField/radioThemeRoles.js +13 -0
  110. package/dist/components/RadioField/radioThemeRoles.js.map +1 -0
  111. package/dist/components/RadioField/types.d.ts +24 -0
  112. package/dist/components/RadioField/types.d.ts.map +1 -0
  113. package/dist/components/RadioField/types.js +3 -0
  114. package/dist/components/RadioField/types.js.map +1 -0
  115. package/dist/components/Select/Select.d.ts +3 -0
  116. package/dist/components/Select/Select.d.ts.map +1 -0
  117. package/dist/components/Select/Select.js +137 -0
  118. package/dist/components/Select/Select.js.map +1 -0
  119. package/dist/components/Select/SelectCloseButton.d.ts +4 -0
  120. package/dist/components/Select/SelectCloseButton.d.ts.map +1 -0
  121. package/dist/components/Select/SelectCloseButton.js +17 -0
  122. package/dist/components/Select/SelectCloseButton.js.map +1 -0
  123. package/dist/components/Select/index.d.ts +6 -0
  124. package/dist/components/Select/index.d.ts.map +1 -0
  125. package/dist/components/Select/index.js +14 -0
  126. package/dist/components/Select/index.js.map +1 -0
  127. package/dist/components/Select/resolveSelectDetentIndex.d.ts +9 -0
  128. package/dist/components/Select/resolveSelectDetentIndex.d.ts.map +1 -0
  129. package/dist/components/Select/resolveSelectDetentIndex.js +16 -0
  130. package/dist/components/Select/resolveSelectDetentIndex.js.map +1 -0
  131. package/dist/components/Select/selectThemeRoles.d.ts +12 -0
  132. package/dist/components/Select/selectThemeRoles.d.ts.map +1 -0
  133. package/dist/components/Select/selectThemeRoles.js +14 -0
  134. package/dist/components/Select/selectThemeRoles.js.map +1 -0
  135. package/dist/components/Select/types.d.ts +35 -0
  136. package/dist/components/Select/types.d.ts.map +1 -0
  137. package/dist/components/Select/types.js +3 -0
  138. package/dist/components/Select/types.js.map +1 -0
  139. package/dist/components/index.d.ts +7 -0
  140. package/dist/components/index.d.ts.map +1 -0
  141. package/dist/components/index.js +27 -0
  142. package/dist/components/index.js.map +1 -0
  143. package/dist/index.d.ts +10 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +66 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/layout/index.d.ts +2 -0
  148. package/dist/layout/index.d.ts.map +1 -0
  149. package/dist/layout/index.js +6 -0
  150. package/dist/layout/index.js.map +1 -0
  151. package/dist/layout/screenPadding.d.ts +3 -0
  152. package/dist/layout/screenPadding.d.ts.map +1 -0
  153. package/dist/layout/screenPadding.js +6 -0
  154. package/dist/layout/screenPadding.js.map +1 -0
  155. package/dist/theme/ThemeProvider.d.ts +11 -0
  156. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  157. package/dist/theme/ThemeProvider.js +11 -0
  158. package/dist/theme/ThemeProvider.js.map +1 -0
  159. package/dist/theme/index.d.ts +6 -0
  160. package/dist/theme/index.d.ts.map +1 -0
  161. package/dist/theme/index.js +12 -0
  162. package/dist/theme/index.js.map +1 -0
  163. package/dist/theme/resolveColorScheme.d.ts +2 -0
  164. package/dist/theme/resolveColorScheme.d.ts.map +1 -0
  165. package/dist/theme/resolveColorScheme.js +7 -0
  166. package/dist/theme/resolveColorScheme.js.map +1 -0
  167. package/dist/theme/types.d.ts +8 -0
  168. package/dist/theme/types.d.ts.map +1 -0
  169. package/dist/theme/types.js +3 -0
  170. package/dist/theme/types.js.map +1 -0
  171. package/dist/theme/useColorScheme.d.ts +2 -0
  172. package/dist/theme/useColorScheme.d.ts.map +1 -0
  173. package/dist/theme/useColorScheme.js +16 -0
  174. package/dist/theme/useColorScheme.js.map +1 -0
  175. package/dist/theme/useThemeColor.d.ts +5 -0
  176. package/dist/theme/useThemeColor.d.ts.map +1 -0
  177. package/dist/theme/useThemeColor.js +25 -0
  178. package/dist/theme/useThemeColor.js.map +1 -0
  179. package/docs/components/button.md +162 -0
  180. package/docs/components/field.md +128 -0
  181. package/docs/components/form.md +86 -0
  182. package/docs/components/link-blocks.md +120 -0
  183. package/docs/components/radio-field.md +100 -0
  184. package/docs/components/select.md +59 -0
  185. package/docs/superpowers/plans/2026-09-24-field.md +583 -0
  186. package/docs/superpowers/plans/2026-09-24-link-blocks.md +414 -0
  187. package/docs/superpowers/plans/2026-09-24-theme-provider-button.md +493 -0
  188. package/docs/superpowers/plans/2026-09-25-radio-field.md +516 -0
  189. package/docs/superpowers/plans/2026-09-25-select.md +22 -0
  190. package/docs/superpowers/specs/2026-09-24-field-design.md +205 -0
  191. package/docs/superpowers/specs/2026-09-24-link-blocks-design.md +167 -0
  192. package/docs/superpowers/specs/2026-09-24-theme-provider-button-design.md +178 -0
  193. package/docs/superpowers/specs/2026-09-25-radio-field-design.md +136 -0
  194. package/docs/superpowers/specs/2026-09-25-select-design.md +164 -0
  195. package/package.json +75 -0
  196. package/src/components/Button/Button.tsx +97 -0
  197. package/src/components/Button/buttonRadius.test.ts +16 -0
  198. package/src/components/Button/buttonRadius.ts +24 -0
  199. package/src/components/Button/index.ts +2 -0
  200. package/src/components/Field/FieldText.tsx +6 -0
  201. package/src/components/Field/FieldTextarea.tsx +18 -0
  202. package/src/components/Field/fieldChrome.tsx +219 -0
  203. package/src/components/Field/fieldThemeRoles.ts +13 -0
  204. package/src/components/Field/index.ts +5 -0
  205. package/src/components/Field/resolveFieldBorder.test.ts +76 -0
  206. package/src/components/Field/resolveFieldBorder.ts +40 -0
  207. package/src/components/Field/resolveFieldColor.test.ts +92 -0
  208. package/src/components/Field/resolveFieldColor.ts +26 -0
  209. package/src/components/Field/types.ts +53 -0
  210. package/src/components/Field/useResolvedFieldColor.ts +15 -0
  211. package/src/components/Form/Form.tsx +205 -0
  212. package/src/components/Form/FormCloseButton.tsx +19 -0
  213. package/src/components/Form/index.ts +3 -0
  214. package/src/components/Form/resolveThemeColor.ts +24 -0
  215. package/src/components/Form/types.ts +30 -0
  216. package/src/components/Form/useResolvedThemeColor.ts +15 -0
  217. package/src/components/Form/withAlpha.test.ts +13 -0
  218. package/src/components/Form/withAlpha.ts +9 -0
  219. package/src/components/LinkBlocks/LinkBlocks.tsx +165 -0
  220. package/src/components/LinkBlocks/filterGroups.test.ts +37 -0
  221. package/src/components/LinkBlocks/filterGroups.ts +7 -0
  222. package/src/components/LinkBlocks/index.ts +7 -0
  223. package/src/components/LinkBlocks/types.ts +28 -0
  224. package/src/components/RadioField/RadioField.tsx +168 -0
  225. package/src/components/RadioField/RadioMark.tsx +54 -0
  226. package/src/components/RadioField/index.ts +3 -0
  227. package/src/components/RadioField/radioThemeRoles.test.ts +15 -0
  228. package/src/components/RadioField/radioThemeRoles.ts +11 -0
  229. package/src/components/RadioField/types.ts +25 -0
  230. package/src/components/Select/Select.tsx +279 -0
  231. package/src/components/Select/SelectCloseButton.tsx +19 -0
  232. package/src/components/Select/index.ts +13 -0
  233. package/src/components/Select/resolveSelectDetentIndex.test.ts +20 -0
  234. package/src/components/Select/resolveSelectDetentIndex.ts +16 -0
  235. package/src/components/Select/selectThemeRoles.ts +12 -0
  236. package/src/components/Select/types.ts +39 -0
  237. package/src/components/index.ts +42 -0
  238. package/src/index.ts +50 -0
  239. package/src/layout/index.ts +1 -0
  240. package/src/layout/screenPadding.ts +2 -0
  241. package/src/theme/ThemeProvider.tsx +20 -0
  242. package/src/theme/index.ts +5 -0
  243. package/src/theme/resolveColorScheme.test.ts +16 -0
  244. package/src/theme/resolveColorScheme.ts +5 -0
  245. package/src/theme/types.ts +5 -0
  246. package/src/theme/useColorScheme.ts +13 -0
  247. package/src/theme/useThemeColor.ts +25 -0
@@ -0,0 +1,279 @@
1
+ import { useCallback, useRef, type ReactNode } from "react";
2
+ import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { TrueSheet } from "@lodev09/react-native-true-sheet";
4
+ import { buttonRadiusValue } from "../Button/buttonRadius";
5
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
6
+ import {
7
+ DEFAULT_SELECT_DETENTS,
8
+ DEFAULT_SELECT_INITIAL_DETENT,
9
+ resolveSelectDetentIndex,
10
+ } from "./resolveSelectDetentIndex";
11
+ import { SelectCloseButton } from "./SelectCloseButton";
12
+ import { SELECT_THEME_ROLES } from "./selectThemeRoles";
13
+ import type { SelectOption, SelectProps } from "./types";
14
+
15
+ export function Select({
16
+ value,
17
+ onChange,
18
+ options,
19
+ placeholder = "Select an option",
20
+ label,
21
+ description,
22
+ error,
23
+ sheetTitle,
24
+ closeButton,
25
+ detents = [...DEFAULT_SELECT_DETENTS],
26
+ initialDetent = DEFAULT_SELECT_INITIAL_DETENT,
27
+ radius = "xl",
28
+ editable = true,
29
+ backgroundColor: backgroundColorProp,
30
+ textColor: textColorProp,
31
+ placeholderColor: placeholderColorProp,
32
+ labelColor: labelColorProp,
33
+ descriptionColor: descriptionColorProp,
34
+ borderColor: borderColorProp,
35
+ errorColor: errorColorProp,
36
+ selectedColor: selectedColorProp,
37
+ style,
38
+ }: SelectProps) {
39
+ const sheetRef = useRef<TrueSheet>(null);
40
+
41
+ const backgroundColor = useResolvedFieldColor(
42
+ backgroundColorProp,
43
+ SELECT_THEME_ROLES.backgroundColor,
44
+ );
45
+ const textColor = useResolvedFieldColor(
46
+ textColorProp,
47
+ SELECT_THEME_ROLES.textColor,
48
+ );
49
+ const placeholderColor = useResolvedFieldColor(
50
+ placeholderColorProp,
51
+ SELECT_THEME_ROLES.placeholderColor,
52
+ );
53
+ const labelColor = useResolvedFieldColor(
54
+ labelColorProp,
55
+ SELECT_THEME_ROLES.labelColor,
56
+ );
57
+ const descriptionColor = useResolvedFieldColor(
58
+ descriptionColorProp,
59
+ SELECT_THEME_ROLES.descriptionColor,
60
+ );
61
+ const borderColor = useResolvedFieldColor(
62
+ borderColorProp,
63
+ SELECT_THEME_ROLES.borderColor,
64
+ );
65
+ const errorColor = useResolvedFieldColor(
66
+ errorColorProp,
67
+ SELECT_THEME_ROLES.errorColor,
68
+ );
69
+ const selectedColor = useResolvedFieldColor(
70
+ selectedColorProp,
71
+ SELECT_THEME_ROLES.selectedColor,
72
+ );
73
+
74
+ const hasError = Boolean(error);
75
+ const selected = options.find((option) => option.value === value);
76
+ const display = selected?.label ?? placeholder;
77
+ const isPlaceholder = selected == null;
78
+ const borderRadius = buttonRadiusValue(radius);
79
+ const title = sheetTitle ?? label ?? "Select";
80
+ const presentIndex = resolveSelectDetentIndex(detents, initialDetent);
81
+
82
+ const dismiss = useCallback(() => {
83
+ void sheetRef.current?.dismiss();
84
+ }, []);
85
+
86
+ const present = useCallback(() => {
87
+ if (!editable) return;
88
+ void sheetRef.current?.present(presentIndex);
89
+ }, [editable, presentIndex]);
90
+
91
+ const resolvedClose: ReactNode =
92
+ typeof closeButton === "function"
93
+ ? closeButton(dismiss)
94
+ : (closeButton ?? <SelectCloseButton onPress={dismiss} />);
95
+
96
+ const onSelect = (option: SelectOption) => {
97
+ if (option.disabled) return;
98
+ onChange(option.value);
99
+ dismiss();
100
+ };
101
+
102
+ return (
103
+ <View style={[styles.column, !editable && styles.blocked, style]}>
104
+ {label ? (
105
+ <Text style={[styles.label, { color: labelColor }]}>{label}</Text>
106
+ ) : null}
107
+ <Pressable
108
+ accessibilityHint="Opens a list of options"
109
+ accessibilityLabel={label ? `${label}. ${display}` : display}
110
+ accessibilityRole="button"
111
+ accessibilityState={{ disabled: !editable }}
112
+ disabled={!editable}
113
+ onPress={present}
114
+ style={[
115
+ styles.trigger,
116
+ {
117
+ backgroundColor,
118
+ borderColor: hasError ? errorColor : borderColor,
119
+ borderRadius,
120
+ },
121
+ ]}
122
+ >
123
+ <Text
124
+ numberOfLines={1}
125
+ style={[
126
+ styles.triggerText,
127
+ { color: isPlaceholder ? placeholderColor : textColor, flex: 1 },
128
+ ]}
129
+ >
130
+ {display}
131
+ </Text>
132
+ <Text
133
+ accessibilityElementsHidden
134
+ importantForAccessibility="no"
135
+ style={[styles.chevron, { color: placeholderColor }]}
136
+ >
137
+ ›
138
+ </Text>
139
+ </Pressable>
140
+ {error ? (
141
+ <Text style={[styles.helper, { color: errorColor }]}>{error}</Text>
142
+ ) : description ? (
143
+ <Text style={[styles.helper, { color: descriptionColor }]}>
144
+ {description}
145
+ </Text>
146
+ ) : null}
147
+
148
+ <TrueSheet
149
+ ref={sheetRef}
150
+ backgroundColor={backgroundColor}
151
+ cornerRadius={buttonRadiusValue("xl")}
152
+ detents={detents}
153
+ grabber
154
+ scrollable
155
+ >
156
+ <View style={styles.sheetHeader}>
157
+ <Text
158
+ accessibilityRole="header"
159
+ numberOfLines={1}
160
+ style={[styles.sheetTitle, { color: textColor, flex: 1 }]}
161
+ >
162
+ {title}
163
+ </Text>
164
+ {resolvedClose}
165
+ </View>
166
+ {options.length === 0 ? (
167
+ <Text style={[styles.empty, { color: placeholderColor }]}>
168
+ No options
169
+ </Text>
170
+ ) : (
171
+ options.map((item) => {
172
+ const isSelected = item.value === value;
173
+ return (
174
+ <Pressable
175
+ key={item.value}
176
+ accessibilityRole="button"
177
+ accessibilityState={{
178
+ selected: isSelected,
179
+ disabled: !!item.disabled,
180
+ }}
181
+ disabled={item.disabled}
182
+ onPress={() => onSelect(item)}
183
+ style={[styles.option, item.disabled && styles.blocked]}
184
+ >
185
+ <Text
186
+ style={[
187
+ styles.optionLabel,
188
+ {
189
+ color: isSelected ? selectedColor : textColor,
190
+ flex: 1,
191
+ },
192
+ ]}
193
+ >
194
+ {item.label}
195
+ </Text>
196
+ {isSelected ? (
197
+ <Text
198
+ accessibilityElementsHidden
199
+ importantForAccessibility="no"
200
+ style={[styles.check, { color: selectedColor }]}
201
+ >
202
+ ✓
203
+ </Text>
204
+ ) : null}
205
+ </Pressable>
206
+ );
207
+ })
208
+ )}
209
+ </TrueSheet>
210
+ </View>
211
+ );
212
+ }
213
+
214
+ const styles = StyleSheet.create({
215
+ column: {
216
+ width: "100%",
217
+ gap: 6,
218
+ },
219
+ blocked: {
220
+ opacity: 0.4,
221
+ },
222
+ label: {
223
+ fontSize: 13,
224
+ fontWeight: "600",
225
+ },
226
+ trigger: {
227
+ minHeight: 48,
228
+ borderWidth: 1,
229
+ paddingHorizontal: 14,
230
+ paddingVertical: 12,
231
+ flexDirection: "row",
232
+ alignItems: "center",
233
+ gap: 8,
234
+ },
235
+ triggerText: {
236
+ fontSize: 17,
237
+ },
238
+ chevron: {
239
+ fontSize: 22,
240
+ fontWeight: "300",
241
+ lineHeight: 24,
242
+ transform: [{ rotate: "90deg" }],
243
+ },
244
+ helper: {
245
+ fontSize: 13,
246
+ },
247
+ sheetHeader: {
248
+ flexDirection: "row",
249
+ alignItems: "center",
250
+ gap: 12,
251
+ paddingHorizontal: 16,
252
+ paddingTop: 8,
253
+ paddingBottom: 12,
254
+ },
255
+ sheetTitle: {
256
+ fontSize: 17,
257
+ fontWeight: "600",
258
+ },
259
+ empty: {
260
+ fontSize: 17,
261
+ paddingHorizontal: 16,
262
+ paddingVertical: 24,
263
+ textAlign: "center",
264
+ },
265
+ option: {
266
+ minHeight: 52,
267
+ paddingHorizontal: 16,
268
+ flexDirection: "row",
269
+ alignItems: "center",
270
+ gap: 12,
271
+ },
272
+ optionLabel: {
273
+ fontSize: 17,
274
+ },
275
+ check: {
276
+ fontSize: 17,
277
+ fontWeight: "600",
278
+ },
279
+ });
@@ -0,0 +1,19 @@
1
+ import { Pressable, StyleSheet, Text } from "react-native";
2
+ import { useThemeColor } from "../../theme/useThemeColor";
3
+
4
+ export function SelectCloseButton({ onPress }: { onPress: () => void }) {
5
+ const text = useThemeColor("text");
6
+
7
+ return (
8
+ <Pressable accessibilityRole="button" hitSlop={8} onPress={onPress}>
9
+ <Text style={[styles.label, { color: text }]}>Cancel</Text>
10
+ </Pressable>
11
+ );
12
+ }
13
+
14
+ const styles = StyleSheet.create({
15
+ label: {
16
+ fontSize: 17,
17
+ fontWeight: "600",
18
+ },
19
+ });
@@ -0,0 +1,13 @@
1
+ export { Select } from "./Select";
2
+ export { SelectCloseButton } from "./SelectCloseButton";
3
+ export {
4
+ DEFAULT_SELECT_DETENTS,
5
+ DEFAULT_SELECT_INITIAL_DETENT,
6
+ resolveSelectDetentIndex,
7
+ } from "./resolveSelectDetentIndex";
8
+ export { SELECT_THEME_ROLES, type SelectThemeColorProp } from "./selectThemeRoles";
9
+ export type {
10
+ SelectCloseButton as SelectCloseButtonProp,
11
+ SelectOption,
12
+ SelectProps,
13
+ } from "./types";
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ DEFAULT_SELECT_DETENTS,
5
+ DEFAULT_SELECT_INITIAL_DETENT,
6
+ resolveSelectDetentIndex,
7
+ } from "./resolveSelectDetentIndex.ts";
8
+
9
+ describe("resolveSelectDetentIndex", () => {
10
+ it("returns the index of the initial detent", () => {
11
+ assert.equal(
12
+ resolveSelectDetentIndex(DEFAULT_SELECT_DETENTS, DEFAULT_SELECT_INITIAL_DETENT),
13
+ 1,
14
+ );
15
+ });
16
+
17
+ it("falls back to 0 when initial is missing", () => {
18
+ assert.equal(resolveSelectDetentIndex([0.25, 0.5, 0.75], 0.9), 0);
19
+ });
20
+ });
@@ -0,0 +1,16 @@
1
+ /** Default sheet heights: quarter, half, three-quarter. */
2
+ export const DEFAULT_SELECT_DETENTS = [0.25, 0.5, 0.75] as const;
3
+
4
+ export const DEFAULT_SELECT_INITIAL_DETENT = 0.5;
5
+
6
+ /**
7
+ * Resolve which detent index to present at.
8
+ * Falls back to 0 when `initial` is missing from `detents`.
9
+ */
10
+ export function resolveSelectDetentIndex(
11
+ detents: readonly number[],
12
+ initial: number,
13
+ ): number {
14
+ const index = detents.indexOf(initial);
15
+ return index >= 0 ? index : 0;
16
+ }
@@ -0,0 +1,12 @@
1
+ export const SELECT_THEME_ROLES = {
2
+ backgroundColor: "fieldBackground",
3
+ textColor: "fieldText",
4
+ placeholderColor: "fieldPlaceholder",
5
+ labelColor: "fieldLabel",
6
+ descriptionColor: "fieldDescription",
7
+ borderColor: "fieldBorder",
8
+ errorColor: "fieldError",
9
+ selectedColor: "fieldRadioSelected",
10
+ } as const;
11
+
12
+ export type SelectThemeColorProp = keyof typeof SELECT_THEME_ROLES;
@@ -0,0 +1,39 @@
1
+ import type { ReactNode } from "react";
2
+ import type { StyleProp, ViewStyle } from "react-native";
3
+ import type { ButtonRadius } from "../Button/buttonRadius";
4
+
5
+ export type SelectOption = {
6
+ value: string;
7
+ label: string;
8
+ disabled?: boolean;
9
+ };
10
+
11
+ export type SelectCloseButton =
12
+ | ReactNode
13
+ | ((dismiss: () => void) => ReactNode);
14
+
15
+ export type SelectProps = {
16
+ value: string | null;
17
+ onChange: (value: string) => void;
18
+ options: SelectOption[];
19
+ placeholder?: string;
20
+ label?: string;
21
+ description?: string;
22
+ error?: string;
23
+ sheetTitle?: string;
24
+ /** Plain Cancel by default. Pass `(dismiss) => <Glass onPress={dismiss} />` for app chrome. */
25
+ closeButton?: SelectCloseButton;
26
+ detents?: number[];
27
+ initialDetent?: number;
28
+ radius?: ButtonRadius;
29
+ editable?: boolean;
30
+ backgroundColor?: string;
31
+ textColor?: string;
32
+ placeholderColor?: string;
33
+ labelColor?: string;
34
+ descriptionColor?: string;
35
+ borderColor?: string;
36
+ errorColor?: string;
37
+ selectedColor?: string;
38
+ style?: StyleProp<ViewStyle>;
39
+ };
@@ -0,0 +1,42 @@
1
+ export {
2
+ Button,
3
+ type ButtonProps,
4
+ buttonRadiusValue,
5
+ type ButtonRadius,
6
+ } from "./Button";
7
+ export {
8
+ LinkBlocks,
9
+ filterLinkBlockGroups,
10
+ type LinkBlockGroup,
11
+ type LinkBlockItem,
12
+ type LinkBlocksProps,
13
+ } from "./LinkBlocks";
14
+ export {
15
+ FieldText,
16
+ FieldTextarea,
17
+ FIELD_THEME_ROLES,
18
+ type FieldThemeColorProp,
19
+ type FieldSharedProps,
20
+ type FieldTextProps,
21
+ type FieldTextareaProps,
22
+ } from "./Field";
23
+ export { Form, FormCloseButton, type FormProps } from "./Form";
24
+ export {
25
+ RadioField,
26
+ RADIO_THEME_ROLES,
27
+ type RadioThemeColorProp,
28
+ type RadioOption,
29
+ type RadioFieldProps,
30
+ } from "./RadioField";
31
+ export {
32
+ Select,
33
+ SelectCloseButton,
34
+ SELECT_THEME_ROLES,
35
+ DEFAULT_SELECT_DETENTS,
36
+ DEFAULT_SELECT_INITIAL_DETENT,
37
+ resolveSelectDetentIndex,
38
+ type SelectThemeColorProp,
39
+ type SelectOption,
40
+ type SelectProps,
41
+ type SelectCloseButtonProp,
42
+ } from "./Select";
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ export const version = "0.1.0";
2
+
3
+ export * from "./theme";
4
+ export {
5
+ Button,
6
+ type ButtonProps,
7
+ buttonRadiusValue,
8
+ type ButtonRadius,
9
+ } from "./components/Button";
10
+ export {
11
+ LinkBlocks,
12
+ filterLinkBlockGroups,
13
+ type LinkBlockGroup,
14
+ type LinkBlockItem,
15
+ type LinkBlocksProps,
16
+ } from "./components/LinkBlocks";
17
+ export {
18
+ FieldText,
19
+ FieldTextarea,
20
+ FIELD_THEME_ROLES,
21
+ type FieldThemeColorProp,
22
+ type FieldSharedProps,
23
+ type FieldTextProps,
24
+ type FieldTextareaProps,
25
+ } from "./components/Field";
26
+ export {
27
+ Form,
28
+ FormCloseButton,
29
+ type FormProps,
30
+ } from "./components/Form";
31
+ export {
32
+ RadioField,
33
+ RADIO_THEME_ROLES,
34
+ type RadioThemeColorProp,
35
+ type RadioOption,
36
+ type RadioFieldProps,
37
+ } from "./components/RadioField";
38
+ export {
39
+ Select,
40
+ SelectCloseButton,
41
+ SELECT_THEME_ROLES,
42
+ DEFAULT_SELECT_DETENTS,
43
+ DEFAULT_SELECT_INITIAL_DETENT,
44
+ resolveSelectDetentIndex,
45
+ type SelectThemeColorProp,
46
+ type SelectOption,
47
+ type SelectProps,
48
+ type SelectCloseButtonProp,
49
+ } from "./components/Select";
50
+ export * as components from "./components";
@@ -0,0 +1 @@
1
+ export { screenPaddingHorizontal } from "./screenPadding";
@@ -0,0 +1,2 @@
1
+ /** Default inset from screen edges for Form header, content, and footer. */
2
+ export const screenPaddingHorizontal = 16;
@@ -0,0 +1,20 @@
1
+ import {
2
+ createContext,
3
+ createElement,
4
+ useMemo,
5
+ type ReactNode,
6
+ } from "react";
7
+ import type { ThemeColors, ThemeContextValue } from "./types";
8
+
9
+ export const ThemeContext = createContext<ThemeContextValue | null>(null);
10
+
11
+ export function ThemeProvider({
12
+ colors,
13
+ children,
14
+ }: {
15
+ colors: { light: ThemeColors; dark: ThemeColors };
16
+ children: ReactNode;
17
+ }) {
18
+ const value = useMemo(() => ({ colors }), [colors]);
19
+ return createElement(ThemeContext.Provider, { value }, children);
20
+ }
@@ -0,0 +1,5 @@
1
+ export type { ThemeColors, ThemeContextValue } from "./types";
2
+ export { ThemeProvider } from "./ThemeProvider";
3
+ export { useColorScheme } from "./useColorScheme";
4
+ export { useThemeColor } from "./useThemeColor";
5
+ export { resolveColorScheme } from "./resolveColorScheme";
@@ -0,0 +1,16 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { resolveColorScheme } from "./resolveColorScheme.ts";
4
+
5
+ describe("resolveColorScheme", () => {
6
+ it("returns dark only for dark", () => {
7
+ assert.equal(resolveColorScheme("dark"), "dark");
8
+ });
9
+
10
+ it("treats everything else as light", () => {
11
+ assert.equal(resolveColorScheme("light"), "light");
12
+ assert.equal(resolveColorScheme(null), "light");
13
+ assert.equal(resolveColorScheme(undefined), "light");
14
+ assert.equal(resolveColorScheme("unspecified"), "light");
15
+ });
16
+ });
@@ -0,0 +1,5 @@
1
+ export function resolveColorScheme(
2
+ scheme: string | null | undefined,
3
+ ): "light" | "dark" {
4
+ return scheme === "dark" ? "dark" : "light";
5
+ }
@@ -0,0 +1,5 @@
1
+ export type ThemeColors = Record<string, string>;
2
+
3
+ export type ThemeContextValue = {
4
+ colors: { light: ThemeColors; dark: ThemeColors };
5
+ };
@@ -0,0 +1,13 @@
1
+ import { useContext } from "react";
2
+ import { useColorScheme as useSystemColorScheme } from "react-native";
3
+ import { resolveColorScheme } from "./resolveColorScheme";
4
+ import { ThemeContext } from "./ThemeProvider";
5
+
6
+ export function useColorScheme(): "light" | "dark" {
7
+ const ctx = useContext(ThemeContext);
8
+ const systemScheme = useSystemColorScheme();
9
+ if (!ctx) {
10
+ throw new Error("useColorScheme must be used within ThemeProvider");
11
+ }
12
+ return resolveColorScheme(systemScheme);
13
+ }
@@ -0,0 +1,25 @@
1
+ import { useContext } from "react";
2
+ import { useColorScheme as useSystemColorScheme } from "react-native";
3
+ import { resolveColorScheme } from "./resolveColorScheme";
4
+ import { ThemeContext } from "./ThemeProvider";
5
+
6
+ export function useThemeColor(
7
+ colorName: string,
8
+ overrides?: { light?: string; dark?: string },
9
+ ): string {
10
+ const ctx = useContext(ThemeContext);
11
+ const systemScheme = useSystemColorScheme();
12
+ if (!ctx) {
13
+ throw new Error("useThemeColor must be used within ThemeProvider");
14
+ }
15
+ const scheme = resolveColorScheme(systemScheme);
16
+ const override = overrides?.[scheme];
17
+ if (override != null) {
18
+ return override;
19
+ }
20
+ const value = ctx.colors[scheme][colorName];
21
+ if (value == null) {
22
+ throw new Error(`Unknown theme colour "${colorName}" for scheme "${scheme}"`);
23
+ }
24
+ return value;
25
+ }