@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,493 @@
1
+ # Theme provider and Button 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 project-agnostic `ThemeProvider` / `useThemeColor` / `useColorScheme` API and a presentational `Button` that takes colours as props, with Tailwind-style radius steps and full-width layout.
6
+
7
+ **Architecture:** Apps pass `{ light, dark }` colour maps into `ThemeProvider`. Pure helpers resolve scheme and roles. `Button` never looks up theme roles — callers pass `backgroundColor` and `labelColor`. Package layout styles stay colour-free.
8
+
9
+ **Tech Stack:** TypeScript, React 19+, React Native 0.76+, `tsc` build to `dist`, Node test runner for pure helpers.
10
+
11
+ ## Global Constraints
12
+
13
+ - No brand names, palettes, or app-specific role catalogues in the package
14
+ - Peer deps only: `react` >= 19, `react-native` >= 0.76; no new runtime dependencies
15
+ - Button width is always `"100%"` of its parent (not a prop)
16
+ - Button colours come only from props (`backgroundColor`, `labelColor`)
17
+ - Radius values: `none` | `sm` | `md` | `lg` | `xl` | `2xl` | `3xl` | `full` (default `xl`)
18
+ - Disabled / loading: opacity (e.g. `0.4`), same colours
19
+ - Spec: `docs/superpowers/specs/2026-09-24-theme-provider-button-design.md`
20
+ - Usage doc: `docs/components/button.md` (keep in sync if API drifts)
21
+ - Commit only when the user asks
22
+ - Remove `drafts/` WIP button leftovers once the real `Button` exists
23
+
24
+ ## File map
25
+
26
+ | File | Responsibility |
27
+ |---|---|
28
+ | `src/theme/types.ts` | `ThemeColors`, provider value types |
29
+ | `src/theme/resolveColorScheme.ts` | Pure `string \| null \| undefined` → `"light" \| "dark"` |
30
+ | `src/theme/resolveColorScheme.test.ts` | Node tests for scheme helper |
31
+ | `src/theme/ThemeProvider.tsx` | Context + provider |
32
+ | `src/theme/useColorScheme.ts` | Hook wrapping RN + `resolveColorScheme` |
33
+ | `src/theme/useThemeColor.ts` | Role → colour with overrides |
34
+ | `src/theme/index.ts` | Theme barrel |
35
+ | `src/components/buttonRadius.ts` | Radius name → number map + type |
36
+ | `src/components/buttonRadius.test.ts` | Node tests for radius map |
37
+ | `src/components/Button.tsx` | Button component |
38
+ | `src/components/index.ts` | Export `Button` |
39
+ | `src/index.ts` | Public entry |
40
+ | `tsconfig.json` | Ensure RN types resolve (`moduleResolution: "bundler"`) |
41
+ | `package.json` | `test` script; include `docs` in `files` if publishing docs |
42
+ | Delete `drafts/` | Remove parked WIP |
43
+
44
+ ---
45
+
46
+ ### Task 1: Theme scheme helper + provider + hooks
47
+
48
+ **Files:**
49
+ - Create: `src/theme/types.ts`
50
+ - Create: `src/theme/resolveColorScheme.ts`
51
+ - Create: `src/theme/resolveColorScheme.test.ts`
52
+ - Create: `src/theme/ThemeProvider.tsx`
53
+ - Create: `src/theme/useColorScheme.ts`
54
+ - Create: `src/theme/useThemeColor.ts`
55
+ - Create: `src/theme/index.ts`
56
+ - Modify: `package.json` (add `"test": "node --experimental-strip-types --test src/**/*.test.ts"`)
57
+ - Modify: `tsconfig.json` (`moduleResolution`: `"bundler"`, keep emit)
58
+
59
+ **Interfaces:**
60
+ - Produces:
61
+ - `resolveColorScheme(scheme: string | null | undefined): "light" | "dark"`
62
+ - `ThemeColors = Record<string, string>`
63
+ - `ThemeProvider({ colors: { light: T; dark: T }, children })`
64
+ - `useColorScheme(): "light" | "dark"`
65
+ - `useThemeColor(colorName: string, overrides?: { light?: string; dark?: string }): string`
66
+
67
+ - [ ] **Step 1: Write failing scheme tests**
68
+
69
+ ```ts
70
+ // src/theme/resolveColorScheme.test.ts
71
+ import assert from "node:assert/strict";
72
+ import { describe, it } from "node:test";
73
+ import { resolveColorScheme } from "./resolveColorScheme.ts";
74
+
75
+ describe("resolveColorScheme", () => {
76
+ it("returns dark only for dark", () => {
77
+ assert.equal(resolveColorScheme("dark"), "dark");
78
+ });
79
+
80
+ it("treats everything else as light", () => {
81
+ assert.equal(resolveColorScheme("light"), "light");
82
+ assert.equal(resolveColorScheme(null), "light");
83
+ assert.equal(resolveColorScheme(undefined), "light");
84
+ assert.equal(resolveColorScheme("unspecified"), "light");
85
+ });
86
+ });
87
+ ```
88
+
89
+ - [ ] **Step 2: Run tests — expect fail**
90
+
91
+ ```bash
92
+ node --experimental-strip-types --test src/theme/resolveColorScheme.test.ts
93
+ ```
94
+
95
+ Expected: FAIL (module not found).
96
+
97
+ - [ ] **Step 3: Implement scheme helper**
98
+
99
+ ```ts
100
+ // src/theme/resolveColorScheme.ts
101
+ export function resolveColorScheme(
102
+ scheme: string | null | undefined,
103
+ ): "light" | "dark" {
104
+ return scheme === "dark" ? "dark" : "light";
105
+ }
106
+ ```
107
+
108
+ - [ ] **Step 4: Re-run tests — expect pass**
109
+
110
+ ```bash
111
+ node --experimental-strip-types --test src/theme/resolveColorScheme.test.ts
112
+ ```
113
+
114
+ Expected: PASS.
115
+
116
+ - [ ] **Step 5: Add theme types + provider + hooks**
117
+
118
+ ```ts
119
+ // src/theme/types.ts
120
+ export type ThemeColors = Record<string, string>;
121
+
122
+ export type ThemeContextValue = {
123
+ colors: { light: ThemeColors; dark: ThemeColors };
124
+ };
125
+ ```
126
+
127
+ ```tsx
128
+ // src/theme/ThemeProvider.tsx
129
+ import { createContext, createElement, type ReactNode } from "react";
130
+ import type { ThemeColors, ThemeContextValue } from "./types";
131
+
132
+ export const ThemeContext = createContext<ThemeContextValue | null>(null);
133
+
134
+ export function ThemeProvider({
135
+ colors,
136
+ children,
137
+ }: {
138
+ colors: { light: ThemeColors; dark: ThemeColors };
139
+ children: ReactNode;
140
+ }) {
141
+ return createElement(
142
+ ThemeContext.Provider,
143
+ { value: { colors } },
144
+ children,
145
+ );
146
+ }
147
+ ```
148
+
149
+ ```ts
150
+ // src/theme/useColorScheme.ts
151
+ import { useContext } from "react";
152
+ import { useColorScheme as useSystemColorScheme } from "react-native";
153
+ import { resolveColorScheme } from "./resolveColorScheme";
154
+ import { ThemeContext } from "./ThemeProvider";
155
+
156
+ export function useColorScheme(): "light" | "dark" {
157
+ if (!useContext(ThemeContext)) {
158
+ throw new Error("useColorScheme must be used within ThemeProvider");
159
+ }
160
+ return resolveColorScheme(useSystemColorScheme());
161
+ }
162
+ ```
163
+
164
+ ```ts
165
+ // src/theme/useThemeColor.ts
166
+ import { useContext } from "react";
167
+ import { ThemeContext } from "./ThemeProvider";
168
+ import { useColorScheme } from "./useColorScheme";
169
+
170
+ export function useThemeColor(
171
+ colorName: string,
172
+ overrides?: { light?: string; dark?: string },
173
+ ): string {
174
+ const ctx = useContext(ThemeContext);
175
+ if (!ctx) {
176
+ throw new Error("useThemeColor must be used within ThemeProvider");
177
+ }
178
+ const scheme = useColorScheme();
179
+ const override = overrides?.[scheme];
180
+ if (override != null) {
181
+ return override;
182
+ }
183
+ const value = ctx.colors[scheme][colorName];
184
+ if (value == null) {
185
+ throw new Error(`Unknown theme colour "${colorName}" for scheme "${scheme}"`);
186
+ }
187
+ return value;
188
+ }
189
+ ```
190
+
191
+ ```ts
192
+ // src/theme/index.ts
193
+ export type { ThemeColors, ThemeContextValue } from "./types";
194
+ export { ThemeProvider } from "./ThemeProvider";
195
+ export { useColorScheme } from "./useColorScheme";
196
+ export { useThemeColor } from "./useThemeColor";
197
+ export { resolveColorScheme } from "./resolveColorScheme";
198
+ ```
199
+
200
+ - [ ] **Step 6: Fix `tsconfig.json` + add test script; build**
201
+
202
+ Set `"moduleResolution": "bundler"` (keep `commonjs` module emit if that is what Metro consumers expect today, or keep current emit settings except resolution). Add `react-native` to `devDependencies` so `tsc` can resolve types during package builds:
203
+
204
+ ```bash
205
+ npm install --save-dev react-native@0.86.3
206
+ ```
207
+
208
+ ```json
209
+ "scripts": {
210
+ "test": "node --experimental-strip-types --test 'src/**/*.test.ts'"
211
+ }
212
+ ```
213
+
214
+ ```bash
215
+ npm test && npm run build
216
+ ```
217
+
218
+ Expected: tests pass; `dist/theme/*` emitted.
219
+
220
+ ---
221
+
222
+ ### Task 2: Button radius map + Button component
223
+
224
+ **Files:**
225
+ - Create: `src/components/buttonRadius.ts`
226
+ - Create: `src/components/buttonRadius.test.ts`
227
+ - Create: `src/components/Button.tsx`
228
+ - Modify: `src/components/index.ts`
229
+ - Delete: `drafts/` (entire folder)
230
+ - Delete: any leftover `src/components/button.tsx` if present
231
+
232
+ **Interfaces:**
233
+ - Consumes: React Native primitives only (no theme hooks inside Button)
234
+ - Produces:
235
+ - `ButtonRadius` type and `buttonRadiusValue(radius: ButtonRadius): number`
236
+ - `Button` props per spec (`label`, `onPress`, `backgroundColor`, `labelColor`, `disabled?`, `loading?`, `variant?`, `radius?`, `style?`, `labelStyle?`)
237
+
238
+ - [ ] **Step 1: Write failing radius tests**
239
+
240
+ ```ts
241
+ // src/components/buttonRadius.test.ts
242
+ import assert from "node:assert/strict";
243
+ import { describe, it } from "node:test";
244
+ import { buttonRadiusValue } from "./buttonRadius.ts";
245
+
246
+ describe("buttonRadiusValue", () => {
247
+ it("maps Tailwind-style steps", () => {
248
+ assert.equal(buttonRadiusValue("none"), 0);
249
+ assert.equal(buttonRadiusValue("sm"), 2);
250
+ assert.equal(buttonRadiusValue("md"), 6);
251
+ assert.equal(buttonRadiusValue("lg"), 8);
252
+ assert.equal(buttonRadiusValue("xl"), 12);
253
+ assert.equal(buttonRadiusValue("2xl"), 16);
254
+ assert.equal(buttonRadiusValue("3xl"), 24);
255
+ assert.equal(buttonRadiusValue("full"), 999);
256
+ });
257
+ });
258
+ ```
259
+
260
+ - [ ] **Step 2: Run — expect fail; then implement map**
261
+
262
+ ```ts
263
+ // src/components/buttonRadius.ts
264
+ export type ButtonRadius =
265
+ | "none"
266
+ | "sm"
267
+ | "md"
268
+ | "lg"
269
+ | "xl"
270
+ | "2xl"
271
+ | "3xl"
272
+ | "full";
273
+
274
+ const RADIUS: Record<ButtonRadius, number> = {
275
+ none: 0,
276
+ sm: 2,
277
+ md: 6,
278
+ lg: 8,
279
+ xl: 12,
280
+ "2xl": 16,
281
+ "3xl": 24,
282
+ full: 999,
283
+ };
284
+
285
+ export function buttonRadiusValue(radius: ButtonRadius): number {
286
+ return RADIUS[radius];
287
+ }
288
+ ```
289
+
290
+ ```bash
291
+ node --experimental-strip-types --test src/components/buttonRadius.test.ts
292
+ ```
293
+
294
+ Expected: PASS.
295
+
296
+ - [ ] **Step 3: Implement `Button.tsx`**
297
+
298
+ ```tsx
299
+ import {
300
+ ActivityIndicator,
301
+ StyleSheet,
302
+ Text,
303
+ TouchableOpacity,
304
+ type StyleProp,
305
+ type TextStyle,
306
+ type ViewStyle,
307
+ } from "react-native";
308
+ import { buttonRadiusValue, type ButtonRadius } from "./buttonRadius";
309
+
310
+ export type ButtonProps = {
311
+ label: string;
312
+ onPress: () => void;
313
+ backgroundColor: string;
314
+ labelColor: string;
315
+ disabled?: boolean;
316
+ loading?: boolean;
317
+ variant?: "filled" | "outline";
318
+ radius?: ButtonRadius;
319
+ style?: StyleProp<ViewStyle>;
320
+ labelStyle?: StyleProp<TextStyle>;
321
+ };
322
+
323
+ export function Button({
324
+ label,
325
+ onPress,
326
+ backgroundColor,
327
+ labelColor,
328
+ disabled = false,
329
+ loading = false,
330
+ variant = "filled",
331
+ radius = "xl",
332
+ style,
333
+ labelStyle,
334
+ }: ButtonProps) {
335
+ const blocked = disabled || loading;
336
+ const isOutline = variant === "outline";
337
+ const borderRadius = buttonRadiusValue(radius);
338
+
339
+ return (
340
+ <TouchableOpacity
341
+ accessibilityRole="button"
342
+ accessibilityState={{ disabled: blocked, busy: loading }}
343
+ activeOpacity={0.75}
344
+ disabled={blocked}
345
+ onPress={onPress}
346
+ style={[
347
+ styles.shell,
348
+ { borderRadius },
349
+ isOutline
350
+ ? {
351
+ backgroundColor: "transparent",
352
+ borderWidth: 1,
353
+ borderColor: backgroundColor,
354
+ }
355
+ : { backgroundColor },
356
+ blocked && styles.blocked,
357
+ style,
358
+ ]}
359
+ >
360
+ {loading ? (
361
+ <ActivityIndicator
362
+ color={isOutline ? backgroundColor : labelColor}
363
+ size="small"
364
+ />
365
+ ) : (
366
+ <Text
367
+ style={[
368
+ styles.label,
369
+ { color: isOutline ? backgroundColor : labelColor },
370
+ labelStyle,
371
+ ]}
372
+ >
373
+ {label}
374
+ </Text>
375
+ )}
376
+ </TouchableOpacity>
377
+ );
378
+ }
379
+
380
+ const styles = StyleSheet.create({
381
+ shell: {
382
+ width: "100%",
383
+ minHeight: 52,
384
+ alignItems: "center",
385
+ justifyContent: "center",
386
+ },
387
+ blocked: {
388
+ opacity: 0.4,
389
+ },
390
+ label: {
391
+ fontSize: 17,
392
+ fontWeight: "600",
393
+ },
394
+ });
395
+ ```
396
+
397
+ - [ ] **Step 4: Export from barrels and public entry**
398
+
399
+ ```ts
400
+ // src/components/index.ts
401
+ export { Button, type ButtonProps } from "./Button";
402
+ export { buttonRadiusValue, type ButtonRadius } from "./buttonRadius";
403
+ ```
404
+
405
+ ```ts
406
+ // src/index.ts
407
+ export const version = "0.1.0";
408
+
409
+ export * from "./theme";
410
+ export { Button, type ButtonProps } from "./components/Button";
411
+ export {
412
+ buttonRadiusValue,
413
+ type ButtonRadius,
414
+ } from "./components/buttonRadius";
415
+ export * as components from "./components";
416
+ ```
417
+
418
+ Remove `drafts/` and any dead lowercase `button.tsx`.
419
+
420
+ - [ ] **Step 5: Build + test**
421
+
422
+ ```bash
423
+ npm test && npm run build
424
+ ```
425
+
426
+ Expected: all tests pass; `dist/components/Button.js` and theme files exist; package entry exports `Button`, `ThemeProvider`, `useThemeColor`.
427
+
428
+ ---
429
+
430
+ ### Task 3: Docs + package metadata sync
431
+
432
+ **Files:**
433
+ - Verify: `docs/components/button.md` matches the implemented API
434
+ - Modify: `README.md` — link usage + design; drop outdated Ribe/`link:` notes; show `file:` / npm install generically
435
+ - Modify: `package.json` `files` to include `docs`
436
+ - Modify: design spec status line to `approved`
437
+
438
+ - [ ] **Step 1: Update README**
439
+
440
+ Replace the Local development / Ribe-specific section with generic install + docs links:
441
+
442
+ ```md
443
+ ## Docs
444
+
445
+ - [Theme provider and Button usage](./docs/components/button.md)
446
+ - [Design: theme provider and Button](./docs/superpowers/specs/2026-09-24-theme-provider-button-design.md)
447
+ ```
448
+
449
+ Keep install, publish, license. Example import should show `Button` + `ThemeProvider`.
450
+
451
+ - [ ] **Step 2: Confirm usage doc**
452
+
453
+ Skim `docs/components/button.md` against Task 2 props. Fix any drift (radius table, prop names).
454
+
455
+ - [ ] **Step 3: Mark design approved + include docs in publish**
456
+
457
+ In the design spec header: `Status: approved`.
458
+
459
+ In `package.json`:
460
+
461
+ ```json
462
+ "files": ["dist", "src", "docs", "README.md", "LICENSE"]
463
+ ```
464
+
465
+ - [ ] **Step 4: Final verify**
466
+
467
+ ```bash
468
+ npm test && npm run build && npm pack --dry-run
469
+ ```
470
+
471
+ Expected: docs appear in the tarball listing; build clean.
472
+
473
+ ---
474
+
475
+ ## Spec coverage
476
+
477
+ | Spec requirement | Task |
478
+ |---|---|
479
+ | ThemeProvider + context | 1 |
480
+ | useColorScheme / resolveColorScheme | 1 |
481
+ | useThemeColor + overrides + missing role error | 1 |
482
+ | Button colour props, variants, loading/disabled opacity | 2 |
483
+ | Full width | 2 |
484
+ | Tailwind radius scale + default `xl` | 2 |
485
+ | Exports / barrels | 2 |
486
+ | Usage documentation | 3 (and pre-written `docs/components/button.md`) |
487
+ | No brand coupling | Global + all tasks |
488
+
489
+ ## Self-review
490
+
491
+ - No TBD / placeholder steps
492
+ - Types aligned: `ButtonRadius`, `ButtonProps`, `ThemeColors`
493
+ - Consumer app wiring (Ribe `ThemeProvider` mount) is out of scope per spec — not in this plan