@proulr/sdui-components 0.1.70 → 0.1.71

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.
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Chevron up  weighty open angle with soft echo.
4
+ -->
5
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
6
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-width="1.6" d="M6.4 14.2L12 8.8l5.6 5.4" opacity="0.32" />
7
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-width="2.2" d="M5.4 14.8L12 8.2l6.6 6.6" />
8
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proulr/sdui-components",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "type": "module",
5
5
  "main": "./src/renderer/index.ts",
6
6
  "types": "./src/renderer/index.ts",
@@ -47,7 +47,9 @@
47
47
  "expo-linear-gradient": ">=56",
48
48
  "react": ">=19",
49
49
  "react-native": ">=0.85",
50
+ "react-native-gesture-handler": ">=2.31",
50
51
  "react-native-google-mobile-ads": ">=16",
52
+ "react-native-reanimated": ">=4",
51
53
  "react-native-safe-area-context": ">=5",
52
54
  "react-native-svg": ">=15"
53
55
  },
@@ -64,9 +66,15 @@
64
66
  "expo-blur": {
65
67
  "optional": true
66
68
  },
69
+ "react-native-gesture-handler": {
70
+ "optional": true
71
+ },
67
72
  "react-native-google-mobile-ads": {
68
73
  "optional": true
69
74
  },
75
+ "react-native-reanimated": {
76
+ "optional": true
77
+ },
70
78
  "react-native-safe-area-context": {
71
79
  "optional": true
72
80
  },
@@ -87,7 +95,9 @@
87
95
  "lint-staged": "^16.4.0",
88
96
  "react": "19.2.3",
89
97
  "react-native": "0.85.3",
98
+ "react-native-gesture-handler": "~2.31.1",
90
99
  "react-native-google-mobile-ads": "^16.3.4",
100
+ "react-native-reanimated": "4.3.1",
91
101
  "react-native-safe-area-context": "~5.7.0",
92
102
  "react-native-svg": "^15.15.5",
93
103
  "typescript": "~6.0.3",
