@streamplace/components 0.8.6 → 0.8.9

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,21 +1,38 @@
1
- import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
1
+ import BottomSheet, { BottomSheetScrollView } from "@gorhom/bottom-sheet";
2
2
  import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
3
3
  import {
4
4
  Check,
5
5
  CheckCircle,
6
6
  ChevronDown,
7
+ ChevronLeft,
7
8
  ChevronRight,
8
9
  ChevronUp,
9
10
  Circle,
10
11
  } from "lucide-react-native";
11
- import React, { forwardRef, ReactNode, useMemo, useRef } from "react";
12
+ import React, {
13
+ createContext,
14
+ forwardRef,
15
+ ReactNode,
16
+ startTransition,
17
+ useContext,
18
+ useRef,
19
+ useState,
20
+ } from "react";
12
21
  import {
13
22
  Platform,
14
23
  Pressable,
24
+ ScrollView,
15
25
  StyleSheet,
16
26
  useWindowDimensions,
17
27
  View,
18
28
  } from "react-native";
29
+ import Animated, {
30
+ runOnJS,
31
+ useAnimatedStyle,
32
+ useSharedValue,
33
+ withTiming,
34
+ } from "react-native-reanimated";
35
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
19
36
  import {
20
37
  a,
21
38
  borderRadius,
@@ -40,12 +57,103 @@ import {
40
57
  } from "./primitives/text";
41
58
  import { Text } from "./text";
42
59
 
60
+ // Navigation stack context for bottom sheet menus
61
+ interface NavigationStackItem {
62
+ key: string;
63
+ title?: string;
64
+ content: ReactNode | ((state: { pressed: boolean }) => ReactNode);
65
+ }
66
+
67
+ interface NavigationStackContextValue {
68
+ stack: NavigationStackItem[];
69
+ push: (item: NavigationStackItem) => void;
70
+ pop: () => void;
71
+ isNested: boolean;
72
+ }
73
+
74
+ const NavigationStackContext =
75
+ createContext<NavigationStackContextValue | null>(null);
76
+
77
+ const useNavigationStack = () => {
78
+ const context = useContext(NavigationStackContext);
79
+ return context;
80
+ };
81
+
82
+ // Context to capture submenu content for mobile navigation
83
+ interface SubMenuContextValue {
84
+ title?: string;
85
+ renderContent: () => ReactNode;
86
+ setRenderContent: (renderer: () => ReactNode) => void;
87
+ setTitle: (title: string) => void;
88
+ trigger: () => void;
89
+ key: string | null;
90
+ }
91
+
92
+ const SubMenuContext = createContext<SubMenuContextValue | null>(null);
93
+
43
94
  export const DropdownMenu = DropdownMenuPrimitive.Root;
44
95
  export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
45
96
  export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
46
- export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
47
97
  export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
48
98
 
99
+ // Custom DropdownMenuSub that works with mobile navigation
100
+ export const DropdownMenuSub = forwardRef<any, any>(
101
+ ({ children, ...props }, ref) => {
102
+ const navStack = useNavigationStack();
103
+ const [subMenuTitle, setSubMenuTitle] = useState<string | undefined>();
104
+ const renderContentRef = useRef<(() => ReactNode) | null>(null);
105
+ const [subMenuKey, setSubMenuKey] = useState<string | null>(null);
106
+
107
+ // If we're in a mobile navigation stack, use custom context
108
+ if (navStack) {
109
+ const trigger = () => {
110
+ if (renderContentRef.current) {
111
+ const key = `submenu-${Date.now()}`;
112
+ setSubMenuKey(key);
113
+ navStack.push({
114
+ key,
115
+ title: subMenuTitle,
116
+ // Store a function that always reads the latest content from the ref
117
+ content: (props: any) => {
118
+ const renderFn = renderContentRef.current;
119
+ return renderFn ? renderFn() : null;
120
+ },
121
+ });
122
+ }
123
+ };
124
+
125
+ const setRenderContent = (renderer: () => ReactNode) => {
126
+ renderContentRef.current = renderer;
127
+ };
128
+
129
+ const contextValue = React.useMemo(
130
+ () => ({
131
+ renderContent: () => renderContentRef.current?.(),
132
+ setRenderContent,
133
+ title: subMenuTitle,
134
+ setTitle: setSubMenuTitle,
135
+ trigger,
136
+ key: subMenuKey,
137
+ }),
138
+ [subMenuTitle, subMenuKey],
139
+ );
140
+
141
+ return (
142
+ <SubMenuContext.Provider value={contextValue}>
143
+ {children}
144
+ </SubMenuContext.Provider>
145
+ );
146
+ }
147
+
148
+ // Web - use primitive
149
+ return (
150
+ <DropdownMenuPrimitive.Sub ref={ref} {...props}>
151
+ {children}
152
+ </DropdownMenuPrimitive.Sub>
153
+ );
154
+ },
155
+ );
156
+
49
157
  export const DropdownMenuBottomSheet = forwardRef<
50
158
  any,
51
159
  DropdownMenuPrimitive.ContentProps & {
@@ -53,61 +161,263 @@ export const DropdownMenuBottomSheet = forwardRef<
53
161
  portalHost?: string;
54
162
  }
55
163
  >(function DropdownMenuBottomSheet(
56
- { overlayStyle, portalHost, children },
164
+ { overlayStyle, portalHost, children, ...rest },
57
165
  _ref,
58
166
  ) {
59
167
  // Use the primitives' context to know if open
60
- const { open, onOpenChange } = DropdownMenuPrimitive.useRootContext();
61
- const { zero: zt } = useTheme();
62
- const snapPoints = useMemo(() => ["25%", "50%", "80%"], []);
168
+ const { onOpenChange } = DropdownMenuPrimitive.useRootContext();
169
+ const { zero: zt, theme } = useTheme();
63
170
  const sheetRef = useRef<BottomSheet>(null);
171
+ const { width } = useWindowDimensions();
172
+ const isWide = Platform.OS !== "web" && width >= 800;
173
+ const sheetWidth = isWide ? 450 : width;
174
+ const horizontalMargin = isWide ? (width - sheetWidth) / 2 : 0;
175
+
176
+ const insets = useSafeAreaInsets();
177
+
178
+ // Navigation stack state
179
+ const [stack, setStack] = useState<NavigationStackItem[]>([
180
+ { key: "root", content: children },
181
+ ]);
182
+
183
+ // Update root content when children changes
184
+ React.useEffect(() => {
185
+ setStack((prev) => {
186
+ if (!Array.isArray(prev) || prev.length === 0) {
187
+ return [{ key: "root", content: children }];
188
+ }
189
+ // Update the root item content
190
+ const newStack = [...prev];
191
+ newStack[0] = { ...newStack[0], content: children };
192
+ return newStack;
193
+ });
194
+ }, [children]);
195
+
196
+ const slideAnim = useSharedValue(0);
197
+ const fadeAnim = useSharedValue(1);
198
+
199
+ const push = (item: NavigationStackItem) => {
200
+ // First, update the stack
201
+ setStack((prev) => {
202
+ if (!Array.isArray(prev))
203
+ return [{ key: "root", content: children }, item];
204
+ return [...prev, item];
205
+ });
206
+
207
+ // Then animate from right to center with fade
208
+ slideAnim.value = 40;
209
+ fadeAnim.value = 0;
210
+ slideAnim.value = withTiming(0, { duration: 350 });
211
+ fadeAnim.value = withTiming(1, { duration: 350 });
212
+ };
213
+
214
+ const popStack = () => {
215
+ startTransition(() => {
216
+ setStack((prev) => {
217
+ if (!Array.isArray(prev) || prev.length <= 1) {
218
+ return [{ key: "root", content: children }];
219
+ }
220
+ return prev.slice(0, -1);
221
+ });
222
+ });
223
+ };
224
+
225
+ const resetAnimationValues = () => {
226
+ setTimeout(() => {
227
+ slideAnim.value = 0;
228
+ fadeAnim.value = 1;
229
+ }, 5);
230
+ };
231
+
232
+ const pop = () => {
233
+ if (stack.length <= 1) return;
234
+
235
+ // Animate out to the right with fade
236
+ slideAnim.value = withTiming(40, { duration: 150 });
237
+ fadeAnim.value = withTiming(0, { duration: 150 }, (finished) => {
238
+ if (finished) {
239
+ // Update stack first with startTransition for smoother render
240
+ runOnJS(popStack)();
241
+
242
+ // Then reset animation position after a brief delay to ensure component has unmounted
243
+ runOnJS(resetAnimationValues)();
244
+ }
245
+ });
246
+ };
247
+
248
+ const animatedStyle = useAnimatedStyle(() => ({
249
+ transform: [{ translateX: slideAnim.value }],
250
+ opacity: fadeAnim.value,
251
+ }));
252
+
253
+ const headerAnimatedStyle = useAnimatedStyle(() => ({
254
+ opacity: fadeAnim.value,
255
+ }));
256
+
257
+ const currentLevel = stack[stack.length - 1];
258
+ const isNested = stack.length > 1;
259
+
260
+ const onBackgroundTap = () => {
261
+ if (sheetRef.current) sheetRef.current?.close();
262
+
263
+ setTimeout(() => {
264
+ onOpenChange?.(false);
265
+ }, 300);
266
+ };
267
+
268
+ // Safety check - if no current level, don't render
269
+ if (!currentLevel) {
270
+ return null;
271
+ }
64
272
 
65
273
  return (
66
274
  <DropdownMenuPrimitive.Portal hostName={portalHost}>
67
- <BottomSheet
68
- ref={sheetRef}
69
- // why the heck is this 1-indexed
70
- index={open ? 3 : -1}
71
- snapPoints={snapPoints}
72
- enablePanDownToClose
73
- enableDynamicSizing
74
- enableContentPanningGesture={false}
75
- backdropComponent={({ style }) => (
76
- <Pressable
77
- style={[style, StyleSheet.absoluteFill]}
78
- onPress={() => onOpenChange?.(false)}
79
- />
80
- )}
81
- onClose={() => onOpenChange?.(false)}
82
- style={[overlayStyle]}
83
- backgroundStyle={[zt.bg.popover, a.radius.all.md, a.shadows.md, p[1]]}
84
- handleIndicatorStyle={[
85
- a.sizes.width[12],
86
- a.sizes.height[1],
87
- zt.bg.mutedForeground,
88
- ]}
89
- >
90
- <BottomSheetView style={[px[4]]}>
91
- {typeof children === "function"
92
- ? children({ pressed: true })
93
- : children}
94
- </BottomSheetView>
95
- </BottomSheet>
275
+ <NavigationStackContext.Provider value={{ stack, push, pop, isNested }}>
276
+ <BottomSheet
277
+ ref={sheetRef}
278
+ enablePanDownToClose
279
+ enableDynamicSizing
280
+ detached={isWide}
281
+ bottomInset={isWide ? 0 : 0}
282
+ backdropComponent={({ style }) => (
283
+ <Pressable
284
+ style={[style, StyleSheet.absoluteFill]}
285
+ onPress={() => onBackgroundTap()}
286
+ />
287
+ )}
288
+ onClose={() => onOpenChange?.(false)}
289
+ style={[
290
+ overlayStyle,
291
+ StyleSheet.flatten(rest.style),
292
+ isWide && { marginHorizontal: horizontalMargin },
293
+ ]}
294
+ backgroundStyle={[zt.bg.popover, a.radius.all.md, a.shadows.md, p[1]]}
295
+ handleIndicatorStyle={[
296
+ a.sizes.width[12],
297
+ a.sizes.height[1],
298
+ zt.bg.mutedForeground,
299
+ ]}
300
+ >
301
+ {isNested && (
302
+ <Animated.View
303
+ style={[
304
+ headerAnimatedStyle,
305
+ a.layout.flex.row,
306
+ a.layout.flex.alignCenter,
307
+ px[4],
308
+ pb[2],
309
+ {
310
+ borderBottomWidth: 1,
311
+ borderBottomColor: theme.colors.border,
312
+ },
313
+ ]}
314
+ >
315
+ <Pressable
316
+ onPress={pop}
317
+ style={[
318
+ a.layout.flex.row,
319
+ a.layout.flex.alignCenter,
320
+ gap.all[2],
321
+ ]}
322
+ hitSlop={80}
323
+ >
324
+ <ChevronLeft size={20} color={theme.colors.foreground} />
325
+ {currentLevel?.title ? (
326
+ <Text size="lg">{currentLevel.title}</Text>
327
+ ) : null}
328
+ </Pressable>
329
+ </Animated.View>
330
+ )}
331
+ <Animated.View style={animatedStyle}>
332
+ <BottomSheetScrollView
333
+ style={[px[4]]}
334
+ contentContainerStyle={{
335
+ paddingBottom: insets.bottom + 50,
336
+ overflow: "hidden",
337
+ }}
338
+ >
339
+ {/* Render all stack levels to keep components mounted, but hide non-current ones */}
340
+ {stack.map((level, index) => {
341
+ const isCurrent = index === stack.length - 1;
342
+ return (
343
+ <View
344
+ key={level.key}
345
+ style={[{ display: isCurrent ? "flex" : "none" }]}
346
+ >
347
+ {typeof level.content === "function"
348
+ ? level.content({ pressed: true })
349
+ : level.content}
350
+ </View>
351
+ );
352
+ })}
353
+ </BottomSheetScrollView>
354
+ </Animated.View>
355
+ </BottomSheet>
356
+ </NavigationStackContext.Provider>
96
357
  </DropdownMenuPrimitive.Portal>
97
358
  );
98
359
  });
99
360
 
100
361
  export const DropdownMenuSubTrigger = forwardRef<
101
362
  any,
102
- DropdownMenuPrimitive.SubTriggerProps & { inset?: boolean } & {
363
+ DropdownMenuPrimitive.SubTriggerProps & {
364
+ inset?: boolean;
365
+ subMenuTitle?: string;
366
+ } & {
103
367
  ref?: React.RefObject<DropdownMenuPrimitive.SubTriggerRef>;
104
368
  className?: string;
105
369
  inset?: boolean;
106
370
  children?: React.ReactNode;
107
371
  }
108
- >(({ inset, children, ...props }, ref) => {
372
+ >(({ inset, children, subMenuTitle, ...props }, ref) => {
373
+ const navStack = useNavigationStack();
374
+ const subMenuContext = useContext(SubMenuContext);
375
+ const { icons, theme } = useTheme();
376
+
377
+ // Set the title in the submenu context if provided
378
+ React.useEffect(() => {
379
+ if (subMenuContext && subMenuTitle) {
380
+ subMenuContext.setTitle(subMenuTitle);
381
+ }
382
+ }, [subMenuContext, subMenuTitle]);
383
+
384
+ // If we're in a navigation stack (mobile bottom sheet), handle differently
385
+ if (navStack && subMenuContext) {
386
+ return (
387
+ <Pressable
388
+ onPress={() => {
389
+ subMenuContext.trigger();
390
+ }}
391
+ {...props}
392
+ >
393
+ <View
394
+ style={[
395
+ inset && gap[2],
396
+ layout.flex.row,
397
+ layout.flex.alignCenter,
398
+ a.radius.all.sm,
399
+ py[1],
400
+ pl[2],
401
+ pr[2],
402
+ ]}
403
+ >
404
+ {typeof children === "function" ? (
405
+ children({ pressed: true })
406
+ ) : typeof children === "string" ? (
407
+ <Text>{children}</Text>
408
+ ) : (
409
+ children
410
+ )}
411
+ <View style={[a.layout.position.absolute, a.position.right[1]]}>
412
+ <ChevronRight size={18} color={icons.color.muted} />
413
+ </View>
414
+ </View>
415
+ </Pressable>
416
+ );
417
+ }
418
+
419
+ // Web behavior - use primitive
109
420
  const { open } = DropdownMenuPrimitive.useSubContext();
110
- const { icons } = useTheme();
111
421
  const Icon =
112
422
  Platform.OS === "web" ? ChevronRight : open ? ChevronUp : ChevronDown;
113
423
  return (
@@ -140,9 +450,42 @@ export const DropdownMenuSubTrigger = forwardRef<
140
450
 
141
451
  export const DropdownMenuSubContent = forwardRef<
142
452
  any,
143
- DropdownMenuPrimitive.SubContentProps
144
- >((props, ref) => {
453
+ DropdownMenuPrimitive.SubContentProps & { children?: ReactNode }
454
+ >(({ children, ...props }, ref) => {
145
455
  const { zero: zt } = useTheme();
456
+ const subMenuContext = useContext(SubMenuContext);
457
+ const navStack = useNavigationStack();
458
+ const prevChildrenRef = useRef<ReactNode>(null);
459
+
460
+ // Register a render function that will be called fresh each time
461
+ React.useEffect(() => {
462
+ if (subMenuContext && navStack) {
463
+ // Only update if children reference actually changed
464
+ if (prevChildrenRef.current === children) {
465
+ return;
466
+ }
467
+
468
+ prevChildrenRef.current = children;
469
+
470
+ // Pass a function that returns the current children
471
+ subMenuContext.setRenderContent(() => children);
472
+
473
+ // Force a stack update to trigger rerender with the actual children
474
+ if (subMenuContext.key) {
475
+ // Store the children directly so React can handle updates
476
+ //navStack.updateContent(subMenuContext.key, children);
477
+ }
478
+ }
479
+ }, [children, subMenuContext, navStack]);
480
+
481
+ // On mobile, don't render the subcontent here - it'll be rendered in the nav stack
482
+ // But keep the component mounted so effects run when children change
483
+ if (navStack && subMenuContext) {
484
+ // Component stays mounted to track prop changes, but renders nothing
485
+ return null;
486
+ }
487
+
488
+ // Web - use primitive
146
489
  return (
147
490
  <DropdownMenuPrimitive.SubContent
148
491
  ref={ref}
@@ -159,7 +502,9 @@ export const DropdownMenuSubContent = forwardRef<
159
502
  a.shadows.md,
160
503
  ]}
161
504
  {...props}
162
- />
505
+ >
506
+ {children}
507
+ </DropdownMenuPrimitive.SubContent>
163
508
  );
164
509
  });
165
510
 
@@ -169,8 +514,11 @@ export const DropdownMenuContent = forwardRef<
169
514
  overlayStyle?: any;
170
515
  portalHost?: string;
171
516
  }
172
- >(({ overlayStyle, portalHost, ...props }, ref) => {
517
+ >(({ overlayStyle, portalHost, style, children, ...props }, ref) => {
173
518
  const { zero: zt } = useTheme();
519
+ const { height } = useWindowDimensions();
520
+ const maxHeight = height * 0.8;
521
+
174
522
  return (
175
523
  <DropdownMenuPrimitive.Portal hostName={portalHost}>
176
524
  <DropdownMenuPrimitive.Overlay
@@ -193,10 +541,17 @@ export const DropdownMenuContent = forwardRef<
193
541
  zt.bg.popover,
194
542
  p[2],
195
543
  a.shadows.md,
544
+ style,
196
545
  ] as any
197
546
  }
198
547
  {...props}
199
- />
548
+ >
549
+ <ScrollView style={{ maxHeight }} showsVerticalScrollIndicator={true}>
550
+ {typeof children === "function"
551
+ ? children({ pressed: false })
552
+ : children}
553
+ </ScrollView>
554
+ </DropdownMenuPrimitive.Content>
200
555
  </DropdownMenuPrimitive.Overlay>
201
556
  </DropdownMenuPrimitive.Portal>
202
557
  );
@@ -206,37 +561,52 @@ export const DropdownMenuContentWithoutPortal = forwardRef<
206
561
  any,
207
562
  DropdownMenuPrimitive.ContentProps & {
208
563
  overlayStyle?: any;
564
+ maxHeightPercentage?: number;
209
565
  }
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
- });
566
+ >(
567
+ (
568
+ { overlayStyle, maxHeightPercentage = 0.8, children, style, ...props },
569
+ ref,
570
+ ) => {
571
+ const { theme } = useTheme();
572
+ const { height } = useWindowDimensions();
573
+ const maxHeight = height * maxHeightPercentage;
574
+
575
+ return (
576
+ <DropdownMenuPrimitive.Overlay
577
+ style={[
578
+ Platform.OS !== "web" ? StyleSheet.absoluteFill : undefined,
579
+ overlayStyle,
580
+ ]}
581
+ >
582
+ <DropdownMenuPrimitive.Content
583
+ ref={ref}
584
+ style={
585
+ [
586
+ { zIndex: 999999 },
587
+ a.sizes.minWidth[32],
588
+ a.sizes.maxWidth[64],
589
+ a.radius.all.md,
590
+ a.borders.width.thin,
591
+ { borderColor: theme.colors.border },
592
+ { backgroundColor: theme.colors.popover },
593
+ p[2],
594
+ a.shadows.md,
595
+ style,
596
+ ] as any
597
+ }
598
+ {...props}
599
+ >
600
+ <ScrollView style={{ maxHeight }} showsVerticalScrollIndicator={true}>
601
+ {typeof children === "function"
602
+ ? children({ pressed: false })
603
+ : children}
604
+ </ScrollView>
605
+ </DropdownMenuPrimitive.Content>
606
+ </DropdownMenuPrimitive.Overlay>
607
+ );
608
+ },
609
+ );
240
610
 
241
611
  /// Responsive Dropdown Menu Content. On mobile this will render a *bottom sheet* that is **portaled to the root of the app**.
242
612
  /// Prefer passing scoped content in as **otherwise it may crash the app**.
@@ -245,7 +615,7 @@ export const ResponsiveDropdownMenuContent = forwardRef<any, any>(
245
615
  const { width } = useWindowDimensions();
246
616
 
247
617
  // On web, you might want to always use the normal dropdown
248
- const isBottomSheet = Platform.OS !== "web" && width < 800;
618
+ const isBottomSheet = Platform.OS !== "web";
249
619
 
250
620
  if (isBottomSheet) {
251
621
  return (
@@ -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) {