@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,205 @@
1
+ # Field (FieldText / FieldTextarea) — design
2
+
3
+ Date: 2026-09-24
4
+ Package: `@studio-piot/native-ui`
5
+ Status: approved
6
+
7
+ ## Goal
8
+
9
+ Add presentational form controls **FieldText** and **FieldTextarea**. Apps own colours, clear icons, and form state. The package renders label, input chrome (including focus and error borders), optional clear affordance, and error text. No brand names, theme role catalogues, or icon libraries live in this package.
10
+
11
+ Replace the current example `src/components/Field/` files (app-coupled imports) with this contract.
12
+
13
+ ## Decisions
14
+
15
+ | Topic | Choice |
16
+ |---|---|
17
+ | API shape | Single controls: `FieldText`, `FieldTextarea` with optional `label` / `error` |
18
+ | Internals | Shared chrome helper (label, border, focus, error, clear) |
19
+ | Colours | Required colour props (same pattern as `Button` / `LinkBlocks`) |
20
+ | Focus | Resting `borderWidth: 1`; focused `borderWidth: 2` with padding compensation so layout does not jump |
21
+ | Error | Message under the control; border uses `errorColor` when `error` is a non-empty string |
22
+ | Focus + error | Error colour wins; double stroke still applies while focused |
23
+ | Clear | Shown when `value` is non-empty **and** `clearIcon` is provided. Optional `onClear`; default clears via `onChangeText("")` |
24
+ | Radius | Reuse `ButtonRadius` / `buttonRadiusValue`. Default `"xl"` |
25
+ | Width | Always full width of the parent |
26
+ | Disabled | `editable={false}` → opacity `0.4` on the control (same treatment as `Button`) |
27
+ | Brand coupling | None |
28
+
29
+ ## Architecture
30
+
31
+ ```
32
+ FieldText ──┐
33
+ ├── fieldChrome (shared layout + border/focus/error/clear)
34
+ FieldTextarea ─┘
35
+ ```
36
+
37
+ - Public exports: `FieldText`, `FieldTextarea` only (no compound `Field` / `FieldLabel` / `FieldError` in the public API).
38
+ - Shared helper is package-internal; not required to be a public export.
39
+ - Controlled only: `value` + `onChangeText` required.
40
+
41
+ ## Colour props (required)
42
+
43
+ | Prop | Role |
44
+ |---|---|
45
+ | `borderColor` | Resting border |
46
+ | `focusedBorderColor` | Focused border (when not in error) |
47
+ | `errorColor` | Error border and error text |
48
+ | `textColor` | Input value text |
49
+ | `labelColor` | Label above the input |
50
+ | `placeholderColor` | Placeholder |
51
+ | `backgroundColor` | Input fill |
52
+
53
+ Callers typically resolve these with `useThemeColor` (package or app) before passing them in. The Field components do not call theme hooks themselves.
54
+
55
+ ## Props
56
+
57
+ ### Shared (`FieldText` and `FieldTextarea`)
58
+
59
+ | Prop | Type | Required | Notes |
60
+ |---|---|---|---|
61
+ | `value` | `string` | yes | Controlled |
62
+ | `onChangeText` | `(text: string) => void` | yes | |
63
+ | `borderColor` | `string` | yes | |
64
+ | `focusedBorderColor` | `string` | yes | |
65
+ | `errorColor` | `string` | yes | |
66
+ | `textColor` | `string` | yes | |
67
+ | `labelColor` | `string` | yes | |
68
+ | `placeholderColor` | `string` | yes | |
69
+ | `backgroundColor` | `string` | yes | |
70
+ | `label` | `string` | no | Above the input |
71
+ | `error` | `string` | no | Under the input; truthy drives error border |
72
+ | `placeholder` | `string` | no | |
73
+ | `radius` | `ButtonRadius` | no | Default `"xl"` |
74
+ | `editable` | `boolean` | no | Default `true` |
75
+ | `clearIcon` | `React.ReactNode` | no | Clear shown only with non-empty `value` and this prop |
76
+ | `onClear` | `() => void` | no | Default: `() => onChangeText("")` |
77
+ | `style` | `StyleProp<ViewStyle>` | no | Outer column wrapper |
78
+ | `inputStyle` | `StyleProp<TextStyle>` | no | Extra styles on `TextInput` |
79
+ | Common `TextInput` passthroughs | — | no | e.g. `secureTextEntry`, `keyboardType`, `autoCapitalize`, `autoComplete`, `textContentType`, `onFocus` / `onBlur`, `ref` |
80
+
81
+ Do **not** invert `editable` (the Ribe example’s `editable={!editable}` bug must not be copied).
82
+
83
+ ### `FieldTextarea` extras
84
+
85
+ | Prop | Type | Required | Notes |
86
+ |---|---|---|---|
87
+ | `numberOfLines` | `number` | no | Default `4` |
88
+ | — | — | — | Always multiline; `textAlignVertical: "top"`; min height derived from line count |
89
+
90
+ ## Layout and behaviour
91
+
92
+ ### Column
93
+
94
+ Vertical stack with a small gap (~6):
95
+
96
+ 1. Optional label — caption-sized (13), weight 600, colour `labelColor`
97
+ 2. Input shell — full width
98
+ 3. Optional error text — size 13, colour `errorColor`
99
+
100
+ ### Input shell
101
+
102
+ - `backgroundColor`, `borderRadius` from `radius`
103
+ - Resting: `borderWidth: 1`, colour `borderColor`
104
+ - Focused (no error): `borderWidth: 2`, colour `focusedBorderColor`; reduce horizontal padding by 1 so the control does not grow
105
+ - Error (with or without focus): border colour `errorColor`; if also focused, keep `borderWidth: 2`
106
+ - Value text: `textColor`; placeholder: `placeholderColor`
107
+ - Single-line (`FieldText`): min height ~48–50, vertical padding aligned with package Button density
108
+ - Textarea: taller min height from `numberOfLines`; top-aligned text
109
+
110
+ ### Clear
111
+
112
+ - Absolute position at the trailing end of the input
113
+ - Extra right padding on the `TextInput` when the clear control is visible
114
+ - `TouchableOpacity` / `Pressable` with adequate hitSlop
115
+ - Package does not import icon libraries; `clearIcon` is opaque `ReactNode`
116
+
117
+ ### Accessibility
118
+
119
+ - Associate label with the input where practical (`accessibilityLabel` from `label` when present)
120
+ - Error text available to assistive tech (e.g. announced with the field or as related text)
121
+ - Clear control has an accessibility label (e.g. "Clear")
122
+
123
+ ## Out of scope (v1)
124
+
125
+ - Uncontrolled mode (`defaultValue`)
126
+ - Validation / form libraries
127
+ - Left adornments, prefixes, floating labels
128
+ - Built-in clear glyph without `clearIcon`
129
+ - Looking up theme roles inside Field
130
+ - Migrating Ribe or Optimum forms onto these components (consumers can follow later)
131
+
132
+ ## Package layout
133
+
134
+ | Path | Job |
135
+ |---|---|
136
+ | `src/components/Field/FieldText.tsx` | Single-line public component |
137
+ | `src/components/Field/FieldTextarea.tsx` | Multiline public component |
138
+ | `src/components/Field/fieldChrome.tsx` | Shared label / border / focus / error / clear (name flexible) |
139
+ | `src/components/Field/types.ts` | Shared props types |
140
+ | `src/components/Field/index.ts` | Barrel |
141
+ | Remove | App-coupled example files (`Field.tsx`, `FieldLabel.tsx`, `FieldInput.tsx`, `FieldError.tsx`, and the old `FieldTextarea.tsx` if replaced) |
142
+ | `src/components/index.ts` | Re-export |
143
+ | `src/index.ts` | Public export |
144
+ | `docs/components/field.md` | Usage doc (linked from README) |
145
+
146
+ Reuse `ButtonRadius` / `buttonRadiusValue` from the Button module.
147
+
148
+ ## Example (app)
149
+
150
+ ```tsx
151
+ import { FieldText, FieldTextarea, useThemeColor } from "@studio-piot/native-ui";
152
+ import Ionicons from "@expo/vector-icons/Ionicons";
153
+
154
+ const border = useThemeColor("border");
155
+ const focused = useThemeColor("tint");
156
+ const error = useThemeColor("danger");
157
+ const text = useThemeColor("text");
158
+ const textSecondary = useThemeColor("textSecondary");
159
+ const surface = useThemeColor("surface");
160
+
161
+ <FieldText
162
+ label="Email"
163
+ value={email}
164
+ onChangeText={setEmail}
165
+ placeholder="you@example.com"
166
+ error={emailError}
167
+ borderColor={border}
168
+ focusedBorderColor={focused}
169
+ errorColor={error}
170
+ textColor={text}
171
+ labelColor={textSecondary}
172
+ placeholderColor={textSecondary}
173
+ backgroundColor={surface}
174
+ keyboardType="email-address"
175
+ autoCapitalize="none"
176
+ clearIcon={<Ionicons name="close-circle" size={20} color={textSecondary} />}
177
+ />
178
+
179
+ <FieldTextarea
180
+ label="Notes"
181
+ value={notes}
182
+ onChangeText={setNotes}
183
+ borderColor={border}
184
+ focusedBorderColor={focused}
185
+ errorColor={error}
186
+ textColor={text}
187
+ labelColor={textSecondary}
188
+ placeholderColor={textSecondary}
189
+ backgroundColor={surface}
190
+ />
191
+ ```
192
+
193
+ Colour role names above are illustrative; the app chooses its own map keys.
194
+
195
+ ## Verification
196
+
197
+ - `npm test` and `npm run build` in the package succeed.
198
+ - Typecheck a consumer that renders labelled `FieldText` (with `error` and `clearIcon`) and `FieldTextarea`.
199
+ - Manual: focus doubles stroke without layout jump; error colour + message; clear only when icon + value; disabled opacity; `editable` is not inverted.
200
+
201
+ ## Related
202
+
203
+ - Button + theme: `docs/superpowers/specs/2026-09-24-theme-provider-button-design.md`
204
+ - LinkBlocks: `docs/superpowers/specs/2026-09-24-link-blocks-design.md`
205
+ - Usage (after implementation): `docs/components/field.md`
@@ -0,0 +1,167 @@
1
+ # LinkBlocks — design
2
+
3
+ Date: 2026-09-24
4
+ Package: `@studio-piot/native-ui`
5
+ Status: approved
6
+
7
+ ## Goal
8
+
9
+ Add a presentational **LinkBlocks** component: one or more rounded groups of tappable rows (optional icon, label, trailing chevron), driven by a config object. Section titles sit outside and above each group. The parent app owns colours, icons, and navigation (`onPress` per item). No brand names, role catalogues, or router dependencies in the package.
10
+
11
+ ## Decisions
12
+
13
+ | Topic | Choice |
14
+ |---|---|
15
+ | API shape | Config-driven `LinkBlocks` with `blocks: LinkBlockGroup[]` |
16
+ | Navigation | `onPress` per item only. Package does not import Expo Router or Linking |
17
+ | Icons | Optional `React.ReactNode` per item. Package does not depend on icon libraries |
18
+ | Colours | Required colour props on the component (same pattern as `Button`) |
19
+ | Chevron | Drawn by the package (text chevron). Colour from `chevronColor` |
20
+ | Radius | Same Tailwind-style steps as `Button`. Default `"xl"` |
21
+ | Width | Always full width of the parent |
22
+ | Brand coupling | None |
23
+
24
+ ## Data
25
+
26
+ ```ts
27
+ type LinkBlockItem = {
28
+ key: string;
29
+ label: string;
30
+ onPress: () => void;
31
+ icon?: React.ReactNode;
32
+ disabled?: boolean;
33
+ };
34
+
35
+ type LinkBlockGroup = {
36
+ key: string;
37
+ title?: string;
38
+ items: LinkBlockItem[];
39
+ };
40
+ ```
41
+
42
+ - `items` must be a non-empty array for a group to render. Empty groups are skipped (no empty surface, no orphan title).
43
+ - `key` is stable for React lists; the parent chooses the string.
44
+
45
+ ## Props
46
+
47
+ | Prop | Type | Required | Notes |
48
+ |---|---|---|---|
49
+ | `blocks` | `LinkBlockGroup[]` | yes | One or more groups |
50
+ | `surfaceColor` | `string` | yes | Rounded container fill |
51
+ | `labelColor` | `string` | yes | Row label |
52
+ | `dividerColor` | `string` | yes | Hairline between rows |
53
+ | `chevronColor` | `string` | yes | Trailing chevron |
54
+ | `titleColor` | `string` | no | Section title above a block. Required in practice when any group has `title`; if a titled group is rendered without `titleColor`, fall back to `labelColor` |
55
+ | `radius` | `ButtonRadius` | no | Default `"xl"` (reuse `buttonRadiusValue`) |
56
+ | `style` | `StyleProp<ViewStyle>` | no | Outer stack wrapper |
57
+
58
+ Reuse `ButtonRadius` / `buttonRadiusValue` from the Button module so radius stays consistent across components.
59
+
60
+ ## Layout and behaviour
61
+
62
+ ### Stack
63
+
64
+ - Vertical stack of groups with a fixed gap (16).
65
+ - Each group: optional title, then the rounded surface.
66
+
67
+ ### Title
68
+
69
+ - Outside the surface, above it.
70
+ - Caption-sized type (13), weight 600.
71
+ - Colour: `titleColor` if provided, else `labelColor`.
72
+
73
+ ### Surface
74
+
75
+ - `backgroundColor: surfaceColor`.
76
+ - `borderRadius` from `radius`.
77
+ - Full width of the parent.
78
+ - Overflow hidden so row presses respect the radius.
79
+
80
+ ### Row
81
+
82
+ - `TouchableOpacity` (or `Pressable`) with `accessibilityRole="button"`.
83
+ - Min height 52, horizontal padding 16, horizontal row: optional icon slot, flex label, chevron.
84
+ - Icon slot: fixed width (~28) when any item in that group has an icon; otherwise no leading gutter. If only some items have icons, keep the slot so labels align.
85
+ - Label: font size 17, colour `labelColor`.
86
+ - Chevron: package-owned `›` (or equivalent), colour `chevronColor`, not selectable.
87
+ - `disabled`: non-interactive, opacity `0.4` (same treatment as `Button`).
88
+
89
+ ### Dividers
90
+
91
+ - Between consecutive items only (not above the first or below the last).
92
+ - Hairline height (`StyleSheet.hairlineWidth`), colour `dividerColor`.
93
+ - Leading inset so the line starts after the icon column (when present) plus a small gap — not full-bleed to the left edge. Trailing edge can meet the padding.
94
+
95
+ ### Out of scope (v1)
96
+
97
+ - Premium / gradient / locked row variants
98
+ - Swipe actions, badges, or trailing custom accessories (chevron only)
99
+ - Built-in navigation or `href`
100
+ - Looking up theme roles inside the package
101
+ - Migrating Ribe Profile onto LinkBlocks (consumer can do that later)
102
+
103
+ ## Package layout
104
+
105
+ | Path | Job |
106
+ |---|---|
107
+ | `src/components/LinkBlocks/LinkBlocks.tsx` | Component |
108
+ | `src/components/LinkBlocks/types.ts` | `LinkBlockItem`, `LinkBlockGroup`, props |
109
+ | `src/components/LinkBlocks/index.ts` | Barrel |
110
+ | `src/components/index.ts` | Re-export |
111
+ | `src/index.ts` | Public export |
112
+ | `docs/components/link-blocks.md` | Usage doc (linked from README) |
113
+
114
+ ## Example (app)
115
+
116
+ ```tsx
117
+ import { LinkBlocks } from "@studio-piot/native-ui";
118
+ import { useThemeColor } from "@/hooks/useThemeColor";
119
+ import Ionicons from "@expo/vector-icons/Ionicons";
120
+ import { router } from "expo-router";
121
+
122
+ const surface = useThemeColor("surface");
123
+ const text = useThemeColor("text");
124
+ const textSecondary = useThemeColor("textSecondary");
125
+ const border = useThemeColor("border");
126
+
127
+ <LinkBlocks
128
+ surfaceColor={surface}
129
+ labelColor={text}
130
+ dividerColor={border}
131
+ chevronColor={textSecondary}
132
+ titleColor={textSecondary}
133
+ blocks={[
134
+ {
135
+ key: "settings",
136
+ title: "Settings",
137
+ items: [
138
+ {
139
+ key: "units",
140
+ label: "Units of Measure",
141
+ icon: <Ionicons name="options-outline" size={22} color={text} />,
142
+ onPress: () => router.push("/settings/units"),
143
+ },
144
+ {
145
+ key: "notifications",
146
+ label: "Notification Settings",
147
+ icon: <Ionicons name="notifications-outline" size={22} color={text} />,
148
+ onPress: () => router.push("/settings/notifications"),
149
+ },
150
+ ],
151
+ },
152
+ ]}
153
+ />
154
+ ```
155
+
156
+ On a Stone (or grey) page background, the app passes a lighter `surface` (e.g. white). On a white page, the app chooses a contrasting surface — the package does not compute “lighter than background”.
157
+
158
+ ## Verification
159
+
160
+ - `npm test` and `npm run build` in the package succeed.
161
+ - Typecheck a consumer that renders one titled multi-item block and one untitled single-item block.
162
+ - Manual: pressed row fires `onPress`; disabled row does not; dividers and title placement match the reference layout.
163
+
164
+ ## Related
165
+
166
+ - Button + theme: `docs/superpowers/specs/2026-09-24-theme-provider-button-design.md`
167
+ - Usage (after implementation): `docs/components/link-blocks.md`
@@ -0,0 +1,178 @@
1
+ # Theme provider and Button — design
2
+
3
+ Date: 2026-09-24
4
+ Package: `@studio-piot/native-ui`
5
+ Status: approved
6
+
7
+ ## Goal
8
+
9
+ Add a project-agnostic theme layer and the first reusable component, `Button`. Apps own their palettes and semantic roles. The package resolves light/dark from React Native and renders controls from colours the caller passes in. No app-specific brand names, role names, or palettes live in this package.
10
+
11
+ ## Decisions
12
+
13
+ | Topic | Choice |
14
+ |---|---|
15
+ | Theme injection | Package `ThemeProvider` + `useThemeColor` / `useColorScheme` |
16
+ | Colour ownership | Apps supply `{ light, dark }` maps of string roles → hex (or any colour string) |
17
+ | Button colours | Required props (`backgroundColor`, `labelColor`); not looked up from theme roles |
18
+ | Scheme source | React Native `useColorScheme`; anything other than `"dark"` is light |
19
+ | Disabled | Opacity on the control; no disabled colour roles |
20
+ | Width | Always full width of the parent (`width: "100%"`). Not configurable |
21
+ | Radius | Prop `radius` with Tailwind-style steps (see below). Default `"xl"` |
22
+ | Layout | Package-owned colour-free `StyleSheet` for the button shell and label |
23
+ | Brand coupling | None. No brand colour names or app-named roles in the package |
24
+
25
+ ## Theme
26
+
27
+ ### `ThemeProvider`
28
+
29
+ ```tsx
30
+ type ThemeColors = Record<string, string>;
31
+
32
+ type ThemeProviderProps<T extends ThemeColors = ThemeColors> = {
33
+ colors: { light: T; dark: T };
34
+ children: React.ReactNode;
35
+ };
36
+ ```
37
+
38
+ - Mounts a React context with the `colors` maps.
39
+ - Does not read storage or set `Appearance`. Apps that override the window scheme (for example with `Appearance.setColorScheme`) keep doing that themselves; this provider only supplies maps.
40
+ - Using `useThemeColor` or `useColorScheme` outside the provider throws a clear error.
41
+
42
+ ### `useColorScheme`
43
+
44
+ Returns `"light" | "dark"`. Reads React Native’s `useColorScheme` and treats every value other than `"dark"` as light (`null`, `undefined`, `"unspecified"`).
45
+
46
+ ### `useThemeColor`
47
+
48
+ ```ts
49
+ function useThemeColor<T extends ThemeColors>(
50
+ colorName: keyof T & string,
51
+ overrides?: { light?: string; dark?: string },
52
+ ): string
53
+ ```
54
+
55
+ Resolution:
56
+
57
+ 1. Resolved scheme from `useColorScheme()`.
58
+ 2. If `overrides` has a string for that scheme, return it.
59
+ 3. Otherwise return `colors[scheme][colorName]`.
60
+
61
+ A missing role is a runtime error (no silent fallback colour). TypeScript narrows `colorName` when the app passes a typed `colors` map into the provider (via generics on the provider / context).
62
+
63
+ Apps may keep their own theme hooks. The package hook is optional for call sites that want one shared API across Studio native apps.
64
+
65
+ ## Button
66
+
67
+ Presentational. Theme-aware only in the sense that callers typically resolve colours with `useThemeColor` (package or app) before passing them in.
68
+
69
+ ### Props
70
+
71
+ | Prop | Type | Required | Notes |
72
+ |---|---|---|---|
73
+ | `label` | `string` | yes | Visible label |
74
+ | `onPress` | `() => void` | yes | |
75
+ | `backgroundColor` | `string` | yes | Filled: fill. Outline: border and label |
76
+ | `labelColor` | `string` | yes | Filled: label and spinner. Outline: unused for the label (label uses `backgroundColor`) |
77
+ | `disabled` | `boolean` | no | Default `false` |
78
+ | `loading` | `boolean` | no | Default `false`; shows a spinner instead of the label |
79
+ | `variant` | `"filled" \| "outline"` | no | Default `"filled"` |
80
+ | `radius` | `ButtonRadius` | no | Default `"xl"`. See radius table |
81
+ | `style` | `StyleProp<ViewStyle>` | no | Merged after the package shell style |
82
+ | `labelStyle` | `StyleProp<TextStyle>` | no | Merged after the package label style |
83
+
84
+ `ButtonRadius` = `"none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full"`.
85
+
86
+ ### Behaviour
87
+
88
+ - **Filled:** background `backgroundColor`; label and spinner `labelColor`.
89
+ - **Outline:** transparent fill; border width 1 and label colour `backgroundColor`; spinner uses `backgroundColor`.
90
+ - **Disabled or loading:** control is non-interactive; opacity reduced (for example `0.4`). Same colours as enabled — no separate disabled palette.
91
+ - Accessibility: `accessibilityRole="button"`; `accessibilityState` reflects `disabled` and `busy` when loading.
92
+ - Uses React Native `TouchableOpacity`, `Text`, and `ActivityIndicator`. No app-local text components.
93
+
94
+ ### Layout (package stylesheet)
95
+
96
+ Colour-free defaults:
97
+
98
+ - Shell: always `width: "100%"` of its parent, min height `52`, centred content
99
+ - Label: font size `17`, font weight `600`
100
+
101
+ Radius follows Tailwind’s named scale, mapped to density-independent pixels (same ratios as Tailwind’s rem steps at a 16px root):
102
+
103
+ | `radius` | Border radius | Tailwind analogue |
104
+ |---|---|---|
105
+ | `none` | `0` | `rounded-none` |
106
+ | `sm` | `2` | `rounded-sm` |
107
+ | `md` | `6` | `rounded-md` |
108
+ | `lg` | `8` | `rounded-lg` |
109
+ | `xl` | `12` | `rounded-xl` (default) |
110
+ | `2xl` | `16` | `rounded-2xl` |
111
+ | `3xl` | `24` | `rounded-3xl` |
112
+ | `full` | `999` | `rounded-full` (pill) |
113
+
114
+ Call sites may still pass `style` / `labelStyle` for spacing overrides. They must not rely on `style` to change width: the package always stretches to the parent.
115
+
116
+ ### Example (app-side)
117
+
118
+ ```tsx
119
+ import { Button, ThemeProvider, useThemeColor } from "@studio-piot/native-ui";
120
+ import { Colors } from "./config/colors";
121
+
122
+ function SaveButton({ onPress }: { onPress: () => void }) {
123
+ return (
124
+ <Button
125
+ label="Save"
126
+ onPress={onPress}
127
+ backgroundColor={useThemeColor("buttonBackground")}
128
+ labelColor={useThemeColor("buttonLabel")}
129
+ />
130
+ );
131
+ }
132
+
133
+ // Root
134
+ <ThemeProvider colors={Colors}>
135
+ <App />
136
+ </ThemeProvider>
137
+ ```
138
+
139
+ Role names such as `buttonBackground` belong to the app’s `Colors` type, not to this package.
140
+
141
+ ## Package layout
142
+
143
+ | Path | Job |
144
+ |---|---|
145
+ | `src/theme/ThemeProvider.tsx` | Context + provider |
146
+ | `src/theme/useColorScheme.ts` | Light/dark resolution |
147
+ | `src/theme/useThemeColor.ts` | Role → colour |
148
+ | `src/theme/index.ts` | Theme exports |
149
+ | `src/components/Button/Button.tsx` | Button |
150
+ | `src/components/Button/buttonRadius.ts` | Radius name → number |
151
+ | `src/components/Button/index.ts` | Button barrel |
152
+ | `src/components/index.ts` | Component barrel |
153
+ | `src/index.ts` | Public entry (`version`, theme, components) |
154
+
155
+ Exports:
156
+
157
+ - `@studio-piot/native-ui` — theme helpers, `Button`, `version`
158
+ - `@studio-piot/native-ui/components` — components only
159
+
160
+ Peer dependencies remain `react` and `react-native`. No new runtime dependencies.
161
+
162
+ ## Out of scope
163
+
164
+ - Shipping any brand palette or semantic role catalogue
165
+ - Preference storage or `Appearance.setColorScheme` inside the package
166
+ - Migrating app screens onto the package Button (consumer apps do that separately)
167
+ - Other components (inputs, text, layouts)
168
+ - Visual regression / Storybook
169
+
170
+ ## Verification
171
+
172
+ - `npm run build` in the package succeeds.
173
+ - Typecheck of a consumer that mounts `ThemeProvider` with a typed `Colors` map and renders `Button` with resolved colour props.
174
+ - Manual: filled and outline, enabled / disabled / loading, light and dark scheme (scheme change via the app’s own preference or the simulator appearance).
175
+
176
+ ## Consumer note (linking)
177
+
178
+ Local apps may depend on the package via `file:` (or a published version) and must keep Metro `watchFolders` / `nodeModulesPaths` pointed at the package when it lives outside the app root, as already set up for local development.
@@ -0,0 +1,136 @@
1
+ # RadioField — design
2
+
3
+ Date: 2026-09-25
4
+ Package: `@studio-piot/native-ui`
5
+ Status: approved
6
+ Apps: Ribe Appearance screen migration in the same pass
7
+
8
+ ## Goal
9
+
10
+ Add a presentational **RadioField**: one controlled radio group in a LinkBlocks-style rounded card. Label (and optional description) on the left, radio control on the right. Colours follow Field’s `field*` theme roles, plus one new selected-control role. Migrate Ribe’s Appearance (theme preference) screen to use it.
11
+
12
+ ## Decisions
13
+
14
+ | Topic | Choice |
15
+ |---|---|
16
+ | API shape | Single group: `value` + `onChange` + `options[]` |
17
+ | Layout | LinkBlocks card: rounded surface, hairline dividers, no icon column |
18
+ | Row content | Label + optional description left; radio right |
19
+ | Colours | Theme defaults from Field roles; optional prop overrides (Field pattern) |
20
+ | Selected colour | New theme role `fieldRadioSelected` (not reused from focus/border) |
21
+ | Radio drawing | Package-drawn Views (ring + selected fill/dot); no icon libraries |
22
+ | Radius | Reuse `ButtonRadius` / `buttonRadiusValue`. Default `"xl"` |
23
+ | Width | Always full width of the parent |
24
+ | Disabled | Per-option `disabled` → opacity `0.4` on that row |
25
+ | Multi-group | Out of scope (no RadioBlocks) |
26
+ | Brand coupling | None |
27
+
28
+ ## Architecture
29
+
30
+ ```
31
+ RadioField
32
+ ├── optional group label (above card)
33
+ └── card surface
34
+ └── option rows (divider between)
35
+ ├── label + description
36
+ └── RadioMark (selected / unselected)
37
+ ```
38
+
39
+ - Public export: `RadioField` (+ types). Internal mark helper need not be public.
40
+ - Controlled only: `value` and `onChange` required.
41
+ - Resolves colours via `ThemeProvider` when override props are omitted (same approach as current Field).
42
+
43
+ ## Theme roles
44
+
45
+ | Usage | Theme role |
46
+ |---|---|
47
+ | Card fill | `fieldBackground` |
48
+ | Option label | `fieldText` |
49
+ | Option description | `fieldDescription` |
50
+ | Dividers / unselected ring | `fieldBorder` |
51
+ | Selected radio fill | `fieldRadioSelected` (**new**) |
52
+ | Optional group label above card | `fieldLabel` |
53
+
54
+ Apps must add `fieldRadioSelected` to light and dark maps. Existing Field `field*` roles are reused.
55
+
56
+ ### Optional colour overrides
57
+
58
+ | Prop | Default role |
59
+ |---|---|
60
+ | `backgroundColor` | `fieldBackground` |
61
+ | `labelColor` | `fieldText` |
62
+ | `descriptionColor` | `fieldDescription` |
63
+ | `dividerColor` | `fieldBorder` |
64
+ | `radioColor` | `fieldBorder` (unselected ring) |
65
+ | `selectedColor` | `fieldRadioSelected` |
66
+ | `titleColor` | `fieldLabel` (group label above card) |
67
+
68
+ ## Props
69
+
70
+ ```ts
71
+ type RadioOption<T extends string = string> = {
72
+ value: T;
73
+ label: string;
74
+ description?: string;
75
+ disabled?: boolean;
76
+ };
77
+
78
+ type RadioFieldProps<T extends string = string> = {
79
+ value: T | null;
80
+ onChange: (value: T) => void;
81
+ options: RadioOption<T>[];
82
+ label?: string;
83
+ radius?: ButtonRadius;
84
+ // optional colour overrides (table above)
85
+ style?: StyleProp<ViewStyle>;
86
+ };
87
+ ```
88
+
89
+ | Prop | Notes |
90
+ |---|---|
91
+ | `value` | Controlled selection; `null` = none selected |
92
+ | `onChange` | Fires with the tapped option’s `value` |
93
+ | `options` | Stable `value` strings; empty list renders nothing (no empty card) |
94
+ | `label` | Optional caption above the card (LinkBlocks title density) |
95
+ | `radius` | Default `"xl"` (12) |
96
+
97
+ ## Layout
98
+
99
+ Aligned with LinkBlocks row metrics:
100
+
101
+ - Card: full width, `overflow: "hidden"`, radius from `buttonRadiusValue`
102
+ - Row: `minHeight` 52, horizontal padding 16, label column `flex: 1`, radio trailing
103
+ - Label: font size 17; description: font size 13 under label, `fieldDescription`
104
+ - Divider: hairline between rows, full width of the card content edge (no icon inset)
105
+ - Radio mark: ~22pt outer diameter; unselected = ring (`fieldBorder`); selected = filled circle (`fieldRadioSelected`) with small center hole in `fieldBackground`
106
+
107
+ Gap between stacked group label and card: 8 (same as LinkBlocks).
108
+
109
+ ## Accessibility
110
+
111
+ - Card / list container: `accessibilityRole="radiogroup"`
112
+ - Each row: `accessibilityRole="radio"`, `accessibilityState={{ selected, disabled }}`, `accessibilityLabel` from option label (description may be included in the accessibility label or as hint where platform supports it)
113
+ - Radio mark is decorative (`accessibilityElementsHidden` / `importantForAccessibility="no"`); the row is the hit target
114
+
115
+ ## App work (Ribe)
116
+
117
+ 1. Add `fieldRadioSelected` to `ThemeColors` light/dark (e.g. charcoal light / stone dark — solid control matching the reference).
118
+ 2. Replace Appearance’s checkmark `Pressable` list with `RadioField` for `automatic` | `light` | `dark`.
119
+ 3. Keep current option labels; optionally add a short description on Automatic for parity with “Use Device Settings” style copy (product choice at implement time; default to no new copy unless already decided).
120
+ 4. Ensure Field `field*` roles already present (from Field migration) remain.
121
+
122
+ ## Out of scope
123
+
124
+ - Multi-group / titled RadioBlocks
125
+ - Leading icons
126
+ - Multi-select
127
+ - Migrating Optimum or other apps
128
+ - Liquid-glass / native UIKit radio controls
129
+ - Keyboard form chrome (Form) changes
130
+
131
+ ## Success criteria
132
+
133
+ - Package exports `RadioField` with usage docs
134
+ - Theme defaults documented; `fieldRadioSelected` required for selected state
135
+ - Ribe Appearance uses `RadioField` and looks like the LinkBlocks card + radio reference
136
+ - `npm test` / `npm run build` (package) and Ribe `pnpm typecheck` pass