@@ -10,6 +10,7 @@ export const APP_ICON_NAMES = [
10
10
  'chat',
11
11
  'close',
12
12
  'chevron-down',
13
+ 'chevron-up',
13
14
  'chevron-right',
14
15
  'flip-camera',
15
16
  'flash',
@@ -41,7 +41,7 @@ describe('block-types', () => {
41
41
 
42
42
  describe('app-icon-names', () => {
43
43
  it('validates icon tokens', () => {
44
- expect(APP_ICON_NAMES).toHaveLength(79);
44
+ expect(APP_ICON_NAMES).toHaveLength(80);
45
45
  expect(isAppIconName('search')).toBe(true);
46
46
  expect(isAppIconName('heart-filled')).toBe(true);
47
47
  expect(isAppIconName('tab-discover')).toBe(true);
@@ -0,0 +1,200 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { LayoutChangeEvent, StyleSheet, Text, View } from 'react-native';
3
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4
+ import Animated, {
5
+ runOnJS,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ } from 'react-native-reanimated';
9
+ import { m3Colors, m3Shape, m3Type, spacing } from '../theme/proulr-theme';
10
+
11
+ export type RangeSliderProps = {
12
+ min: number;
13
+ max: number;
14
+ step?: number;
15
+ value: [number, number];
16
+ onChange: (value: [number, number]) => void;
17
+ formatLabel?: (n: number) => string;
18
+ };
19
+
20
+ const THUMB_SIZE = 28;
21
+ const TRACK_HEIGHT = 4;
22
+
23
+ function clamp(value: number, lo: number, hi: number): number {
24
+ 'worklet';
25
+ return Math.min(hi, Math.max(lo, value));
26
+ }
27
+
28
+ /**
29
+ * Generic double-thumb range slider (reanimated + gesture-handler).
30
+ * Fully theme-driven — no product logic. Emits stepped `[low, high]` values.
31
+ */
32
+ export function RangeSlider({
33
+ min,
34
+ max,
35
+ step = 1,
36
+ value,
37
+ onChange,
38
+ formatLabel,
39
+ }: RangeSliderProps) {
40
+ const [trackWidth, setTrackWidth] = useState(0);
41
+ const [labels, setLabels] = useState<[number, number]>(value);
42
+
43
+ const usable = Math.max(0, trackWidth - THUMB_SIZE);
44
+ const span = max - min || 1;
45
+
46
+ const lowX = useSharedValue(0);
47
+ const highX = useSharedValue(0);
48
+ const lowStart = useSharedValue(0);
49
+ const highStart = useSharedValue(0);
50
+
51
+ const valueToX = useCallback(
52
+ (v: number) => ((clampJs(v, min, max) - min) / span) * usable,
53
+ [min, max, span, usable],
54
+ );
55
+
56
+ // Sync thumbs when external value or track width changes.
57
+ useEffect(() => {
58
+ lowX.value = valueToX(value[0]);
59
+ highX.value = valueToX(value[1]);
60
+ setLabels(value);
61
+ }, [value, valueToX, lowX, highX]);
62
+
63
+ const emit = useCallback(
64
+ (low: number, high: number) => {
65
+ setLabels([low, high]);
66
+ onChange([low, high]);
67
+ },
68
+ [onChange],
69
+ );
70
+
71
+ const updateLabels = useCallback((low: number, high: number) => {
72
+ setLabels([low, high]);
73
+ }, []);
74
+
75
+ const lowGesture = Gesture.Pan()
76
+ .onBegin(() => {
77
+ lowStart.value = lowX.value;
78
+ })
79
+ .onUpdate((event) => {
80
+ const next = clamp(lowStart.value + event.translationX, 0, highX.value);
81
+ lowX.value = next;
82
+ runOnJS(updateLabels)(jsXToValue(next, min, max, span, step, usable), jsXToValue(highX.value, min, max, span, step, usable));
83
+ })
84
+ .onEnd(() => {
85
+ runOnJS(emit)(
86
+ jsXToValue(lowX.value, min, max, span, step, usable),
87
+ jsXToValue(highX.value, min, max, span, step, usable),
88
+ );
89
+ });
90
+
91
+ const highGesture = Gesture.Pan()
92
+ .onBegin(() => {
93
+ highStart.value = highX.value;
94
+ })
95
+ .onUpdate((event) => {
96
+ const next = clamp(highStart.value + event.translationX, lowX.value, usable);
97
+ highX.value = next;
98
+ runOnJS(updateLabels)(jsXToValue(lowX.value, min, max, span, step, usable), jsXToValue(next, min, max, span, step, usable));
99
+ })
100
+ .onEnd(() => {
101
+ runOnJS(emit)(
102
+ jsXToValue(lowX.value, min, max, span, step, usable),
103
+ jsXToValue(highX.value, min, max, span, step, usable),
104
+ );
105
+ });
106
+
107
+ const lowThumbStyle = useAnimatedStyle(() => ({
108
+ transform: [{ translateX: lowX.value }],
109
+ }));
110
+ const highThumbStyle = useAnimatedStyle(() => ({
111
+ transform: [{ translateX: highX.value }],
112
+ }));
113
+ const activeTrackStyle = useAnimatedStyle(() => ({
114
+ left: lowX.value + THUMB_SIZE / 2,
115
+ width: Math.max(0, highX.value - lowX.value),
116
+ }));
117
+
118
+ const onTrackLayout = useCallback((event: LayoutChangeEvent) => {
119
+ setTrackWidth(event.nativeEvent.layout.width);
120
+ }, []);
121
+
122
+ const format = formatLabel ?? ((n: number) => String(n));
123
+
124
+ return (
125
+ <View style={styles.container}>
126
+ <View style={styles.labelsRow}>
127
+ <Text style={styles.label}>{format(labels[0])}</Text>
128
+ <Text style={styles.label}>{format(labels[1])}</Text>
129
+ </View>
130
+ <View style={styles.trackArea} onLayout={onTrackLayout}>
131
+ <View style={styles.trackBase} />
132
+ <Animated.View style={[styles.trackActive, activeTrackStyle]} />
133
+ <GestureDetector gesture={lowGesture}>
134
+ <Animated.View style={[styles.thumb, lowThumbStyle]} hitSlop={12} />
135
+ </GestureDetector>
136
+ <GestureDetector gesture={highGesture}>
137
+ <Animated.View style={[styles.thumb, highThumbStyle]} hitSlop={12} />
138
+ </GestureDetector>
139
+ </View>
140
+ </View>
141
+ );
142
+ }
143
+
144
+ function clampJs(value: number, lo: number, hi: number): number {
145
+ return Math.min(hi, Math.max(lo, value));
146
+ }
147
+
148
+ function jsXToValue(
149
+ x: number,
150
+ min: number,
151
+ max: number,
152
+ span: number,
153
+ step: number,
154
+ usable: number,
155
+ ): number {
156
+ const raw = min + (x / (usable || 1)) * span;
157
+ const stepped = Math.round(raw / step) * step;
158
+ return clampJs(stepped, min, max);
159
+ }
160
+
161
+ const styles = StyleSheet.create({
162
+ container: {
163
+ gap: spacing.sm,
164
+ paddingVertical: spacing.xs,
165
+ },
166
+ labelsRow: {
167
+ flexDirection: 'row',
168
+ justifyContent: 'space-between',
169
+ },
170
+ label: {
171
+ color: m3Colors.onSurface,
172
+ fontFamily: m3Type.label.fontFamily,
173
+ fontSize: m3Type.label.fontSize,
174
+ },
175
+ trackArea: {
176
+ height: THUMB_SIZE,
177
+ justifyContent: 'center',
178
+ },
179
+ trackBase: {
180
+ height: TRACK_HEIGHT,
181
+ borderRadius: m3Shape.cornerFull,
182
+ backgroundColor: m3Colors.surfaceContainerHighest,
183
+ },
184
+ trackActive: {
185
+ position: 'absolute',
186
+ height: TRACK_HEIGHT,
187
+ borderRadius: m3Shape.cornerFull,
188
+ backgroundColor: m3Colors.primary,
189
+ },
190
+ thumb: {
191
+ position: 'absolute',
192
+ left: 0,
193
+ width: THUMB_SIZE,
194
+ height: THUMB_SIZE,
195
+ borderRadius: m3Shape.cornerFull,
196
+ backgroundColor: m3Colors.primary,
197
+ borderWidth: 3,
198
+ borderColor: m3Colors.onPrimary,
199
+ },
200
+ });
@@ -9,8 +9,8 @@ const ICONS_DIR = path.join(ROOT, 'assets', 'icons');
9
9
  const GENERATED_DIR = path.join(ROOT, 'src', 'renderer', 'ui', 'generated');
10
10
 
11
11
  describe('AppIcon SVG ↔ catalog 1:1', () => {
12
- it('has exactly 79 AppIconName tokens', () => {
13
- expect(APP_ICON_NAMES).toHaveLength(79);
12
+ it('has exactly 80 AppIconName tokens', () => {
13
+ expect(APP_ICON_NAMES).toHaveLength(80);
14
14
  });
15
15
 
16
16
  it('has one SVG file per AppIconName and no orphan SVGs', () => {
@@ -0,0 +1,16 @@
1
+ /* Generated by scripts/generate-app-icons.mjs — do not edit. */
2
+ import Svg, { Path } from 'react-native-svg';
3
+ import { m3Colors } from '../../theme/proulr-theme';
4
+ import type { AppIconGlyphProps } from '../app-icon-glyph-props';
5
+
6
+ export function ChevronUpIcon({
7
+ color = m3Colors.primary,
8
+ size = 24,
9
+ }: AppIconGlyphProps) {
10
+ return (
11
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
12
+ <Path stroke={color} strokeLinecap="round" strokeLinejoin="round" fill="none" strokeWidth={1.6} d="M6.4 14.2L12 8.8l5.6 5.4" opacity={0.32} />
13
+ <Path stroke={color} strokeLinecap="round" strokeLinejoin="round" fill="none" strokeWidth={2.2} d="M5.4 14.8L12 8.2l6.6 6.6" />
14
+ </Svg>
15
+ );
16
+ }
@@ -12,6 +12,7 @@ import { SendIcon } from './send';
12
12
  import { ChatIcon } from './chat';
13
13
  import { CloseIcon } from './close';
14
14
  import { ChevronDownIcon } from './chevron-down';
15
+ import { ChevronUpIcon } from './chevron-up';
15
16
  import { ChevronRightIcon } from './chevron-right';
16
17
  import { FlipCameraIcon } from './flip-camera';
17
18
  import { FlashIcon } from './flash';
@@ -96,6 +97,7 @@ export const GENERATED_ICON_REGISTRY: Record<
96
97
  'chat': ChatIcon,
97
98
  'close': CloseIcon,
98
99
  'chevron-down': ChevronDownIcon,
100
+ 'chevron-up': ChevronUpIcon,
99
101
  'chevron-right': ChevronRightIcon,
100
102
  'flip-camera': FlipCameraIcon,
101
103
  'flash': FlashIcon,
@@ -177,6 +179,7 @@ export { SendIcon } from './send';
177
179
  export { ChatIcon } from './chat';
178
180
  export { CloseIcon } from './close';
179
181
  export { ChevronDownIcon } from './chevron-down';
182
+ export { ChevronUpIcon } from './chevron-up';
180
183
  export { ChevronRightIcon } from './chevron-right';
181
184
  export { FlipCameraIcon } from './flip-camera';
182
185
  export { FlashIcon } from './flash';
@@ -56,6 +56,7 @@ export { InfoBanner } from './InfoBanner';
56
56
  export { Label } from './Label';
57
57
  export { ListItem } from './ListItem';
58
58
  export { ProgressBar } from './ProgressBar';
59
+ export { RangeSlider, type RangeSliderProps } from './RangeSlider';
59
60
  export { SectionHeader } from './SectionHeader';
60
61
  export { SelectRow, MultiSelectRow } from './SelectRow';
61
62
  export { StatRow } from './StatRow';
@@ -10,6 +10,7 @@ export { InfoBanner } from './InfoBanner';
10
10
  export { Label } from './Label';
11
11
  export { ListItem } from './ListItem';
12
12
  export { ProgressBar } from './ProgressBar';
13
+ export { RangeSlider, type RangeSliderProps } from './RangeSlider';
13
14
  export { SectionHeader } from './SectionHeader';
14
15
  export { SelectRow, MultiSelectRow } from './SelectRow';
15
16
  export { StatRow } from './StatRow';