@studio-piot/native-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +91 -0
- package/dist/components/Button/Button.d.ts +16 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +44 -0
- package/dist/components/Button/Button.js.map +1 -0
- package/dist/components/Button/buttonRadius.d.ts +3 -0
- package/dist/components/Button/buttonRadius.d.ts.map +1 -0
- package/dist/components/Button/buttonRadius.js +17 -0
- package/dist/components/Button/buttonRadius.js.map +1 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Button/index.js +8 -0
- package/dist/components/Button/index.js.map +1 -0
- package/dist/components/Field/FieldText.d.ts +3 -0
- package/dist/components/Field/FieldText.d.ts.map +1 -0
- package/dist/components/Field/FieldText.js +9 -0
- package/dist/components/Field/FieldText.js.map +1 -0
- package/dist/components/Field/FieldTextarea.d.ts +3 -0
- package/dist/components/Field/FieldTextarea.d.ts.map +1 -0
- package/dist/components/Field/FieldTextarea.js +10 -0
- package/dist/components/Field/FieldTextarea.js.map +1 -0
- package/dist/components/Field/fieldChrome.d.ts +10 -0
- package/dist/components/Field/fieldChrome.d.ts.map +1 -0
- package/dist/components/Field/fieldChrome.js +100 -0
- package/dist/components/Field/fieldChrome.js.map +1 -0
- package/dist/components/Field/fieldThemeRoles.d.ts +13 -0
- package/dist/components/Field/fieldThemeRoles.d.ts.map +1 -0
- package/dist/components/Field/fieldThemeRoles.js +15 -0
- package/dist/components/Field/fieldThemeRoles.js.map +1 -0
- package/dist/components/Field/index.d.ts +6 -0
- package/dist/components/Field/index.d.ts.map +1 -0
- package/dist/components/Field/index.js +10 -0
- package/dist/components/Field/index.js.map +1 -0
- package/dist/components/Field/resolveFieldBorder.d.ts +16 -0
- package/dist/components/Field/resolveFieldBorder.d.ts.map +1 -0
- package/dist/components/Field/resolveFieldBorder.js +20 -0
- package/dist/components/Field/resolveFieldBorder.js.map +1 -0
- package/dist/components/Field/resolveFieldColor.d.ts +7 -0
- package/dist/components/Field/resolveFieldColor.d.ts.map +1 -0
- package/dist/components/Field/resolveFieldColor.js +21 -0
- package/dist/components/Field/resolveFieldColor.js.map +1 -0
- package/dist/components/Field/types.d.ts +46 -0
- package/dist/components/Field/types.d.ts.map +1 -0
- package/dist/components/Field/types.js +3 -0
- package/dist/components/Field/types.js.map +1 -0
- package/dist/components/Field/useResolvedFieldColor.d.ts +2 -0
- package/dist/components/Field/useResolvedFieldColor.d.ts.map +1 -0
- package/dist/components/Field/useResolvedFieldColor.js +15 -0
- package/dist/components/Field/useResolvedFieldColor.js.map +1 -0
- package/dist/components/Form/Form.d.ts +3 -0
- package/dist/components/Form/Form.d.ts.map +1 -0
- package/dist/components/Form/Form.js +100 -0
- package/dist/components/Form/Form.js.map +1 -0
- package/dist/components/Form/FormCloseButton.d.ts +4 -0
- package/dist/components/Form/FormCloseButton.d.ts.map +1 -0
- package/dist/components/Form/FormCloseButton.js +17 -0
- package/dist/components/Form/FormCloseButton.js.map +1 -0
- package/dist/components/Form/index.d.ts +4 -0
- package/dist/components/Form/index.d.ts.map +1 -0
- package/dist/components/Form/index.js +8 -0
- package/dist/components/Form/index.js.map +1 -0
- package/dist/components/Form/resolveThemeColor.d.ts +11 -0
- package/dist/components/Form/resolveThemeColor.d.ts.map +1 -0
- package/dist/components/Form/resolveThemeColor.js +21 -0
- package/dist/components/Form/resolveThemeColor.js.map +1 -0
- package/dist/components/Form/types.d.ts +30 -0
- package/dist/components/Form/types.d.ts.map +1 -0
- package/dist/components/Form/types.js +3 -0
- package/dist/components/Form/types.js.map +1 -0
- package/dist/components/Form/useResolvedThemeColor.d.ts +2 -0
- package/dist/components/Form/useResolvedThemeColor.d.ts.map +1 -0
- package/dist/components/Form/useResolvedThemeColor.js +15 -0
- package/dist/components/Form/useResolvedThemeColor.js.map +1 -0
- package/dist/components/Form/withAlpha.d.ts +2 -0
- package/dist/components/Form/withAlpha.d.ts.map +1 -0
- package/dist/components/Form/withAlpha.js +12 -0
- package/dist/components/Form/withAlpha.js.map +1 -0
- package/dist/components/LinkBlocks/LinkBlocks.d.ts +3 -0
- package/dist/components/LinkBlocks/LinkBlocks.d.ts.map +1 -0
- package/dist/components/LinkBlocks/LinkBlocks.js +75 -0
- package/dist/components/LinkBlocks/LinkBlocks.js.map +1 -0
- package/dist/components/LinkBlocks/filterGroups.d.ts +3 -0
- package/dist/components/LinkBlocks/filterGroups.d.ts.map +1 -0
- package/dist/components/LinkBlocks/filterGroups.js +7 -0
- package/dist/components/LinkBlocks/filterGroups.js.map +1 -0
- package/dist/components/LinkBlocks/index.d.ts +4 -0
- package/dist/components/LinkBlocks/index.d.ts.map +1 -0
- package/dist/components/LinkBlocks/index.js +8 -0
- package/dist/components/LinkBlocks/index.js.map +1 -0
- package/dist/components/LinkBlocks/types.d.ts +26 -0
- package/dist/components/LinkBlocks/types.d.ts.map +1 -0
- package/dist/components/LinkBlocks/types.js +3 -0
- package/dist/components/LinkBlocks/types.js.map +1 -0
- package/dist/components/RadioField/RadioField.d.ts +3 -0
- package/dist/components/RadioField/RadioField.d.ts.map +1 -0
- package/dist/components/RadioField/RadioField.js +76 -0
- package/dist/components/RadioField/RadioField.js.map +1 -0
- package/dist/components/RadioField/RadioMark.d.ts +7 -0
- package/dist/components/RadioField/RadioMark.d.ts.map +1 -0
- package/dist/components/RadioField/RadioMark.js +32 -0
- package/dist/components/RadioField/RadioMark.js.map +1 -0
- package/dist/components/RadioField/index.d.ts +4 -0
- package/dist/components/RadioField/index.d.ts.map +1 -0
- package/dist/components/RadioField/index.js +8 -0
- package/dist/components/RadioField/index.js.map +1 -0
- package/dist/components/RadioField/radioThemeRoles.d.ts +11 -0
- package/dist/components/RadioField/radioThemeRoles.d.ts.map +1 -0
- package/dist/components/RadioField/radioThemeRoles.js +13 -0
- package/dist/components/RadioField/radioThemeRoles.js.map +1 -0
- package/dist/components/RadioField/types.d.ts +24 -0
- package/dist/components/RadioField/types.d.ts.map +1 -0
- package/dist/components/RadioField/types.js +3 -0
- package/dist/components/RadioField/types.js.map +1 -0
- package/dist/components/Select/Select.d.ts +3 -0
- package/dist/components/Select/Select.d.ts.map +1 -0
- package/dist/components/Select/Select.js +137 -0
- package/dist/components/Select/Select.js.map +1 -0
- package/dist/components/Select/SelectCloseButton.d.ts +4 -0
- package/dist/components/Select/SelectCloseButton.d.ts.map +1 -0
- package/dist/components/Select/SelectCloseButton.js +17 -0
- package/dist/components/Select/SelectCloseButton.js.map +1 -0
- package/dist/components/Select/index.d.ts +6 -0
- package/dist/components/Select/index.d.ts.map +1 -0
- package/dist/components/Select/index.js +14 -0
- package/dist/components/Select/index.js.map +1 -0
- package/dist/components/Select/resolveSelectDetentIndex.d.ts +9 -0
- package/dist/components/Select/resolveSelectDetentIndex.d.ts.map +1 -0
- package/dist/components/Select/resolveSelectDetentIndex.js +16 -0
- package/dist/components/Select/resolveSelectDetentIndex.js.map +1 -0
- package/dist/components/Select/selectThemeRoles.d.ts +12 -0
- package/dist/components/Select/selectThemeRoles.d.ts.map +1 -0
- package/dist/components/Select/selectThemeRoles.js +14 -0
- package/dist/components/Select/selectThemeRoles.js.map +1 -0
- package/dist/components/Select/types.d.ts +35 -0
- package/dist/components/Select/types.d.ts.map +1 -0
- package/dist/components/Select/types.js +3 -0
- package/dist/components/Select/types.js.map +1 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +27 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +6 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/screenPadding.d.ts +3 -0
- package/dist/layout/screenPadding.d.ts.map +1 -0
- package/dist/layout/screenPadding.js +6 -0
- package/dist/layout/screenPadding.js.map +1 -0
- package/dist/theme/ThemeProvider.d.ts +11 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +11 -0
- package/dist/theme/ThemeProvider.js.map +1 -0
- package/dist/theme/index.d.ts +6 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +12 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/resolveColorScheme.d.ts +2 -0
- package/dist/theme/resolveColorScheme.d.ts.map +1 -0
- package/dist/theme/resolveColorScheme.js +7 -0
- package/dist/theme/resolveColorScheme.js.map +1 -0
- package/dist/theme/types.d.ts +8 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +3 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/theme/useColorScheme.d.ts +2 -0
- package/dist/theme/useColorScheme.d.ts.map +1 -0
- package/dist/theme/useColorScheme.js +16 -0
- package/dist/theme/useColorScheme.js.map +1 -0
- package/dist/theme/useThemeColor.d.ts +5 -0
- package/dist/theme/useThemeColor.d.ts.map +1 -0
- package/dist/theme/useThemeColor.js +25 -0
- package/dist/theme/useThemeColor.js.map +1 -0
- package/docs/components/button.md +162 -0
- package/docs/components/field.md +128 -0
- package/docs/components/form.md +86 -0
- package/docs/components/link-blocks.md +120 -0
- package/docs/components/radio-field.md +100 -0
- package/docs/components/select.md +59 -0
- package/docs/superpowers/plans/2026-09-24-field.md +583 -0
- package/docs/superpowers/plans/2026-09-24-link-blocks.md +414 -0
- package/docs/superpowers/plans/2026-09-24-theme-provider-button.md +493 -0
- package/docs/superpowers/plans/2026-09-25-radio-field.md +516 -0
- package/docs/superpowers/plans/2026-09-25-select.md +22 -0
- package/docs/superpowers/specs/2026-09-24-field-design.md +205 -0
- package/docs/superpowers/specs/2026-09-24-link-blocks-design.md +167 -0
- package/docs/superpowers/specs/2026-09-24-theme-provider-button-design.md +178 -0
- package/docs/superpowers/specs/2026-09-25-radio-field-design.md +136 -0
- package/docs/superpowers/specs/2026-09-25-select-design.md +164 -0
- package/package.json +75 -0
- package/src/components/Button/Button.tsx +97 -0
- package/src/components/Button/buttonRadius.test.ts +16 -0
- package/src/components/Button/buttonRadius.ts +24 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Field/FieldText.tsx +6 -0
- package/src/components/Field/FieldTextarea.tsx +18 -0
- package/src/components/Field/fieldChrome.tsx +219 -0
- package/src/components/Field/fieldThemeRoles.ts +13 -0
- package/src/components/Field/index.ts +5 -0
- package/src/components/Field/resolveFieldBorder.test.ts +76 -0
- package/src/components/Field/resolveFieldBorder.ts +40 -0
- package/src/components/Field/resolveFieldColor.test.ts +92 -0
- package/src/components/Field/resolveFieldColor.ts +26 -0
- package/src/components/Field/types.ts +53 -0
- package/src/components/Field/useResolvedFieldColor.ts +15 -0
- package/src/components/Form/Form.tsx +205 -0
- package/src/components/Form/FormCloseButton.tsx +19 -0
- package/src/components/Form/index.ts +3 -0
- package/src/components/Form/resolveThemeColor.ts +24 -0
- package/src/components/Form/types.ts +30 -0
- package/src/components/Form/useResolvedThemeColor.ts +15 -0
- package/src/components/Form/withAlpha.test.ts +13 -0
- package/src/components/Form/withAlpha.ts +9 -0
- package/src/components/LinkBlocks/LinkBlocks.tsx +165 -0
- package/src/components/LinkBlocks/filterGroups.test.ts +37 -0
- package/src/components/LinkBlocks/filterGroups.ts +7 -0
- package/src/components/LinkBlocks/index.ts +7 -0
- package/src/components/LinkBlocks/types.ts +28 -0
- package/src/components/RadioField/RadioField.tsx +168 -0
- package/src/components/RadioField/RadioMark.tsx +54 -0
- package/src/components/RadioField/index.ts +3 -0
- package/src/components/RadioField/radioThemeRoles.test.ts +15 -0
- package/src/components/RadioField/radioThemeRoles.ts +11 -0
- package/src/components/RadioField/types.ts +25 -0
- package/src/components/Select/Select.tsx +279 -0
- package/src/components/Select/SelectCloseButton.tsx +19 -0
- package/src/components/Select/index.ts +13 -0
- package/src/components/Select/resolveSelectDetentIndex.test.ts +20 -0
- package/src/components/Select/resolveSelectDetentIndex.ts +16 -0
- package/src/components/Select/selectThemeRoles.ts +12 -0
- package/src/components/Select/types.ts +39 -0
- package/src/components/index.ts +42 -0
- package/src/index.ts +50 -0
- package/src/layout/index.ts +1 -0
- package/src/layout/screenPadding.ts +2 -0
- package/src/theme/ThemeProvider.tsx +20 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/resolveColorScheme.test.ts +16 -0
- package/src/theme/resolveColorScheme.ts +5 -0
- package/src/theme/types.ts +5 -0
- package/src/theme/useColorScheme.ts +13 -0
- package/src/theme/useThemeColor.ts +25 -0
|
@@ -0,0 +1,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
|