Live video on the AT Protocol

Render items on web slightly differently, use sheet on (thin) web as well

+129 -26
+4 -4
js/components/src/components/mobile-player/ui/viewer-context-menu.tsx
··· 179 179 <View 180 180 style={[ 181 181 zero.flex.values[1], 182 - zero.layout.flex.row, 182 + isMobile ? zero.layout.flex.row : zero.layout.flex.column, 183 183 zero.layout.flex.spaceBetween, 184 184 zero.pr[4], 185 185 ]} 186 186 > 187 187 <Text>Quality</Text> 188 - <Text muted> 189 - ({quality}, {lowLatency ? "low latency" : "regular latency"} 190 - ) 188 + <Text muted size={isMobile ? "base" : "sm"}> 189 + {quality === "source" ? "Source" : quality},{" "} 190 + {lowLatency ? "Low Latency" : ""} 191 191 </Text> 192 192 </View> 193 193 </DropdownMenuSubTrigger>
+125 -22
js/components/src/components/ui/dropdown.tsx
··· 94 94 export const DropdownMenu = DropdownMenuPrimitive.Root; 95 95 export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 96 96 export const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 97 - export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 97 + 98 + export const DropdownMenuRadioGroup = forwardRef< 99 + React.ElementRef<typeof DropdownMenuPrimitive.RadioGroup>, 100 + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioGroup> 101 + >(({ children, ...props }, ref) => { 102 + const navStack = useNavigationStack(); 103 + 104 + // On mobile/navigation stack, just render children without the primitive RadioGroup 105 + if (navStack) { 106 + return <>{children}</>; 107 + } 108 + 109 + // Web - use primitive 110 + return ( 111 + <DropdownMenuPrimitive.RadioGroup ref={ref} {...props}> 112 + {children} 113 + </DropdownMenuPrimitive.RadioGroup> 114 + ); 115 + }); 98 116 99 117 // Custom DropdownMenuSub that works with mobile navigation 100 118 export const DropdownMenuSub = forwardRef<any, any>( ··· 169 187 const { zero: zt, theme } = useTheme(); 170 188 const sheetRef = useRef<BottomSheet>(null); 171 189 const { width } = useWindowDimensions(); 172 - const isWide = Platform.OS !== "web" && width >= 800; 190 + const isWide = width >= 450; 173 191 const sheetWidth = isWide ? 450 : width; 174 192 const horizontalMargin = isWide ? (width - sheetWidth) / 2 : 0; 175 193 ··· 491 509 ref={ref} 492 510 style={[ 493 511 a.zIndex[50], 494 - a.sizes.minWidth[32], 512 + a.sizes.minWidth[64], 513 + a.sizes.maxWidth[64], 495 514 a.overflow.hidden, 496 515 a.radius.all.md, 497 516 a.borders.width.thin, ··· 532 551 style={ 533 552 [ 534 553 a.zIndex[50], 535 - a.sizes.minWidth[32], 554 + a.sizes.minWidth[64], 536 555 a.sizes.maxWidth[64], 537 556 a.overflow.hidden, 538 557 a.radius.all.md, ··· 584 603 style={ 585 604 [ 586 605 { zIndex: 999999 }, 587 - a.sizes.minWidth[32], 606 + a.sizes.minWidth[64], 588 607 a.sizes.maxWidth[64], 589 608 a.radius.all.md, 590 609 a.borders.width.thin, ··· 610 629 611 630 /// Responsive Dropdown Menu Content. On mobile this will render a *bottom sheet* that is **portaled to the root of the app**. 612 631 /// Prefer passing scoped content in as **otherwise it may crash the app**. 613 - export const ResponsiveDropdownMenuContent = forwardRef<any, any>( 614 - ({ children, ...props }, ref) => { 615 - const { width } = useWindowDimensions(); 632 + export const ResponsiveDropdownMenuContent = forwardRef< 633 + any, 634 + any & { onModeChange?: (isSheet: boolean) => void } 635 + >(({ children, onModeChange, ...props }, ref) => { 636 + const { width } = useWindowDimensions(); 616 637 617 - // On web, you might want to always use the normal dropdown 618 - const isBottomSheet = Platform.OS !== "web"; 638 + // if we're non-web or not showing sidebar (<=980px) show bottom sheet 639 + const isBottomSheet = 640 + Platform.OS !== "web" || (Platform.OS === "web" && width <= 980); 619 641 620 - if (isBottomSheet) { 621 - return ( 622 - <DropdownMenuBottomSheet ref={ref} {...props}> 623 - {children} 624 - </DropdownMenuBottomSheet> 625 - ); 626 - } 642 + // Notify parent of mode 643 + React.useEffect(() => { 644 + onModeChange?.(isBottomSheet); 645 + }, [isBottomSheet, onModeChange]); 646 + 647 + if (isBottomSheet) { 627 648 return ( 628 - <DropdownMenuContent ref={ref} {...props}> 649 + <DropdownMenuBottomSheet ref={ref} {...props}> 629 650 {children} 630 - </DropdownMenuContent> 651 + </DropdownMenuBottomSheet> 631 652 ); 632 - }, 633 - ); 653 + } 654 + return ( 655 + <DropdownMenuContent ref={ref} {...props}> 656 + {children} 657 + </DropdownMenuContent> 658 + ); 659 + }); 634 660 635 661 export const DropdownMenuItem = forwardRef< 636 662 any, ··· 678 704 } 679 705 >(({ children, checked, ...props }, ref) => { 680 706 const { theme } = useTheme(); 707 + const navStack = useNavigationStack(); 708 + 709 + // On mobile/navigation stack, render as a Pressable that doesn't depend on primitive context 710 + if (navStack) { 711 + return ( 712 + <Pressable 713 + onPress={(e) => { 714 + props.onCheckedChange?.(!checked); 715 + props.onPress?.(e); 716 + }} 717 + {...props} 718 + > 719 + <View 720 + style={[ 721 + a.layout.flex.row, 722 + a.layout.flex.alignCenter, 723 + a.radius.all.sm, 724 + py[1], 725 + pl[2], 726 + pr[2], 727 + pr[8], 728 + ]} 729 + > 730 + {children} 731 + <View style={[pl[1], layout.position.absolute, right[1]]}> 732 + {checked ? ( 733 + <CheckCircle 734 + size={14} 735 + strokeWidth={3} 736 + color={theme.colors.foreground} 737 + /> 738 + ) : ( 739 + <Circle 740 + size={14} 741 + strokeWidth={3} 742 + color={theme.colors.mutedForeground} 743 + /> 744 + )} 745 + </View> 746 + </View> 747 + </Pressable> 748 + ); 749 + } 750 + 751 + // Web - use primitive 681 752 return ( 682 753 <DropdownMenuPrimitive.CheckboxItem 683 754 ref={ref} ··· 722 793 DropdownMenuPrimitive.RadioItemProps & { 723 794 ref?: React.RefObject<DropdownMenuPrimitive.RadioItemRef>; 724 795 children?: React.ReactNode; 796 + value?: string; 725 797 } 726 - >(({ children, ...props }, ref) => { 798 + >(({ children, value, ...props }, ref) => { 727 799 const { theme } = useTheme(); 800 + const navStack = useNavigationStack(); 801 + 802 + // On mobile/navigation stack, render as a Pressable that doesn't depend on primitive context 803 + if (navStack) { 804 + // For radio items in nav stack, we need to get the current value from RadioGroup context 805 + // For now, render as a simple pressable 806 + return ( 807 + <Pressable 808 + onPress={(e) => { 809 + props.onPress?.(e); 810 + }} 811 + {...props} 812 + > 813 + <View 814 + style={[ 815 + a.layout.flex.row, 816 + a.layout.flex.alignCenter, 817 + a.radius.all.sm, 818 + py[1], 819 + pl[2], 820 + pr[1], 821 + ]} 822 + > 823 + {children} 824 + </View> 825 + </Pressable> 826 + ); 827 + } 828 + 829 + // Web - use primitive 728 830 return ( 729 831 <DropdownMenuPrimitive.RadioItem 730 832 ref={ref} 731 833 closeOnPress={props.closeOnPress || false} 834 + value={value} 732 835 {...props} 733 836 > 734 837 <View