@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,414 @@
1
+ # LinkBlocks Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Ship a presentational `LinkBlocks` component driven by a config of titled groups and tappable rows (optional icon, label, chevron), with colour props from the parent.
6
+
7
+ **Architecture:** Same pattern as `Button`: package owns layout; parent passes colours and `onPress`. Reuse `buttonRadiusValue` for surface radius. Pure helper filters empty groups for testability without a React Native renderer.
8
+
9
+ **Tech Stack:** TypeScript, React 19+, React Native, `tsc` → `dist`, Node test runner for pure helpers.
10
+
11
+ ## Global Constraints
12
+
13
+ - No brand names, palettes, or app role catalogues in the package
14
+ - No Expo Router, Linking, or icon library dependencies
15
+ - Navigation is `onPress` per item only
16
+ - Icons are optional `React.ReactNode`
17
+ - Colours are props: `surfaceColor`, `labelColor`, `dividerColor`, `chevronColor`, optional `titleColor`
18
+ - Width always `100%` of parent; radius reuses `ButtonRadius` (default `"xl"`)
19
+ - Empty `items` groups are skipped (no surface, no title)
20
+ - Spec: `docs/superpowers/specs/2026-09-24-link-blocks-design.md`
21
+ - Commit only when the user asks
22
+
23
+ ## File map
24
+
25
+ | File | Responsibility |
26
+ |---|---|
27
+ | `src/components/LinkBlocks/types.ts` | `LinkBlockItem`, `LinkBlockGroup`, `LinkBlocksProps` |
28
+ | `src/components/LinkBlocks/filterGroups.ts` | Pure filter: drop groups with no items |
29
+ | `src/components/LinkBlocks/filterGroups.test.ts` | Node tests |
30
+ | `src/components/LinkBlocks/LinkBlocks.tsx` | UI |
31
+ | `src/components/LinkBlocks/index.ts` | Barrel |
32
+ | `src/components/index.ts` | Re-export |
33
+ | `src/index.ts` | Public export |
34
+ | `docs/components/link-blocks.md` | Usage |
35
+ | `README.md` | Link to usage + design |
36
+
37
+ ---
38
+
39
+ ### Task 1: LinkBlocks types, filter helper, and component
40
+
41
+ **Files:**
42
+ - Create: `src/components/LinkBlocks/types.ts`
43
+ - Create: `src/components/LinkBlocks/filterGroups.ts`
44
+ - Create: `src/components/LinkBlocks/filterGroups.test.ts`
45
+ - Create: `src/components/LinkBlocks/LinkBlocks.tsx`
46
+ - Create: `src/components/LinkBlocks/index.ts`
47
+ - Modify: `src/components/index.ts`
48
+ - Modify: `src/index.ts`
49
+
50
+ **Interfaces:**
51
+ - Consumes: `buttonRadiusValue`, `ButtonRadius` from `../Button/buttonRadius`
52
+ - Produces: `LinkBlocks`, `LinkBlocksProps`, `LinkBlockItem`, `LinkBlockGroup`, `filterLinkBlockGroups`
53
+
54
+ - [ ] **Step 1: Write failing filter tests**
55
+
56
+ ```ts
57
+ // src/components/LinkBlocks/filterGroups.test.ts
58
+ import assert from "node:assert/strict";
59
+ import { describe, it } from "node:test";
60
+ import { filterLinkBlockGroups } from "./filterGroups.ts";
61
+ import type { LinkBlockGroup } from "./types.ts";
62
+
63
+ describe("filterLinkBlockGroups", () => {
64
+ it("drops groups with empty items", () => {
65
+ const blocks: LinkBlockGroup[] = [
66
+ { key: "a", title: "A", items: [] },
67
+ {
68
+ key: "b",
69
+ items: [{ key: "1", label: "One", onPress: () => undefined }],
70
+ },
71
+ ];
72
+ const next = filterLinkBlockGroups(blocks);
73
+ assert.equal(next.length, 1);
74
+ assert.equal(next[0]?.key, "b");
75
+ });
76
+
77
+ it("keeps titled and untitled non-empty groups in order", () => {
78
+ const blocks: LinkBlockGroup[] = [
79
+ {
80
+ key: "t",
81
+ title: "Settings",
82
+ items: [{ key: "1", label: "One", onPress: () => undefined }],
83
+ },
84
+ {
85
+ key: "u",
86
+ items: [{ key: "2", label: "Two", onPress: () => undefined }],
87
+ },
88
+ ];
89
+ assert.deepEqual(
90
+ filterLinkBlockGroups(blocks).map((g) => g.key),
91
+ ["t", "u"],
92
+ );
93
+ });
94
+ });
95
+ ```
96
+
97
+ - [ ] **Step 2: Run — expect fail**
98
+
99
+ ```bash
100
+ node --experimental-strip-types --test src/components/LinkBlocks/filterGroups.test.ts
101
+ ```
102
+
103
+ Expected: FAIL (module not found).
104
+
105
+ - [ ] **Step 3: Add types + filter**
106
+
107
+ ```ts
108
+ // src/components/LinkBlocks/types.ts
109
+ import type { ReactNode } from "react";
110
+ import type { StyleProp, ViewStyle } from "react-native";
111
+ import type { ButtonRadius } from "../Button/buttonRadius";
112
+
113
+ export type LinkBlockItem = {
114
+ key: string;
115
+ label: string;
116
+ onPress: () => void;
117
+ icon?: ReactNode;
118
+ disabled?: boolean;
119
+ };
120
+
121
+ export type LinkBlockGroup = {
122
+ key: string;
123
+ title?: string;
124
+ items: LinkBlockItem[];
125
+ };
126
+
127
+ export type LinkBlocksProps = {
128
+ blocks: LinkBlockGroup[];
129
+ surfaceColor: string;
130
+ labelColor: string;
131
+ dividerColor: string;
132
+ chevronColor: string;
133
+ titleColor?: string;
134
+ radius?: ButtonRadius;
135
+ style?: StyleProp<ViewStyle>;
136
+ };
137
+ ```
138
+
139
+ ```ts
140
+ // src/components/LinkBlocks/filterGroups.ts
141
+ import type { LinkBlockGroup } from "./types";
142
+
143
+ export function filterLinkBlockGroups(
144
+ blocks: LinkBlockGroup[],
145
+ ): LinkBlockGroup[] {
146
+ return blocks.filter((group) => group.items.length > 0);
147
+ }
148
+ ```
149
+
150
+ - [ ] **Step 4: Re-run filter tests — expect pass**
151
+
152
+ ```bash
153
+ node --experimental-strip-types --test src/components/LinkBlocks/filterGroups.test.ts
154
+ ```
155
+
156
+ - [ ] **Step 5: Implement `LinkBlocks.tsx`**
157
+
158
+ ```tsx
159
+ import {
160
+ StyleSheet,
161
+ Text,
162
+ TouchableOpacity,
163
+ View,
164
+ } from "react-native";
165
+ import { buttonRadiusValue } from "../Button/buttonRadius";
166
+ import { filterLinkBlockGroups } from "./filterGroups";
167
+ import type { LinkBlockGroup, LinkBlocksProps } from "./types";
168
+
169
+ const ICON_SLOT = 28;
170
+ const ROW_PAD_H = 16;
171
+
172
+ export function LinkBlocks({
173
+ blocks,
174
+ surfaceColor,
175
+ labelColor,
176
+ dividerColor,
177
+ chevronColor,
178
+ titleColor,
179
+ radius = "xl",
180
+ style,
181
+ }: LinkBlocksProps) {
182
+ const visible = filterLinkBlockGroups(blocks);
183
+ const borderRadius = buttonRadiusValue(radius);
184
+ const resolvedTitleColor = titleColor ?? labelColor;
185
+
186
+ return (
187
+ <View style={[styles.stack, style]}>
188
+ {visible.map((group) => (
189
+ <Group
190
+ key={group.key}
191
+ group={group}
192
+ surfaceColor={surfaceColor}
193
+ labelColor={labelColor}
194
+ dividerColor={dividerColor}
195
+ chevronColor={chevronColor}
196
+ titleColor={resolvedTitleColor}
197
+ borderRadius={borderRadius}
198
+ />
199
+ ))}
200
+ </View>
201
+ );
202
+ }
203
+
204
+ function Group({
205
+ group,
206
+ surfaceColor,
207
+ labelColor,
208
+ dividerColor,
209
+ chevronColor,
210
+ titleColor,
211
+ borderRadius,
212
+ }: {
213
+ group: LinkBlockGroup;
214
+ surfaceColor: string;
215
+ labelColor: string;
216
+ dividerColor: string;
217
+ chevronColor: string;
218
+ titleColor: string;
219
+ borderRadius: number;
220
+ }) {
221
+ const showIconSlot = group.items.some((item) => item.icon != null);
222
+ const dividerInset = ROW_PAD_H + (showIconSlot ? ICON_SLOT + 12 : 0);
223
+
224
+ return (
225
+ <View style={styles.group}>
226
+ {group.title ? (
227
+ <Text style={[styles.title, { color: titleColor }]}>{group.title}</Text>
228
+ ) : null}
229
+ <View
230
+ style={[
231
+ styles.surface,
232
+ { backgroundColor: surfaceColor, borderRadius },
233
+ ]}
234
+ >
235
+ {group.items.map((item, index) => (
236
+ <View key={item.key}>
237
+ {index > 0 ? (
238
+ <View
239
+ style={[
240
+ styles.divider,
241
+ {
242
+ backgroundColor: dividerColor,
243
+ marginLeft: dividerInset,
244
+ },
245
+ ]}
246
+ />
247
+ ) : null}
248
+ <TouchableOpacity
249
+ accessibilityRole="button"
250
+ accessibilityState={{ disabled: !!item.disabled }}
251
+ activeOpacity={0.75}
252
+ disabled={item.disabled}
253
+ onPress={item.onPress}
254
+ style={[styles.row, item.disabled && styles.blocked]}
255
+ >
256
+ {showIconSlot ? (
257
+ <View style={styles.iconSlot}>{item.icon ?? null}</View>
258
+ ) : null}
259
+ <Text style={[styles.label, { color: labelColor, flex: 1 }]}>
260
+ {item.label}
261
+ </Text>
262
+ <Text
263
+ accessibilityElementsHidden
264
+ importantForAccessibility="no"
265
+ style={[styles.chevron, { color: chevronColor }]}
266
+ >
267
+ ›
268
+ </Text>
269
+ </TouchableOpacity>
270
+ </View>
271
+ ))}
272
+ </View>
273
+ </View>
274
+ );
275
+ }
276
+
277
+ const styles = StyleSheet.create({
278
+ stack: {
279
+ width: "100%",
280
+ gap: 16,
281
+ },
282
+ group: {
283
+ width: "100%",
284
+ gap: 8,
285
+ },
286
+ title: {
287
+ fontSize: 13,
288
+ fontWeight: "600",
289
+ paddingHorizontal: 4,
290
+ },
291
+ surface: {
292
+ width: "100%",
293
+ overflow: "hidden",
294
+ },
295
+ row: {
296
+ minHeight: 52,
297
+ paddingHorizontal: ROW_PAD_H,
298
+ flexDirection: "row",
299
+ alignItems: "center",
300
+ gap: 12,
301
+ },
302
+ blocked: {
303
+ opacity: 0.4,
304
+ },
305
+ iconSlot: {
306
+ width: ICON_SLOT,
307
+ alignItems: "center",
308
+ justifyContent: "center",
309
+ },
310
+ label: {
311
+ fontSize: 17,
312
+ },
313
+ chevron: {
314
+ fontSize: 22,
315
+ fontWeight: "300",
316
+ lineHeight: 24,
317
+ },
318
+ divider: {
319
+ height: StyleSheet.hairlineWidth,
320
+ },
321
+ });
322
+ ```
323
+
324
+ - [ ] **Step 6: Export from barrels**
325
+
326
+ ```ts
327
+ // src/components/LinkBlocks/index.ts
328
+ export { LinkBlocks } from "./LinkBlocks";
329
+ export type {
330
+ LinkBlockGroup,
331
+ LinkBlockItem,
332
+ LinkBlocksProps,
333
+ } from "./types";
334
+ export { filterLinkBlockGroups } from "./filterGroups";
335
+ ```
336
+
337
+ ```ts
338
+ // src/components/index.ts
339
+ export {
340
+ Button,
341
+ type ButtonProps,
342
+ buttonRadiusValue,
343
+ type ButtonRadius,
344
+ } from "./Button";
345
+ export {
346
+ LinkBlocks,
347
+ filterLinkBlockGroups,
348
+ type LinkBlockGroup,
349
+ type LinkBlockItem,
350
+ type LinkBlocksProps,
351
+ } from "./LinkBlocks";
352
+ ```
353
+
354
+ ```ts
355
+ // src/index.ts — add alongside Button exports:
356
+ export {
357
+ LinkBlocks,
358
+ filterLinkBlockGroups,
359
+ type LinkBlockGroup,
360
+ type LinkBlockItem,
361
+ type LinkBlocksProps,
362
+ } from "./components/LinkBlocks";
363
+ ```
364
+
365
+ - [ ] **Step 7: Verify**
366
+
367
+ ```bash
368
+ npm test && npm run build
369
+ ```
370
+
371
+ Expected: all existing tests plus filter suite pass; `dist/components/LinkBlocks/` emitted.
372
+
373
+ ---
374
+
375
+ ### Task 2: Usage docs and README
376
+
377
+ **Files:**
378
+ - Create: `docs/components/link-blocks.md`
379
+ - Modify: `README.md` (Docs section)
380
+ - Confirm: design spec status is `approved`
381
+
382
+ - [ ] **Step 1: Write usage doc** covering props table, data shape, colour wiring with `useThemeColor`, example with `onPress` + optional icon `ReactNode`. Link to the design spec.
383
+
384
+ - [ ] **Step 2: README** — add:
385
+ - [LinkBlocks usage](./docs/components/link-blocks.md)
386
+ - [Design: LinkBlocks](./docs/superpowers/specs/2026-09-24-link-blocks-design.md)
387
+
388
+ - [ ] **Step 3: Final verify**
389
+
390
+ ```bash
391
+ npm test && npm run build && npm pack --dry-run
392
+ ```
393
+
394
+ Expected: docs appear in tarball; build clean.
395
+
396
+ ---
397
+
398
+ ## Spec coverage
399
+
400
+ | Spec requirement | Task |
401
+ |---|---|
402
+ | Config `blocks` / item / group types | 1 |
403
+ | Colour props + title fallback | 1 |
404
+ | Skip empty groups | 1 |
405
+ | Title outside surface, rows, chevron, dividers, radius, disabled opacity | 1 |
406
+ | Exports | 1 |
407
+ | Usage doc + README | 2 |
408
+ | No router / brand / icon deps | Global |
409
+
410
+ ## Self-review
411
+
412
+ - No TBD placeholders
413
+ - Types align with spec (`LinkBlockItem`, `LinkBlockGroup`, `LinkBlocksProps`)
414
+ - Ribe Profile migration remains out of scope