@studio-piot/native-ui 0.2.0 → 0.2.2
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/README.md +3 -1
- package/dist/components/Select/Select.d.ts +3 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +72 -28
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/SelectCloseButton.d.ts +9 -3
- package/dist/components/Select/SelectCloseButton.d.ts.map +1 -1
- package/dist/components/Select/SelectCloseButton.js +2 -5
- package/dist/components/Select/SelectCloseButton.js.map +1 -1
- package/dist/components/Select/presentSelectSheet.d.ts +7 -0
- package/dist/components/Select/presentSelectSheet.d.ts.map +1 -0
- package/dist/components/Select/presentSelectSheet.js +14 -0
- package/dist/components/Select/presentSelectSheet.js.map +1 -0
- package/dist/components/Select/selectThemeRoles.d.ts +1 -1
- package/dist/components/Select/selectThemeRoles.js +1 -1
- package/dist/components/Select/selectThemeRoles.js.map +1 -1
- package/dist/components/Select/types.d.ts +2 -2
- package/dist/components/Select/types.d.ts.map +1 -1
- package/dist/components/Slider/Slider.d.ts +3 -0
- package/dist/components/Slider/Slider.d.ts.map +1 -0
- package/dist/components/Slider/Slider.js +46 -0
- package/dist/components/Slider/Slider.js.map +1 -0
- package/dist/components/Slider/index.d.ts +4 -0
- package/dist/components/Slider/index.d.ts.map +1 -0
- package/dist/components/Slider/index.js +8 -0
- package/dist/components/Slider/index.js.map +1 -0
- package/dist/components/Slider/sliderThemeRoles.d.ts +8 -0
- package/dist/components/Slider/sliderThemeRoles.d.ts.map +1 -0
- package/dist/components/Slider/sliderThemeRoles.js +10 -0
- package/dist/components/Slider/sliderThemeRoles.js.map +1 -0
- package/dist/components/Slider/types.d.ts +18 -0
- package/dist/components/Slider/types.d.ts.map +1 -0
- package/dist/components/Slider/types.js +3 -0
- package/dist/components/Slider/types.js.map +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -1
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/docs/components/select.md +11 -6
- package/docs/components/slider.md +65 -0
- package/docs/superpowers/plans/2026-09-25-slider.md +35 -0
- package/docs/superpowers/specs/2026-09-25-select-design.md +3 -3
- package/docs/superpowers/specs/2026-09-25-slider-design.md +117 -0
- package/package.json +6 -2
- package/src/components/Select/Select.tsx +133 -64
- package/src/components/Select/SelectCloseButton.tsx +15 -7
- package/src/components/Select/selectThemeRoles.test.ts +2 -2
- package/src/components/Select/selectThemeRoles.ts +1 -1
- package/src/components/Select/types.ts +2 -2
- package/src/components/Slider/Slider.tsx +95 -0
- package/src/components/Slider/index.ts +6 -0
- package/src/components/Slider/sliderThemeRoles.test.ts +12 -0
- package/src/components/Slider/sliderThemeRoles.ts +8 -0
- package/src/components/Slider/types.ts +18 -0
- package/src/components/index.ts +6 -0
- package/src/index.ts +7 -1
package/dist/index.js
CHANGED
|
@@ -36,8 +36,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.components = exports.resolveSelectDetentIndex = exports.detentsToSnapPoints = exports.DEFAULT_SELECT_INITIAL_DETENT = exports.DEFAULT_SELECT_DETENTS = exports.SELECT_THEME_ROLES = exports.SelectCloseButton = exports.Select = exports.RADIO_THEME_ROLES = exports.RadioField = exports.FormCloseButton = exports.Form = exports.FIELD_THEME_ROLES = exports.FieldTextarea = exports.FieldText = exports.filterLinkBlockGroups = exports.LinkBlocks = exports.buttonRadiusValue = exports.Button = exports.version = void 0;
|
|
40
|
-
exports.version = "0.2.
|
|
39
|
+
exports.components = exports.SLIDER_THEME_ROLES = exports.Slider = exports.resolveSelectDetentIndex = exports.detentsToSnapPoints = exports.DEFAULT_SELECT_INITIAL_DETENT = exports.DEFAULT_SELECT_DETENTS = exports.SELECT_THEME_ROLES = exports.SelectCloseButton = exports.Select = exports.RADIO_THEME_ROLES = exports.RadioField = exports.FormCloseButton = exports.Form = exports.FIELD_THEME_ROLES = exports.FieldTextarea = exports.FieldText = exports.filterLinkBlockGroups = exports.LinkBlocks = exports.buttonRadiusValue = exports.Button = exports.version = void 0;
|
|
40
|
+
exports.version = "0.2.2";
|
|
41
41
|
__exportStar(require("./theme"), exports);
|
|
42
42
|
var Button_1 = require("./components/Button");
|
|
43
43
|
Object.defineProperty(exports, "Button", { enumerable: true, get: function () { return Button_1.Button; } });
|
|
@@ -63,5 +63,8 @@ Object.defineProperty(exports, "DEFAULT_SELECT_DETENTS", { enumerable: true, get
|
|
|
63
63
|
Object.defineProperty(exports, "DEFAULT_SELECT_INITIAL_DETENT", { enumerable: true, get: function () { return Select_1.DEFAULT_SELECT_INITIAL_DETENT; } });
|
|
64
64
|
Object.defineProperty(exports, "detentsToSnapPoints", { enumerable: true, get: function () { return Select_1.detentsToSnapPoints; } });
|
|
65
65
|
Object.defineProperty(exports, "resolveSelectDetentIndex", { enumerable: true, get: function () { return Select_1.resolveSelectDetentIndex; } });
|
|
66
|
+
var Slider_1 = require("./components/Slider");
|
|
67
|
+
Object.defineProperty(exports, "Slider", { enumerable: true, get: function () { return Slider_1.Slider; } });
|
|
68
|
+
Object.defineProperty(exports, "SLIDER_THEME_ROLES", { enumerable: true, get: function () { return Slider_1.SLIDER_THEME_ROLES; } });
|
|
66
69
|
exports.components = __importStar(require("./components"));
|
|
67
70
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAa,QAAA,OAAO,GAAG,OAAO,CAAC;AAE/B,0CAAwB;AACxB,8CAK6B;AAJ3B,gGAAA,MAAM,OAAA;AAEN,2GAAA,iBAAiB,OAAA;AAGnB,sDAMiC;AAL/B,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAKvB,4CAQ4B;AAP1B,kGAAA,SAAS,OAAA;AACT,sGAAA,aAAa,OAAA;AACb,0GAAA,iBAAiB,OAAA;AAMnB,0CAI2B;AAHzB,4FAAA,IAAI,OAAA;AACJ,uGAAA,eAAe,OAAA;AAGjB,sDAMiC;AAL/B,wGAAA,UAAU,OAAA;AACV,+GAAA,iBAAiB,OAAA;AAKnB,8CAY6B;AAX3B,gGAAA,MAAM,OAAA;AACN,2GAAA,iBAAiB,OAAA;AACjB,4GAAA,kBAAkB,OAAA;AAClB,gHAAA,sBAAsB,OAAA;AACtB,uHAAA,6BAA6B,OAAA;AAC7B,6GAAA,mBAAmB,OAAA;AACnB,kHAAA,wBAAwB,OAAA;AAM1B,2DAA2C"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAa,QAAA,OAAO,GAAG,OAAO,CAAC;AAE/B,0CAAwB;AACxB,8CAK6B;AAJ3B,gGAAA,MAAM,OAAA;AAEN,2GAAA,iBAAiB,OAAA;AAGnB,sDAMiC;AAL/B,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAKvB,4CAQ4B;AAP1B,kGAAA,SAAS,OAAA;AACT,sGAAA,aAAa,OAAA;AACb,0GAAA,iBAAiB,OAAA;AAMnB,0CAI2B;AAHzB,4FAAA,IAAI,OAAA;AACJ,uGAAA,eAAe,OAAA;AAGjB,sDAMiC;AAL/B,wGAAA,UAAU,OAAA;AACV,+GAAA,iBAAiB,OAAA;AAKnB,8CAY6B;AAX3B,gGAAA,MAAM,OAAA;AACN,2GAAA,iBAAiB,OAAA;AACjB,4GAAA,kBAAkB,OAAA;AAClB,gHAAA,sBAAsB,OAAA;AACtB,uHAAA,6BAA6B,OAAA;AAC7B,6GAAA,mBAAmB,OAAA;AACnB,kHAAA,wBAAwB,OAAA;AAM1B,8CAK6B;AAJ3B,gGAAA,MAAM,OAAA;AACN,4GAAA,kBAAkB,OAAA;AAIpB,2DAA2C"}
|
|
@@ -9,19 +9,24 @@ For `ThemeProvider`, see [theme provider and Button usage](./button.md). Design:
|
|
|
9
9
|
Install in the app (Expo example):
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx expo install @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler
|
|
12
|
+
npx expo install @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler react-native-screens
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
On iOS, Select uses `FullWindowOverlay` so the sheet appears above React Navigation `fullScreenModal` screens.
|
|
16
|
+
|
|
17
|
+
Wrap the app root with `GestureHandlerRootView` and `BottomSheetModalProvider`. Put **`BottomSheetModalProvider` inside `ThemeProvider`** so sheet content (portaled) still resolves theme colours:
|
|
16
18
|
|
|
17
19
|
```tsx
|
|
18
20
|
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
|
21
|
+
import { ThemeProvider } from "@studio-piot/native-ui";
|
|
19
22
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
20
23
|
|
|
21
24
|
export default function RootLayout() {
|
|
22
25
|
return (
|
|
23
26
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
24
|
-
<
|
|
27
|
+
<ThemeProvider colors={Colors}>
|
|
28
|
+
<BottomSheetModalProvider>{/* … */}</BottomSheetModalProvider>
|
|
29
|
+
</ThemeProvider>
|
|
25
30
|
</GestureHandlerRootView>
|
|
26
31
|
);
|
|
27
32
|
}
|
|
@@ -31,7 +36,7 @@ Import `react-native-gesture-handler` at the top of the entry layout (before oth
|
|
|
31
36
|
|
|
32
37
|
## Theme roles
|
|
33
38
|
|
|
34
|
-
Uses Field roles: `fieldBackground`, `fieldText`, `fieldPlaceholder`, `fieldLabel`, `fieldDescription`, `fieldBorder`, `fieldError`, plus
|
|
39
|
+
Uses Field roles: `fieldBackground`, `fieldText`, `fieldPlaceholder`, `fieldLabel`, `fieldDescription`, `fieldBorder`, `fieldError`, plus **`fieldSelectSelected`** for the selected option check / label tint.
|
|
35
40
|
|
|
36
41
|
## Basic usage
|
|
37
42
|
|
|
@@ -59,9 +64,9 @@ Default is a dark disc with a white × (`SelectCloseButton`). Override:
|
|
|
59
64
|
closeButton={(dismiss) => <YourClose onPress={dismiss} />}
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
## Detents /
|
|
67
|
+
## Detents / height
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
Sheet height follows content (`enableDynamicSizing`), capped by the largest `detents` fraction (default `0.75` of the window). Dragging cannot expand past content (or that cap when the list is long).
|
|
65
70
|
|
|
66
71
|
## Imports
|
|
67
72
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Slider — usage
|
|
2
|
+
|
|
3
|
+
Controlled slider with label on the left and a live formatted value on the top right. Track uses `@react-native-community/slider`. Your app owns form state.
|
|
4
|
+
|
|
5
|
+
Design: [Slider design](../superpowers/specs/2026-09-25-slider-design.md).
|
|
6
|
+
|
|
7
|
+
## Peer dependency
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx expo install @react-native-community/slider
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Theme roles
|
|
14
|
+
|
|
15
|
+
| Usage | Role |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Label | `fieldLabel` |
|
|
18
|
+
| Value | `fieldText` |
|
|
19
|
+
| Filled track + thumb | **`fieldSliderHighlight`** (apps must add) |
|
|
20
|
+
| Unfilled track | `fieldBorder` |
|
|
21
|
+
|
|
22
|
+
## Basic usage
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Slider } from "@studio-piot/native-ui";
|
|
26
|
+
import { useState } from "react";
|
|
27
|
+
|
|
28
|
+
export function EffortSlider() {
|
|
29
|
+
const [effort, setEffort] = useState(5);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Slider
|
|
33
|
+
label="Effort"
|
|
34
|
+
value={effort}
|
|
35
|
+
onChange={setEffort}
|
|
36
|
+
minimumValue={1}
|
|
37
|
+
maximumValue={10}
|
|
38
|
+
step={1}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Optional colour overrides
|
|
45
|
+
|
|
46
|
+
| Prop | Default theme role |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `labelColor` | `fieldLabel` |
|
|
49
|
+
| `valueColor` | `fieldText` |
|
|
50
|
+
| `highlightColor` | `fieldSliderHighlight` |
|
|
51
|
+
| `trackColor` | `fieldBorder` |
|
|
52
|
+
|
|
53
|
+
### Props
|
|
54
|
+
|
|
55
|
+
| Prop | Type | Default | Notes |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `value` | `number` | — | Required |
|
|
58
|
+
| `onChange` | `(value: number) => void` | — | Required |
|
|
59
|
+
| `minimumValue` | `number` | — | Required |
|
|
60
|
+
| `maximumValue` | `number` | — | Required |
|
|
61
|
+
| `step` | `number` | — | Optional; omit for continuous |
|
|
62
|
+
| `label` | `string` | — | Optional |
|
|
63
|
+
| `formatValue` | `(value: number) => string` | `String` | Value display |
|
|
64
|
+
| `disabled` | `boolean` | `false` | |
|
|
65
|
+
| `style` | view style | — | Outer column |
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Slider Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** Inline execution in session; checkboxes track progress.
|
|
4
|
+
|
|
5
|
+
**Goal:** Ship controlled `Slider` (label left, formatted value top-right, community track) with `fieldSliderHighlight`, and wire Ribe create-activity Effort (1–10).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Presentational wrapper around `@react-native-community/slider`. Theme colours resolve like Field via `useResolvedFieldColor`. New role `fieldSliderHighlight` for filled track + thumb; unfilled track uses `fieldBorder`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, RN, `@react-native-community/slider` peer, Expo Ribe.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Spec: `docs/superpowers/specs/2026-09-25-slider-design.md`
|
|
14
|
+
- Peer `@react-native-community/slider`
|
|
15
|
+
- No brand in package; no custom-drawn track; no Optimum migration
|
|
16
|
+
- Commit only when the user asks
|
|
17
|
+
|
|
18
|
+
## File map
|
|
19
|
+
|
|
20
|
+
| File | Responsibility |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `src/components/Slider/sliderThemeRoles.ts` | Role map |
|
|
23
|
+
| `src/components/Slider/sliderThemeRoles.test.ts` | Role assertions |
|
|
24
|
+
| `src/components/Slider/types.ts` | `SliderProps` |
|
|
25
|
+
| `src/components/Slider/Slider.tsx` | Header + community Slider |
|
|
26
|
+
| `src/components/Slider/index.ts` | Barrel |
|
|
27
|
+
| Exports / README / `docs/components/slider.md` | Public surface |
|
|
28
|
+
| Ribe `colors.ts` (+ test) | `fieldSliderHighlight` |
|
|
29
|
+
| Ribe create-activity | Effort Slider |
|
|
30
|
+
| Ribe `package.json` | Install community slider |
|
|
31
|
+
|
|
32
|
+
## Tasks
|
|
33
|
+
|
|
34
|
+
- [x] Package Slider + peer + docs + exports + unit test
|
|
35
|
+
- [x] Ribe: theme role + Effort on create-activity + install peer
|
|
@@ -15,9 +15,9 @@ Ship a presentational **Select**: Field-like trigger that opens a Gorhom bottom
|
|
|
15
15
|
|---|---|
|
|
16
16
|
| API shape | Single `Select`: trigger + internal sheet |
|
|
17
17
|
| Sheet library | `@gorhom/bottom-sheet` (`BottomSheetModal`) |
|
|
18
|
-
|
|
|
18
|
+
| Sheet height | Content-sized (`enableDynamicSizing`); max = largest detent (default 75% of window) |
|
|
19
19
|
| Close control | `closeButton` slot; package default **DiscClose** (dark disc + white ×); Ribe uses default |
|
|
20
|
-
| Colours | Field `field*` roles + `
|
|
20
|
+
| Colours | Field `field*` roles + `fieldSelectSelected` for selected check / emphasis |
|
|
21
21
|
| Trigger | Field-like chrome (label, description, error, chevron, radius) |
|
|
22
22
|
| Options | `{ value, label, disabled? }[]`; scrollable when long |
|
|
23
23
|
| Select behaviour | Tap option → `onChange(value)` + dismiss |
|
|
@@ -55,7 +55,7 @@ Select
|
|
|
55
55
|
| Description under trigger | `fieldDescription` |
|
|
56
56
|
| Trigger border / dividers | `fieldBorder` |
|
|
57
57
|
| Error border + message | `fieldError` |
|
|
58
|
-
| Selected option emphasis / check | `
|
|
58
|
+
| Selected option emphasis / check | `fieldSelectSelected` |
|
|
59
59
|
|
|
60
60
|
Optional colour override props follow the Field pattern (explicit prop wins over theme role).
|
|
61
61
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Slider — design
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-25
|
|
4
|
+
Package: `@studio-piot/native-ui`
|
|
5
|
+
Status: approved
|
|
6
|
+
Apps: Ribe create-activity Effort field in the same pass
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Ship a presentational **Slider**: label on the left, live numeric value on the top right, community slider track below. Highlight (filled track + thumb) uses a dedicated theme role. Ribe adds an Effort control (1–10) on create-activity.
|
|
11
|
+
|
|
12
|
+
## Decisions
|
|
13
|
+
|
|
14
|
+
| Topic | Choice |
|
|
15
|
+
|---|---|
|
|
16
|
+
| API shape | Controlled: `value` + `onChange` + `minimumValue` / `maximumValue` |
|
|
17
|
+
| Track library | `@react-native-community/slider` (peer) |
|
|
18
|
+
| Step | Optional `step`; omit = continuous |
|
|
19
|
+
| Value display | `formatValue?: (value: number) => string`; default `String(value)` |
|
|
20
|
+
| Layout | Header row (label left, value right); track full width below |
|
|
21
|
+
| Min/max UI | Props only — no captions under the track |
|
|
22
|
+
| Highlight colour | New theme role `fieldSliderHighlight` |
|
|
23
|
+
| Unfilled track | `fieldBorder` |
|
|
24
|
+
| Label / value text | `fieldLabel` / `fieldText` |
|
|
25
|
+
| Disabled | Optional `disabled` → reduced opacity, non-interactive |
|
|
26
|
+
| Brand coupling | None in the package |
|
|
27
|
+
| Optimum LogForm | Out of scope this pass |
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Slider
|
|
33
|
+
├── header row
|
|
34
|
+
│ ├── label (optional)
|
|
35
|
+
│ └── formatValue(value)
|
|
36
|
+
└── @react-native-community/slider
|
|
37
|
+
├── minimumTrackTintColor / thumb → fieldSliderHighlight
|
|
38
|
+
└── maximumTrackTintColor → fieldBorder
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Public export: `Slider` (+ types, `SLIDER_THEME_ROLES`).
|
|
42
|
+
- Controlled only.
|
|
43
|
+
- Peer: `@react-native-community/slider` (document in usage docs; matching `devDependency` for types/build).
|
|
44
|
+
|
|
45
|
+
## Theme roles
|
|
46
|
+
|
|
47
|
+
| Usage | Theme role |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Label | `fieldLabel` |
|
|
50
|
+
| Value | `fieldText` |
|
|
51
|
+
| Filled track + thumb | `fieldSliderHighlight` (**new**) |
|
|
52
|
+
| Unfilled track | `fieldBorder` |
|
|
53
|
+
|
|
54
|
+
Apps must add `fieldSliderHighlight` to light and dark maps. Ribe uses flare for both schemes.
|
|
55
|
+
|
|
56
|
+
### Optional colour overrides
|
|
57
|
+
|
|
58
|
+
| Prop | Default role |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `labelColor` | `fieldLabel` |
|
|
61
|
+
| `valueColor` | `fieldText` |
|
|
62
|
+
| `highlightColor` | `fieldSliderHighlight` |
|
|
63
|
+
| `trackColor` | `fieldBorder` |
|
|
64
|
+
|
|
65
|
+
## Props
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
type SliderProps = {
|
|
69
|
+
value: number;
|
|
70
|
+
onChange: (value: number) => void;
|
|
71
|
+
minimumValue: number;
|
|
72
|
+
maximumValue: number;
|
|
73
|
+
step?: number;
|
|
74
|
+
label?: string;
|
|
75
|
+
formatValue?: (value: number) => string;
|
|
76
|
+
disabled?: boolean;
|
|
77
|
+
labelColor?: string;
|
|
78
|
+
valueColor?: string;
|
|
79
|
+
highlightColor?: string;
|
|
80
|
+
trackColor?: string;
|
|
81
|
+
style?: StyleProp<ViewStyle>;
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Prop | Notes |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `value` / `onChange` | Required. Controlled |
|
|
88
|
+
| `minimumValue` / `maximumValue` | Required. Passed through to community Slider |
|
|
89
|
+
| `step` | Optional; omit for continuous |
|
|
90
|
+
| `label` | Optional; omitted → value still shows top-right |
|
|
91
|
+
| `formatValue` | Default `String(value)` |
|
|
92
|
+
| `disabled` | Opacity ~0.4; slider not interactive |
|
|
93
|
+
|
|
94
|
+
## Layout
|
|
95
|
+
|
|
96
|
+
- Full width of the parent
|
|
97
|
+
- Header: `flexDirection: "row"`, label `flex: 1`, value right-aligned; font sizes aligned with Field (label 13 / semibold, value 17)
|
|
98
|
+
- Gap ~6 between header and track (Field-like)
|
|
99
|
+
- Track height from community Slider default (~40pt hit area)
|
|
100
|
+
|
|
101
|
+
## Accessibility
|
|
102
|
+
|
|
103
|
+
- Community Slider exposes the native control
|
|
104
|
+
- When `label` is set, pass it as `accessibilityLabel` on the slider (or combine label + formatted value)
|
|
105
|
+
- `accessibilityState.disabled` when `disabled`
|
|
106
|
+
|
|
107
|
+
## Ribe migration
|
|
108
|
+
|
|
109
|
+
- `Colors` light/dark: `fieldSliderHighlight: flare`
|
|
110
|
+
- create-activity: **Effort** between Type and Notes — `minimumValue={1}`, `maximumValue={10}`, `step={1}`, default value `5`, `formatValue={(n) => String(n)}`
|
|
111
|
+
- Install peer `@react-native-community/slider` (Expo-compatible version)
|
|
112
|
+
|
|
113
|
+
## Out of scope
|
|
114
|
+
|
|
115
|
+
- Custom-drawn track / dual thumbs / marks
|
|
116
|
+
- Migrating Optimum LogForm sliders
|
|
117
|
+
- Error / description helpers (can follow Field later if needed)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studio-piot/native-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Shared React Native components for Studio Piot apps.",
|
|
5
5
|
"homepage": "https://github.com/Studio-Piot/studio-native-ui#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -51,12 +51,14 @@
|
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@gorhom/bottom-sheet": "^5.2.14",
|
|
54
|
+
"@react-native-community/slider": "^5.2.0",
|
|
54
55
|
"react": ">=19.0.0",
|
|
55
56
|
"react-native": ">=0.76.0",
|
|
56
57
|
"react-native-gesture-handler": "^2.32.0",
|
|
57
58
|
"react-native-keyboard-controller": ">=1.14.0",
|
|
58
59
|
"react-native-reanimated": "^4.5.1",
|
|
59
|
-
"react-native-safe-area-context": ">=4.0.0"
|
|
60
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
61
|
+
"react-native-screens": ">=4.0.0"
|
|
60
62
|
},
|
|
61
63
|
"scripts": {
|
|
62
64
|
"test": "node --experimental-strip-types --test 'src/**/*.test.ts'",
|
|
@@ -68,12 +70,14 @@
|
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
70
72
|
"@gorhom/bottom-sheet": "^5.2.14",
|
|
73
|
+
"@react-native-community/slider": "^5.2.0",
|
|
71
74
|
"@types/react": "^19.2.2",
|
|
72
75
|
"react-native": "^0.86.3",
|
|
73
76
|
"react-native-gesture-handler": "^2.32.0",
|
|
74
77
|
"react-native-keyboard-controller": "1.21.9",
|
|
75
78
|
"react-native-reanimated": "^4.5.1",
|
|
76
79
|
"react-native-safe-area-context": "~5.7.0",
|
|
80
|
+
"react-native-screens": "~4.26.0",
|
|
77
81
|
"typescript": "^5.9.3"
|
|
78
82
|
}
|
|
79
83
|
}
|
|
@@ -5,26 +5,41 @@ import {
|
|
|
5
5
|
type ElementRef,
|
|
6
6
|
type ReactNode,
|
|
7
7
|
} from "react";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Platform,
|
|
10
|
+
Pressable,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
Text,
|
|
13
|
+
useWindowDimensions,
|
|
14
|
+
View,
|
|
15
|
+
} from "react-native";
|
|
9
16
|
import {
|
|
10
17
|
BottomSheetBackdrop,
|
|
18
|
+
BottomSheetHandle,
|
|
11
19
|
BottomSheetModal,
|
|
12
20
|
BottomSheetScrollView,
|
|
13
21
|
type BottomSheetBackdropProps,
|
|
22
|
+
type BottomSheetHandleProps,
|
|
14
23
|
} from "@gorhom/bottom-sheet";
|
|
24
|
+
import { FullWindowOverlay } from "react-native-screens";
|
|
15
25
|
import { buttonRadiusValue } from "../Button/buttonRadius";
|
|
16
26
|
import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
DEFAULT_SELECT_INITIAL_DETENT,
|
|
20
|
-
detentsToSnapPoints,
|
|
21
|
-
resolveSelectDetentIndex,
|
|
22
|
-
} from "./resolveSelectDetentIndex";
|
|
27
|
+
import { useThemeColor } from "../../theme/useThemeColor";
|
|
28
|
+
import { DEFAULT_SELECT_DETENTS } from "./resolveSelectDetentIndex";
|
|
23
29
|
import { SelectCloseButton } from "./SelectCloseButton";
|
|
24
30
|
import { SELECT_THEME_ROLES } from "./selectThemeRoles";
|
|
25
31
|
import type { SelectOption, SelectProps } from "./types";
|
|
26
32
|
|
|
27
33
|
const BACKDROP_OPACITY = 0.72;
|
|
34
|
+
const SHEET_TOP_RADIUS = 20;
|
|
35
|
+
|
|
36
|
+
/** Renders the sheet above React Navigation native modals (iOS). */
|
|
37
|
+
function SheetContainer({ children }: { children?: ReactNode }) {
|
|
38
|
+
if (Platform.OS === "ios") {
|
|
39
|
+
return <FullWindowOverlay>{children}</FullWindowOverlay>;
|
|
40
|
+
}
|
|
41
|
+
return <>{children}</>;
|
|
42
|
+
}
|
|
28
43
|
|
|
29
44
|
export function Select({
|
|
30
45
|
value,
|
|
@@ -36,8 +51,9 @@ export function Select({
|
|
|
36
51
|
error,
|
|
37
52
|
sheetTitle,
|
|
38
53
|
closeButton,
|
|
54
|
+
/** Max sheet height as a fraction of the window (largest value wins). Default `[0.25, 0.5, 0.75]` → 75%. */
|
|
39
55
|
detents = [...DEFAULT_SELECT_DETENTS],
|
|
40
|
-
initialDetent
|
|
56
|
+
initialDetent: _initialDetent,
|
|
41
57
|
radius = "xl",
|
|
42
58
|
editable = true,
|
|
43
59
|
backgroundColor: backgroundColorProp,
|
|
@@ -51,6 +67,7 @@ export function Select({
|
|
|
51
67
|
style,
|
|
52
68
|
}: SelectProps) {
|
|
53
69
|
const modalRef = useRef<ElementRef<typeof BottomSheetModal>>(null);
|
|
70
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
54
71
|
|
|
55
72
|
const backgroundColor = useResolvedFieldColor(
|
|
56
73
|
backgroundColorProp,
|
|
@@ -84,6 +101,9 @@ export function Select({
|
|
|
84
101
|
selectedColorProp,
|
|
85
102
|
SELECT_THEME_ROLES.selectedColor,
|
|
86
103
|
);
|
|
104
|
+
// Resolved on the trigger tree (has ThemeProvider), then passed into the portal.
|
|
105
|
+
const closeDiscColor = useThemeColor("text");
|
|
106
|
+
const closeIconColor = useThemeColor("background");
|
|
87
107
|
|
|
88
108
|
const hasError = Boolean(error);
|
|
89
109
|
const selected = options.find((option) => option.value === value);
|
|
@@ -91,8 +111,11 @@ export function Select({
|
|
|
91
111
|
const isPlaceholder = selected == null;
|
|
92
112
|
const borderRadius = buttonRadiusValue(radius);
|
|
93
113
|
const title = sheetTitle ?? label ?? "Select";
|
|
94
|
-
|
|
95
|
-
const
|
|
114
|
+
|
|
115
|
+
const maxDynamicContentSize = useMemo(() => {
|
|
116
|
+
const maxDetent = Math.max(...detents, 0.75);
|
|
117
|
+
return Math.round(windowHeight * maxDetent);
|
|
118
|
+
}, [detents, windowHeight]);
|
|
96
119
|
|
|
97
120
|
const dismiss = useCallback(() => {
|
|
98
121
|
modalRef.current?.dismiss();
|
|
@@ -119,7 +142,52 @@ export function Select({
|
|
|
119
142
|
const resolvedClose: ReactNode =
|
|
120
143
|
typeof closeButton === "function"
|
|
121
144
|
? closeButton(dismiss)
|
|
122
|
-
: (closeButton ??
|
|
145
|
+
: (closeButton ?? (
|
|
146
|
+
<SelectCloseButton
|
|
147
|
+
backgroundColor={closeDiscColor}
|
|
148
|
+
iconColor={closeIconColor}
|
|
149
|
+
onPress={dismiss}
|
|
150
|
+
/>
|
|
151
|
+
));
|
|
152
|
+
|
|
153
|
+
// Header lives in the handle so it stays fixed; only options scroll.
|
|
154
|
+
const renderHandle = useCallback(
|
|
155
|
+
(props: BottomSheetHandleProps) => (
|
|
156
|
+
<View
|
|
157
|
+
style={[
|
|
158
|
+
styles.handleRoot,
|
|
159
|
+
{
|
|
160
|
+
backgroundColor,
|
|
161
|
+
borderTopLeftRadius: SHEET_TOP_RADIUS,
|
|
162
|
+
borderTopRightRadius: SHEET_TOP_RADIUS,
|
|
163
|
+
},
|
|
164
|
+
]}
|
|
165
|
+
>
|
|
166
|
+
<BottomSheetHandle
|
|
167
|
+
{...props}
|
|
168
|
+
indicatorStyle={{
|
|
169
|
+
backgroundColor: placeholderColor,
|
|
170
|
+
opacity: 0.9,
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
<View style={styles.header}>
|
|
174
|
+
<Text style={[styles.sheetTitle, { color: textColor, flex: 1 }]}>
|
|
175
|
+
{title}
|
|
176
|
+
</Text>
|
|
177
|
+
{resolvedClose}
|
|
178
|
+
</View>
|
|
179
|
+
<View style={[styles.headerDivider, { backgroundColor: borderColor }]} />
|
|
180
|
+
</View>
|
|
181
|
+
),
|
|
182
|
+
[
|
|
183
|
+
backgroundColor,
|
|
184
|
+
borderColor,
|
|
185
|
+
placeholderColor,
|
|
186
|
+
resolvedClose,
|
|
187
|
+
textColor,
|
|
188
|
+
title,
|
|
189
|
+
],
|
|
190
|
+
);
|
|
123
191
|
|
|
124
192
|
const onSelect = (option: SelectOption) => {
|
|
125
193
|
if (option.disabled) return;
|
|
@@ -176,67 +244,62 @@ export function Select({
|
|
|
176
244
|
<BottomSheetModal
|
|
177
245
|
ref={modalRef}
|
|
178
246
|
backdropComponent={renderBackdrop}
|
|
179
|
-
backgroundStyle={{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
opacity: 0.9,
|
|
247
|
+
backgroundStyle={{
|
|
248
|
+
backgroundColor,
|
|
249
|
+
borderTopLeftRadius: SHEET_TOP_RADIUS,
|
|
250
|
+
borderTopRightRadius: SHEET_TOP_RADIUS,
|
|
184
251
|
}}
|
|
185
|
-
|
|
186
|
-
|
|
252
|
+
containerComponent={SheetContainer}
|
|
253
|
+
enableDynamicSizing
|
|
254
|
+
enablePanDownToClose
|
|
255
|
+
handleComponent={renderHandle}
|
|
256
|
+
handleStyle={styles.handleShell}
|
|
257
|
+
maxDynamicContentSize={maxDynamicContentSize}
|
|
258
|
+
stackBehavior="push"
|
|
187
259
|
>
|
|
188
|
-
<BottomSheetScrollView
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
</Text>
|
|
193
|
-
{resolvedClose}
|
|
194
|
-
</View>
|
|
260
|
+
<BottomSheetScrollView
|
|
261
|
+
contentContainerStyle={styles.sheetContent}
|
|
262
|
+
enableFooterMarginAdjustment={false}
|
|
263
|
+
>
|
|
195
264
|
{options.length === 0 ? (
|
|
196
265
|
<Text style={[styles.empty, { color: placeholderColor }]}>
|
|
197
266
|
No options
|
|
198
267
|
</Text>
|
|
199
268
|
) : (
|
|
200
|
-
options.map((option
|
|
269
|
+
options.map((option) => {
|
|
201
270
|
const isSelected = option.value === value;
|
|
202
271
|
return (
|
|
203
|
-
<
|
|
204
|
-
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
272
|
+
<Pressable
|
|
273
|
+
key={option.value}
|
|
274
|
+
accessibilityRole="button"
|
|
275
|
+
accessibilityState={{
|
|
276
|
+
selected: isSelected,
|
|
277
|
+
disabled: !!option.disabled,
|
|
278
|
+
}}
|
|
279
|
+
disabled={option.disabled}
|
|
280
|
+
onPress={() => onSelect(option)}
|
|
281
|
+
style={[
|
|
282
|
+
styles.option,
|
|
283
|
+
option.disabled && styles.optionBlocked,
|
|
284
|
+
]}
|
|
285
|
+
>
|
|
286
|
+
<Text
|
|
217
287
|
style={[
|
|
218
|
-
styles.
|
|
219
|
-
|
|
288
|
+
styles.optionLabel,
|
|
289
|
+
{
|
|
290
|
+
color: isSelected ? selectedColor : textColor,
|
|
291
|
+
flex: 1,
|
|
292
|
+
},
|
|
220
293
|
]}
|
|
221
294
|
>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
flex: 1,
|
|
228
|
-
},
|
|
229
|
-
]}
|
|
230
|
-
>
|
|
231
|
-
{option.label}
|
|
295
|
+
{option.label}
|
|
296
|
+
</Text>
|
|
297
|
+
{isSelected ? (
|
|
298
|
+
<Text style={[styles.check, { color: selectedColor }]}>
|
|
299
|
+
✓
|
|
232
300
|
</Text>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
✓
|
|
236
|
-
</Text>
|
|
237
|
-
) : null}
|
|
238
|
-
</Pressable>
|
|
239
|
-
</View>
|
|
301
|
+
) : null}
|
|
302
|
+
</Pressable>
|
|
240
303
|
);
|
|
241
304
|
})
|
|
242
305
|
)}
|
|
@@ -275,10 +338,17 @@ const styles = StyleSheet.create({
|
|
|
275
338
|
fontSize: 22,
|
|
276
339
|
fontWeight: "300",
|
|
277
340
|
lineHeight: 24,
|
|
341
|
+
transform: [{ rotate: "90deg" }],
|
|
278
342
|
},
|
|
279
343
|
helper: {
|
|
280
344
|
fontSize: 13,
|
|
281
345
|
},
|
|
346
|
+
handleShell: {
|
|
347
|
+
paddingBottom: 0,
|
|
348
|
+
},
|
|
349
|
+
handleRoot: {
|
|
350
|
+
overflow: "hidden",
|
|
351
|
+
},
|
|
282
352
|
sheetContent: {
|
|
283
353
|
paddingBottom: 24,
|
|
284
354
|
},
|
|
@@ -287,9 +357,12 @@ const styles = StyleSheet.create({
|
|
|
287
357
|
alignItems: "center",
|
|
288
358
|
gap: 12,
|
|
289
359
|
paddingHorizontal: 16,
|
|
290
|
-
paddingTop:
|
|
360
|
+
paddingTop: 4,
|
|
291
361
|
paddingBottom: 12,
|
|
292
362
|
},
|
|
363
|
+
headerDivider: {
|
|
364
|
+
height: StyleSheet.hairlineWidth,
|
|
365
|
+
},
|
|
293
366
|
sheetTitle: {
|
|
294
367
|
fontSize: 17,
|
|
295
368
|
fontWeight: "600",
|
|
@@ -316,8 +389,4 @@ const styles = StyleSheet.create({
|
|
|
316
389
|
fontSize: 17,
|
|
317
390
|
fontWeight: "600",
|
|
318
391
|
},
|
|
319
|
-
divider: {
|
|
320
|
-
height: StyleSheet.hairlineWidth,
|
|
321
|
-
marginHorizontal: 16,
|
|
322
|
-
},
|
|
323
392
|
});
|