@streamplace/components 0.8.6 → 0.8.8

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.
@@ -1,7 +1,7 @@
1
1
  import { Check } from "lucide-react-native";
2
2
  import { forwardRef } from "react";
3
3
  import { StyleSheet, TouchableOpacity, View } from "react-native";
4
- import { useTheme } from "../../lib/theme/theme";
4
+ import { Theme, useTheme } from "../../lib/theme/theme";
5
5
  import { Text } from "./text";
6
6
 
7
7
  export interface CheckboxProps {
@@ -60,9 +60,23 @@ export const Checkbox = forwardRef<any, CheckboxProps>(
60
60
  </View>
61
61
  {(label || description) && (
62
62
  <View style={styles.content}>
63
- {label && <Text style={styles.label}>{label}</Text>}
63
+ {label && (
64
+ <Text
65
+ size={size === "sm" ? "sm" : size === "lg" ? "lg" : "base"}
66
+ color={disabled ? "muted" : "default"}
67
+ leading="snug"
68
+ >
69
+ {label}
70
+ </Text>
71
+ )}
64
72
  {description && (
65
- <Text style={styles.description}>{description}</Text>
73
+ <Text
74
+ size={size === "sm" ? "xs" : size === "lg" ? "base" : "sm"}
75
+ color={disabled ? "muted" : "muted"}
76
+ style={{ marginTop: theme.spacing[1] }}
77
+ >
78
+ {description}
79
+ </Text>
66
80
  )}
67
81
  </View>
68
82
  )}
@@ -74,7 +88,7 @@ export const Checkbox = forwardRef<any, CheckboxProps>(
74
88
  Checkbox.displayName = "Checkbox";
75
89
 
76
90
  function createStyles(
77
- theme: any,
91
+ theme: Theme,
78
92
  size: string,
79
93
  disabled: boolean,
80
94
  checked: boolean,
@@ -90,13 +104,13 @@ function createStyles(
90
104
  checkboxSize: 20,
91
105
  borderRadius: 4,
92
106
  padding: theme.spacing[1],
93
- gap: theme.spacing[2],
107
+ gap: theme.spacing[1],
94
108
  },
95
109
  lg: {
96
110
  checkboxSize: 24,
97
111
  borderRadius: 6,
98
- padding: theme.spacing[2],
99
- gap: theme.spacing[3],
112
+ padding: theme.spacing[1],
113
+ gap: theme.spacing[2],
100
114
  },
101
115
  };
102
116
 
@@ -116,7 +130,7 @@ function createStyles(
116
130
  ? theme.colors.border
117
131
  : checked
118
132
  ? theme.colors.primary
119
- : theme.colors.border,
133
+ : theme.colors.textMuted,
120
134
  borderRadius: currentSize.borderRadius,
121
135
  backgroundColor: disabled
122
136
  ? theme.colors.muted
@@ -128,20 +142,8 @@ function createStyles(
128
142
  },
129
143
  content: {
130
144
  flex: 1,
131
- paddingTop: currentSize.padding * 0.5,
145
+ paddingTop: currentSize.padding * 0.25,
132
146
  paddingLeft: theme.spacing[2],
133
147
  },
134
- label: {
135
- fontSize: size === "sm" ? 14 : size === "lg" ? 18 : 16,
136
- fontWeight: "500",
137
- color: disabled ? theme.colors.textDisabled : theme.colors.text,
138
- lineHeight: size === "sm" ? 18 : size === "lg" ? 22 : 20,
139
- },
140
- description: {
141
- fontSize: size === "sm" ? 12 : size === "lg" ? 16 : 14,
142
- color: disabled ? theme.colors.textDisabled : theme.colors.textMuted,
143
- marginTop: theme.spacing[1],
144
- lineHeight: size === "sm" ? 16 : size === "lg" ? 20 : 18,
145
- },
146
148
  });
147
149
  }
@@ -8,14 +8,16 @@ import {
8
8
  ChevronUp,
9
9
  Circle,
10
10
  } from "lucide-react-native";
11
- import React, { forwardRef, ReactNode, useMemo, useRef } from "react";
11
+ import React, { forwardRef, ReactNode, useRef } from "react";
12
12
  import {
13
13
  Platform,
14
14
  Pressable,
15
+ ScrollView,
15
16
  StyleSheet,
16
17
  useWindowDimensions,
17
18
  View,
18
19
  } from "react-native";
20
+ import { zero } from "../..";
19
21
  import {
20
22
  a,
21
23
  borderRadius,
@@ -53,22 +55,18 @@ export const DropdownMenuBottomSheet = forwardRef<
53
55
  portalHost?: string;
54
56
  }
55
57
  >(function DropdownMenuBottomSheet(
56
- { overlayStyle, portalHost, children },
58
+ { overlayStyle, portalHost, children, ...rest },
57
59
  _ref,
58
60
  ) {
59
61
  // Use the primitives' context to know if open
60
62
  const { open, onOpenChange } = DropdownMenuPrimitive.useRootContext();
61
63
  const { zero: zt } = useTheme();
62
- const snapPoints = useMemo(() => ["25%", "50%", "80%"], []);
63
64
  const sheetRef = useRef<BottomSheet>(null);
64
65
 
65
66
  return (
66
67
  <DropdownMenuPrimitive.Portal hostName={portalHost}>
67
68
  <BottomSheet
68
69
  ref={sheetRef}
69
- // why the heck is this 1-indexed
70
- index={open ? 3 : -1}
71
- snapPoints={snapPoints}
72
70
  enablePanDownToClose
73
71
  enableDynamicSizing
74
72
  enableContentPanningGesture={false}
@@ -79,7 +77,7 @@ export const DropdownMenuBottomSheet = forwardRef<
79
77
  />
80
78
  )}
81
79
  onClose={() => onOpenChange?.(false)}
82
- style={[overlayStyle]}
80
+ style={[overlayStyle, StyleSheet.flatten(rest.style)]}
83
81
  backgroundStyle={[zt.bg.popover, a.radius.all.md, a.shadows.md, p[1]]}
84
82
  handleIndicatorStyle={[
85
83
  a.sizes.width[12],
@@ -169,7 +167,7 @@ export const DropdownMenuContent = forwardRef<
169
167
  overlayStyle?: any;
170
168
  portalHost?: string;
171
169
  }
172
- >(({ overlayStyle, portalHost, ...props }, ref) => {
170
+ >(({ overlayStyle, portalHost, style, children, ...props }, ref) => {
173
171
  const { zero: zt } = useTheme();
174
172
  return (
175
173
  <DropdownMenuPrimitive.Portal hostName={portalHost}>
@@ -193,10 +191,17 @@ export const DropdownMenuContent = forwardRef<
193
191
  zt.bg.popover,
194
192
  p[2],
195
193
  a.shadows.md,
194
+ style,
196
195
  ] as any
197
196
  }
198
197
  {...props}
199
- />
198
+ >
199
+ <ScrollView showsVerticalScrollIndicator={true}>
200
+ {typeof children === "function"
201
+ ? children({ pressed: false })
202
+ : children}
203
+ </ScrollView>
204
+ </DropdownMenuPrimitive.Content>
200
205
  </DropdownMenuPrimitive.Overlay>
201
206
  </DropdownMenuPrimitive.Portal>
202
207
  );
@@ -206,37 +211,52 @@ export const DropdownMenuContentWithoutPortal = forwardRef<
206
211
  any,
207
212
  DropdownMenuPrimitive.ContentProps & {
208
213
  overlayStyle?: any;
214
+ maxHeightPercentage?: number;
209
215
  }
210
- >(({ overlayStyle, ...props }, ref) => {
211
- const { theme } = useTheme();
212
- return (
213
- <DropdownMenuPrimitive.Overlay
214
- style={[
215
- Platform.OS !== "web" ? StyleSheet.absoluteFill : undefined,
216
- overlayStyle,
217
- ]}
218
- >
219
- <DropdownMenuPrimitive.Content
220
- ref={ref}
221
- style={
222
- [
223
- { zIndex: 999999 },
224
- a.sizes.minWidth[32],
225
- a.sizes.maxWidth[64],
226
- a.overflow.hidden,
227
- a.radius.all.md,
228
- a.borders.width.thin,
229
- { borderColor: theme.colors.border },
230
- { backgroundColor: theme.colors.popover },
231
- p[2],
232
- a.shadows.md,
233
- ] as any
234
- }
235
- {...props}
236
- />
237
- </DropdownMenuPrimitive.Overlay>
238
- );
239
- });
216
+ >(
217
+ (
218
+ { overlayStyle, maxHeightPercentage = 0.8, children, style, ...props },
219
+ ref,
220
+ ) => {
221
+ const { theme } = useTheme();
222
+ const { height } = useWindowDimensions();
223
+ const maxHeight = height * maxHeightPercentage;
224
+
225
+ return (
226
+ <DropdownMenuPrimitive.Overlay
227
+ style={[
228
+ Platform.OS !== "web" ? StyleSheet.absoluteFill : undefined,
229
+ overlayStyle,
230
+ ]}
231
+ >
232
+ <DropdownMenuPrimitive.Content
233
+ ref={ref}
234
+ style={
235
+ [
236
+ { zIndex: 999999 },
237
+ a.sizes.minWidth[32],
238
+ a.sizes.maxWidth[64],
239
+ a.radius.all.md,
240
+ a.borders.width.thin,
241
+ { borderColor: theme.colors.border },
242
+ { backgroundColor: theme.colors.popover },
243
+ p[2],
244
+ a.shadows.md,
245
+ style,
246
+ ] as any
247
+ }
248
+ {...props}
249
+ >
250
+ <ScrollView style={{ maxHeight }} showsVerticalScrollIndicator={true}>
251
+ {typeof children === "function"
252
+ ? children({ pressed: false })
253
+ : children}
254
+ </ScrollView>
255
+ </DropdownMenuPrimitive.Content>
256
+ </DropdownMenuPrimitive.Overlay>
257
+ );
258
+ },
259
+ );
240
260
 
241
261
  /// Responsive Dropdown Menu Content. On mobile this will render a *bottom sheet* that is **portaled to the root of the app**.
242
262
  /// Prefer passing scoped content in as **otherwise it may crash the app**.
@@ -250,7 +270,7 @@ export const ResponsiveDropdownMenuContent = forwardRef<any, any>(
250
270
  if (isBottomSheet) {
251
271
  return (
252
272
  <DropdownMenuBottomSheet ref={ref} {...props}>
253
- {children}
273
+ <ScrollView style={[zero.pb[12]]}>{children}</ScrollView>
254
274
  </DropdownMenuBottomSheet>
255
275
  );
256
276
  }
@@ -277,6 +277,22 @@ export const TextRoot = forwardRef<RNText, TextPrimitiveProps>(
277
277
  const inheritedContext =
278
278
  inherit && !reset && parentContext ? parentContext : {};
279
279
 
280
+ // Calculate fontSize first for line height calculation
281
+ let calculatedFontSize = inheritedContext.fontSize;
282
+
283
+ // Apply variant font size
284
+ if (variant && variantStyles[variant]?.fontSize) {
285
+ calculatedFontSize = variantStyles[variant].fontSize as number;
286
+ }
287
+
288
+ // Apply size-based font size
289
+ if (size) {
290
+ calculatedFontSize = typeof size === "number" ? size : sizeMap[size];
291
+ }
292
+
293
+ // Use default if still undefined
294
+ calculatedFontSize = calculatedFontSize || 16;
295
+
280
296
  // Calculate final styles
281
297
  const finalStyles: TextStyle = {
282
298
  // Start with inherited values
@@ -344,7 +360,10 @@ export const TextRoot = forwardRef<RNText, TextPrimitiveProps>(
344
360
 
345
361
  // Apply line height
346
362
  ...(leading && {
347
- lineHeight: typeof leading === "number" ? leading : leadingMap[leading],
363
+ lineHeight:
364
+ typeof leading === "number"
365
+ ? leading
366
+ : leadingMap[leading] * calculatedFontSize,
348
367
  }),
349
368
 
350
369
  // Apply letter spacing
@@ -389,7 +408,7 @@ export const TextRoot = forwardRef<RNText, TextPrimitiveProps>(
389
408
  if (typeof fontSize === "number" && !styleObj.lineHeight && !leading) {
390
409
  return {
391
410
  ...styleObj,
392
- lineHeight: fontSize * 1.2,
411
+ lineHeight: fontSize,
393
412
  };
394
413
  }
395
414
  }
@@ -478,7 +497,7 @@ export function createTextStyle(
478
497
  if (props.leading === undefined) {
479
498
  style.lineHeight =
480
499
  typeof props.size === "number"
481
- ? props.size * 1.2 // Auto line height for numeric sizes
500
+ ? props.size
482
501
  : sizeLineHeightMap[props.size];
483
502
  }
484
503
  }
@@ -492,10 +511,11 @@ export function createTextStyle(
492
511
  }
493
512
 
494
513
  if (props.leading) {
514
+ const fontSize = style.fontSize || 16; // default font size
495
515
  style.lineHeight =
496
516
  typeof props.leading === "number"
497
517
  ? props.leading
498
- : leadingMap[props.leading];
518
+ : leadingMap[props.leading] * fontSize;
499
519
  }
500
520
 
501
521
  if (props.tracking) {
@@ -1,15 +1,20 @@
1
- import { ChevronDown } from "lucide-react-native";
2
- import { forwardRef, useState } from "react";
3
- import {
4
- FlatList,
5
- Modal,
6
- StyleSheet,
7
- TouchableOpacity,
8
- View,
9
- } from "react-native";
1
+ import { Check, ChevronDown } from "lucide-react-native";
2
+ import { forwardRef } from "react";
3
+ import { View } from "react-native";
4
+ import { zero } from "../..";
10
5
  import { useTheme } from "../../lib/theme/theme";
6
+ import { flex } from "../../ui";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuItem,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ ResponsiveDropdownMenuContent,
13
+ } from "./dropdown";
11
14
  import { Text } from "./text";
12
15
 
16
+ const { layout, px, py, borders, r, gap } = zero;
17
+
13
18
  export interface SelectItem {
14
19
  label: string;
15
20
  value: string;
@@ -25,7 +30,7 @@ export interface SelectProps {
25
30
  style?: any;
26
31
  }
27
32
 
28
- export const Select = forwardRef<any, SelectProps>(
33
+ export const Select = forwardRef<View, SelectProps>(
29
34
  (
30
35
  {
31
36
  value,
@@ -38,138 +43,105 @@ export const Select = forwardRef<any, SelectProps>(
38
43
  ref,
39
44
  ) => {
40
45
  const { theme } = useTheme();
41
- const [isOpen, setIsOpen] = useState(false);
42
46
 
43
47
  const selectedItem = items.find((item) => item.value === value);
44
48
 
45
- const handleSelect = (itemValue: string) => {
46
- onValueChange(itemValue);
47
- setIsOpen(false);
48
- };
49
-
50
- const styles = createStyles(theme, disabled);
51
-
52
49
  return (
53
- <>
54
- <TouchableOpacity
55
- ref={ref}
56
- style={[styles.container, style]}
57
- onPress={() => !disabled && setIsOpen(true)}
58
- disabled={disabled}
59
- >
60
- <Text style={styles.value}>{selectedItem?.label || placeholder}</Text>
61
- <ChevronDown size={16} color={theme.colors.textMuted} />
62
- </TouchableOpacity>
50
+ <DropdownMenu>
51
+ <DropdownMenuTrigger disabled={disabled}>
52
+ <View
53
+ ref={ref}
54
+ style={[
55
+ {
56
+ width: "100%",
57
+ paddingHorizontal: theme.spacing[3],
58
+ paddingVertical: theme.spacing[3],
59
+ borderWidth: 1,
60
+ borderColor: theme.colors.border,
61
+ borderRadius: theme.borderRadius.md,
62
+ backgroundColor: disabled
63
+ ? theme.colors.muted
64
+ : theme.colors.card,
65
+ minHeight: theme.touchTargets.minimum,
66
+ opacity: disabled ? 0.5 : 1,
67
+ },
68
+ style,
69
+ ]}
70
+ >
71
+ <View
72
+ style={{
73
+ flexDirection: "row",
74
+ alignItems: "center",
75
+ justifyContent: "space-between",
76
+ width: "100%",
77
+ gap: 8,
78
+ }}
79
+ >
80
+ <Text
81
+ style={{
82
+ fontSize: 16,
83
+ color: disabled
84
+ ? theme.colors.textDisabled
85
+ : theme.colors.text,
86
+ flex: 1,
87
+ }}
88
+ >
89
+ {selectedItem?.label || placeholder}
90
+ </Text>
91
+ <ChevronDown size={16} color={theme.colors.textMuted} />
92
+ </View>
93
+ </View>
94
+ </DropdownMenuTrigger>
63
95
 
64
- <Modal
65
- visible={isOpen}
66
- transparent
67
- animationType="fade"
68
- onRequestClose={() => setIsOpen(false)}
96
+ <ResponsiveDropdownMenuContent
97
+ align="start"
98
+ style={[
99
+ {
100
+ maxHeight: 400,
101
+ },
102
+ ]}
69
103
  >
70
- <TouchableOpacity
71
- style={styles.overlay}
72
- activeOpacity={1}
73
- onPress={() => setIsOpen(false)}
74
- >
75
- <View style={styles.dropdown}>
76
- <FlatList
77
- data={items}
78
- keyExtractor={(item) => item.value}
79
- renderItem={({ item }) => (
80
- <TouchableOpacity
81
- style={[
82
- styles.item,
83
- item.value === value && styles.selectedItem,
84
- ]}
85
- onPress={() => handleSelect(item.value)}
86
- >
104
+ {items.map((item, index) => (
105
+ <View key={item.value}>
106
+ <DropdownMenuItem onPress={() => onValueChange(item.value)}>
107
+ <View
108
+ style={{
109
+ flexDirection: "row",
110
+ alignItems: "center",
111
+ justifyContent: "space-between",
112
+ width: "100%",
113
+ gap: 8,
114
+ }}
115
+ >
116
+ <View style={[gap.all[1], py[1], flex.values[1]]}>
87
117
  <Text
88
- style={[
89
- styles.itemText,
90
- item.value === value ? styles.selectedItemText : {},
91
- ]}
118
+ style={{
119
+ fontWeight: item.value === value ? "500" : "400",
120
+ }}
121
+ color={item.value === value ? "primary" : "default"}
92
122
  >
93
123
  {item.label}
94
124
  </Text>
95
125
  {item.description && (
96
- <Text style={styles.itemDescription}>
126
+ <Text size="sm" color="muted">
97
127
  {item.description}
98
128
  </Text>
99
129
  )}
100
- </TouchableOpacity>
101
- )}
102
- style={styles.list}
103
- />
130
+ </View>
131
+ {item.value === value ? (
132
+ <Check size={16} color={theme.colors.primary} />
133
+ ) : (
134
+ <View style={{ width: 16 }} />
135
+ )}
136
+ </View>
137
+ </DropdownMenuItem>
138
+ {index < items.length - 1 && <DropdownMenuSeparator />}
104
139
  </View>
105
- </TouchableOpacity>
106
- </Modal>
107
- </>
140
+ ))}
141
+ </ResponsiveDropdownMenuContent>
142
+ </DropdownMenu>
108
143
  );
109
144
  },
110
145
  );
111
146
 
112
147
  Select.displayName = "Select";
113
-
114
- function createStyles(theme: any, disabled: boolean) {
115
- return StyleSheet.create({
116
- container: {
117
- flexDirection: "row",
118
- alignItems: "center",
119
- justifyContent: "space-between",
120
- paddingHorizontal: theme.spacing[3],
121
- paddingVertical: theme.spacing[3],
122
- borderWidth: 1,
123
- borderColor: theme.colors.border,
124
- borderRadius: theme.borderRadius.md,
125
- backgroundColor: disabled ? theme.colors.muted : theme.colors.card,
126
- minHeight: theme.touchTargets.minimum,
127
- },
128
- value: {
129
- fontSize: 16,
130
- color: disabled ? theme.colors.textDisabled : theme.colors.text,
131
- flex: 1,
132
- },
133
- overlay: {
134
- flex: 1,
135
- backgroundColor: "rgba(0, 0, 0, 0.5)",
136
- justifyContent: "center",
137
- alignItems: "center",
138
- },
139
- dropdown: {
140
- backgroundColor: theme.colors.background,
141
- borderRadius: theme.borderRadius.md,
142
- borderWidth: 1,
143
- borderColor: theme.colors.border,
144
- maxHeight: 300,
145
- width: "90%",
146
- maxWidth: 400,
147
- ...theme.shadows.lg,
148
- },
149
- list: {
150
- maxHeight: 300,
151
- },
152
- item: {
153
- paddingHorizontal: theme.spacing[4],
154
- paddingVertical: theme.spacing[3],
155
- borderBottomWidth: 1,
156
- borderBottomColor: theme.colors.border,
157
- },
158
- selectedItem: {
159
- backgroundColor: theme.colors.primary,
160
- },
161
- itemText: {
162
- fontSize: 16,
163
- color: theme.colors.text,
164
- },
165
- selectedItemText: {
166
- color: theme.colors.primaryForeground,
167
- fontWeight: "500",
168
- },
169
- itemDescription: {
170
- fontSize: 14,
171
- color: theme.colors.textMuted,
172
- marginTop: theme.spacing[1],
173
- },
174
- });
175
- }