ci(release): publish latest release

parent c32c1f3e
......@@ -4,8 +4,7 @@ Added more supported tokens on Fiat Onramp — Now users can enjoy access to USD
Other changes:
- Support for “Enter” during onboarding
- Username support on notification toasts
- Improved warning modals
- Improved linkouts to helpcenter articles
- Improved linkouts to help center articles
- Various bug fixes and performance improvements
extension/1.5.0
\ No newline at end of file
mobile/1.35
\ No newline at end of file
......@@ -39,7 +39,7 @@ struct ActionButton<Icon: Shape>: View {
}
Text(text)
.foregroundColor(textColor)
.font(Font(UIFont(name: "Basel Grotesk Book", size: 16)!))
.font(Font(UIFont(name: "BaselGrotesk-Book", size: 16)!))
}
}
.padding(EdgeInsets(top: 8, leading: icon != nil ? 12 : 16, bottom: 8, trailing: 16))
......
......@@ -186,10 +186,7 @@
A70E4DD82C260416002D6D86 /* SwapOrderStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70E4DD62C260416002D6D86 /* SwapOrderStatus.graphql.swift */; };
A7B8EFCB2BF68F0D00CA4A1C /* FeeData.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B8EFCA2BF68F0D00CA4A1C /* FeeData.graphql.swift */; };
AC0EE0982BD826E700BCCF07 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC0EE0972BD826E700BCCF07 /* PrivacyInfo.xcprivacy */; };
AE1757582C49B0A7000120A5 /* Basel-Grotesk-Book.otf in Resources */ = {isa = PBXBuildFile; fileRef = AE1757572C49B0A1000120A5 /* Basel-Grotesk-Book.otf */; };
AE1757592C49B0AA000120A5 /* Basel-Grotesk-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = AE1757562C49B0A1000120A5 /* Basel-Grotesk-Medium.otf */; };
AEB2767B2C49CB060056FE52 /* Basel-Grotesk-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = AE1757562C49B0A1000120A5 /* Basel-Grotesk-Medium.otf */; };
AEB2767C2C49CB080056FE52 /* Basel-Grotesk-Book.otf in Resources */ = {isa = PBXBuildFile; fileRef = AE1757572C49B0A1000120A5 /* Basel-Grotesk-Book.otf */; };
AC2EF4032C914B1600EEEFDB /* fonts in Resources */ = {isa = PBXBuildFile; fileRef = AC2EF4022C914B1600EEEFDB /* fonts */; };
B193AD315CF844A3BDC3D11D /* Basel-Grotesk-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3C606D2C81014A0A8898F38E /* Basel-Grotesk-Medium.otf */; };
D3B63ACA9B0C42F68080B080 /* InputMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */; };
F35AFD3E27EE49990011A725 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35AFD3D27EE49990011A725 /* NotificationService.swift */; };
......@@ -503,8 +500,7 @@
A7C9F415D0E128A43003E071 /* Pods-Uniswap.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.debug.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.debug.xcconfig"; sourceTree = "<group>"; };
AC0EE0972BD826E700BCCF07 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Uniswap/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
AC2794442C51541E00F9AF68 /* sourcemaps-datadog.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "sourcemaps-datadog.sh"; sourceTree = "<group>"; };
AE1757562C49B0A1000120A5 /* Basel-Grotesk-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Basel-Grotesk-Medium.otf"; path = "../../src/assets/fonts/Basel-Grotesk-Medium.otf"; sourceTree = "<group>"; };
AE1757572C49B0A1000120A5 /* Basel-Grotesk-Book.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Basel-Grotesk-Book.otf"; path = "../../src/assets/fonts/Basel-Grotesk-Book.otf"; sourceTree = "<group>"; };
AC2EF4022C914B1600EEEFDB /* fonts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fonts; path = ../src/assets/fonts; sourceTree = "<group>"; };
B0DA4D39B1A6D74A1D05B99F /* Pods-WidgetsCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.debug.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.debug.xcconfig"; sourceTree = "<group>"; };
B2176D5449C2B3B68A17466B /* libPods-WidgetsCoreTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WidgetsCoreTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
BCB2A43E5FB0D7B69CA02312 /* Pods-WidgetsCore.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.dev.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.dev.xcconfig"; sourceTree = "<group>"; };
......@@ -694,15 +690,6 @@
path = MobileSchema;
sourceTree = "<group>";
};
074321872A82BA2700F8518D /* Fonts */ = {
isa = PBXGroup;
children = (
AE1757572C49B0A1000120A5 /* Basel-Grotesk-Book.otf */,
AE1757562C49B0A1000120A5 /* Basel-Grotesk-Medium.otf */,
);
path = Fonts;
sourceTree = "<group>";
};
0743218E2A83E3C900F8518D /* Operations */ = {
isa = PBXGroup;
children = (
......@@ -1025,8 +1012,8 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
AC2EF4022C914B1600EEEFDB /* fonts */,
AC2794442C51541E00F9AF68 /* sourcemaps-datadog.sh */,
074321872A82BA2700F8518D /* Fonts */,
FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */,
13B07FAE1A68108700A75B9A /* Uniswap */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
......@@ -1419,8 +1406,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AEB2767B2C49CB060056FE52 /* Basel-Grotesk-Medium.otf in Resources */,
AEB2767C2C49CB080056FE52 /* Basel-Grotesk-Book.otf in Resources */,
072F6C2B2A44A32F00DA720A /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
......@@ -1436,13 +1421,13 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AE1757592C49B0AA000120A5 /* Basel-Grotesk-Medium.otf in Resources */,
FD7304CE28A364FC0085BDEA /* Colors.xcassets in Resources */,
A32F9FBD272343C9002CFCDB /* GoogleService-Info.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
77CF6065C8A24FE48204A2C1 /* SplashScreen.storyboard in Resources */,
AC0EE0982BD826E700BCCF07 /* PrivacyInfo.xcprivacy in Resources */,
D3B63ACA9B0C42F68080B080 /* InputMono-Regular.ttf in Resources */,
AC2EF4032C914B1600EEEFDB /* fonts in Resources */,
A3551F2CAC134AD49D40927F /* Basel-Grotesk-Book.otf in Resources */,
B193AD315CF844A3BDC3D11D /* Basel-Grotesk-Medium.otf in Resources */,
);
......
......@@ -14,8 +14,8 @@ struct BankWord: Hashable {
}
struct MnemonicConfirmationWordBankView: View {
let smallFont = UIFont(name: "Basel Grotesk Book", size: 14)
let mediumFont = UIFont(name: "Basel Grotesk Book", size: 16)
let smallFont = UIFont(name: "BaselGrotesk-Book", size: 14)
let mediumFont = UIFont(name: "BaselGrotesk-Book", size: 16)
var groupedWords: [[BankWord]] = [[BankWord]]()
let screenWidth = UIScreen.main.bounds.width // Used to calculate max number of tags per row
......
......@@ -54,7 +54,7 @@ struct MnemonicDisplay: View {
@State private var buttonPadding: CGFloat = 20
let rnEthersRS = RNEthersRS()
let interFont = UIFont(name: "Basel-Grotesk-Medium", size: 20)
let interFont = UIFont(name: "BaselGrotesk-Medium", size: 20)
func setMnemonicId(mnemonicId: String) {
props.mnemonicId = mnemonicId
......
......@@ -14,8 +14,8 @@ enum MnemonicInputStatus {
}
struct MnemonicTextField: View {
let smallFont = UIFont(name: "Basel Grotesk Book", size: 14)
let mediumFont = UIFont(name: "Basel Grotesk Book", size: 16)
let smallFont = UIFont(name: "BaselGrotesk-Book", size: 14)
let mediumFont = UIFont(name: "BaselGrotesk-Book", size: 16)
var index: Int
var word = ""
......
......@@ -109,10 +109,10 @@ struct SeedPhraseInput: View {
@ObservedObject var viewModel = SeedPhraseInputViewModel()
@FocusState private var focused: Bool
private var font = Font(UIFont(name: "Basel Grotesk Book", size: 17)!)
private var subtitleFont = Font(UIFont(name: "Basel Grotesk Book", size: 17)!)
private var labelFont = Font(UIFont(name: "Basel Grotesk Book", size: 15)!)
private var buttonFont = Font(UIFont(name: "Basel Grotesk Medium", size: 15)!)
private var font = Font(UIFont(name: "BaselGrotesk-Book", size: 17)!)
private var subtitleFont = Font(UIFont(name: "BaselGrotesk-Book", size: 17)!)
private var labelFont = Font(UIFont(name: "BaselGrotesk-Book", size: 15)!)
private var buttonFont = Font(UIFont(name: "BaselGrotesk-Medium", size: 15)!)
var body: some View {
VStack(spacing: 12) {
......
......@@ -10,17 +10,17 @@ import SwiftUI
public extension Text {
func withHeading1Style() -> some View {
self.font(.custom("Basel Grotesk Book", size: 28))
self.font(.custom("BaselGrotesk-Book", size: 28))
.foregroundColor(.white)
}
func withHeading2Style() -> some View {
self.font(.custom("Basel Grotesk Book", size: 20))
self.font(.custom("BaselGrotesk-Book", size: 20))
.foregroundColor(.widgetLightGrey)
}
func withHeading3Style() -> some View {
self.font(.custom("Basel Grotesk Medium", size: 12))
self.font(.custom("BaselGrotesk-Medium", size: 12))
.foregroundColor(.widgetGrey)
}
}
......
import { memo, useMemo } from 'react'
import { PropsWithChildren, ReactElement, memo, useMemo } from 'react'
import { I18nManager } from 'react-native'
import { SharedValue, useDerivedValue } from 'react-native-reanimated'
import { LineChart, LineChartProvider } from 'react-native-wagmi-charts'
import PriceExplorerAnimatedNumber from 'src/components/PriceExplorer/PriceExplorerAnimatedNumber'
import { PriceExplorerError } from 'src/components/PriceExplorer/PriceExplorerError'
import { DatetimeText, RelativeChangeText } from 'src/components/PriceExplorer/Text'
import { TimeRangeGroup } from 'src/components/PriceExplorer/TimeRangeGroup'
import { CURSOR_INNER_SIZE, CURSOR_SIZE } from 'src/components/PriceExplorer/constants'
import { CURSOR_INNER_SIZE, CURSOR_SIZE, TIME_RANGES } from 'src/components/PriceExplorer/constants'
import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions'
import { useLineChartPrice } from 'src/components/PriceExplorer/usePrice'
import { PriceNumberOfDigits, TokenSpotData, useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory'
import { Loader } from 'src/components/loading'
import { Flex, useHapticFeedback } from 'ui/src'
import { Flex, SegmentedControl, Text, useHapticFeedback } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementNameType } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { CurrencyId } from 'uniswap/src/types/currency'
import { isDetoxBuild } from 'utilities/src/environment/constants'
......@@ -43,6 +44,15 @@ function PriceTextSection({ loading, numberOfDigits, spotPrice }: PriceTextProps
)
}
const TimeRangeTraceWrapper = ({
children,
elementName,
}: PropsWithChildren<{ elementName: ElementNameType }>): ReactElement => (
<Trace logPress element={elementName}>
{children}
</Trace>
)
export type LineChartPriceAndDateTimeTextProps = {
currencyId: CurrencyId
}
......@@ -128,7 +138,26 @@ export const PriceExplorer = memo(function PriceExplorer({
spotPrice={convertedSpot?.value}
/>
{content}
<TimeRangeGroup setDuration={setDuration} />
<Flex px="$spacing8">
<SegmentedControl
fullWidth
options={TIME_RANGES.map(([duration, label, elementName]) => ({
value: duration,
wrapper: <TimeRangeTraceWrapper key={`${duration}-trace`} elementName={elementName} />,
display: (
<Text
allowFontScaling={false}
testID={`token-details-chart-time-range-button-${label}`}
variant="buttonLabel2"
>
{label}
</Text>
),
}))}
selectedOption={selectedDuration}
onSelectOption={setDuration}
/>
</Flex>
</Flex>
</LineChartProvider>
)
......
import React, { useState } from 'react'
import { I18nManager, StyleSheet, View } from 'react-native'
import { SharedValue, interpolateColor, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { TIME_RANGES } from 'src/components/PriceExplorer/constants'
import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions'
import { Flex, TouchableArea, useSporeColors } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { AnimatedText } from 'ui/src/components/text/AnimatedText'
import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
interface Props {
label: string
index: number
selectedIndex: SharedValue<number>
transition: SharedValue<number>
}
export function TimeRangeLabel({ index, label, selectedIndex, transition }: Props): JSX.Element {
const colors = useSporeColors()
const style = useAnimatedStyle(() => {
const selected = index === selectedIndex.value
if (!selected) {
return { color: colors.neutral2.val }
}
const color = interpolateColor(transition.value, [0, 1], [colors.neutral2.val, colors.neutral1.val])
return { color }
})
return (
<AnimatedText
allowFontScaling={false}
style={style}
testID={`token-details-chart-time-range-button-${label}`}
textAlign="center"
variant="buttonLabel2"
>
{label}
</AnimatedText>
)
}
export function TimeRangeGroup({ setDuration }: { setDuration: (newDuration: HistoryDuration) => void }): JSX.Element {
const { chartWidth, buttonWidth, labelWidth } = useChartDimensions()
const transition = useSharedValue(1)
const previousIndex = useSharedValue(1)
const currentIndex = useSharedValue(1)
const [adjustedLabelWidth, setAdjustedLabelWidth] = useState(labelWidth)
const isRTL = I18nManager.isRTL
// animates slider (time range label background) on press
const sliderStyle = useAnimatedStyle(
() => ({
transform: [
{
translateX:
(buttonWidth * currentIndex.value + (buttonWidth - adjustedLabelWidth) / 2) *
// left if RTL, right if LTR
(isRTL ? -1 : 1),
},
],
}),
[adjustedLabelWidth, buttonWidth, currentIndex, isRTL],
)
return (
<Flex row alignSelf="center" width={chartWidth}>
<View style={StyleSheet.absoluteFill}>
<AnimatedFlex
backgroundColor="$surface3"
borderRadius="$rounded20"
style={[StyleSheet.absoluteFillObject, sliderStyle]}
width={adjustedLabelWidth}
/>
</View>
{TIME_RANGES.map(([duration, label, element], index) => {
return (
<Trace key={label} logPress element={element}>
<TouchableArea
p="$spacing4"
width={buttonWidth}
onPress={(): void => {
setDuration(duration)
previousIndex.value = currentIndex.value
transition.value = 0
currentIndex.value = index
transition.value = 1
}}
>
<Flex
alignSelf="center"
minWidth={adjustedLabelWidth}
px="$spacing8"
onLayout={({
nativeEvent: {
layout: { width },
},
}): void => {
if (width > adjustedLabelWidth) {
setAdjustedLabelWidth(width)
}
}}
>
<TimeRangeLabel index={index} label={label} selectedIndex={currentIndex} transition={transition} />
</Flex>
</TouchableArea>
</Trace>
)
})}
</Flex>
)
}
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Button, SpinningLoader } from 'ui/src'
import { Button, SpinningLoader, useIsShortMobileDevice } from 'ui/src'
import { InfoCircleFilled } from 'ui/src/components/icons'
interface FiatOnRampCtaButtonProps {
......@@ -19,6 +19,7 @@ export function FiatOnRampCtaButton({
onPress,
}: FiatOnRampCtaButtonProps): JSX.Element {
const { t } = useTranslation()
const isShortMobileDevice = useIsShortMobileDevice()
const buttonAvailable = eligible || isLoading
const continueText = eligible ? continueButtonText : t('fiatOnRamp.error.unsupported')
return (
......@@ -28,7 +29,7 @@ export function FiatOnRampCtaButton({
icon={
isLoading ? <SpinningLoader color="$white" /> : !eligible ? <InfoCircleFilled color="$neutral3" /> : undefined
}
size="large"
size={isShortMobileDevice ? 'small' : 'large'}
theme={buttonAvailable ? 'primary' : 'tertiary'}
onPress={onPress}
>
......
......@@ -62,7 +62,7 @@ export function CloudBackupPasswordFormContextProvider({
const onPasswordChangeText = useCallback(
(newPassword: string): void => {
if (isConfirmation && newPassword === password) {
if (isConfirmation) {
setError(undefined)
}
// always reset error if not confirmation
......@@ -72,7 +72,7 @@ export function CloudBackupPasswordFormContextProvider({
}
setPassword(newPassword)
},
[isConfirmation, password],
[isConfirmation],
)
const onPasswordSubmitEditing = useCallback((): void => {
......
......@@ -4,7 +4,15 @@ import { useTranslation } from 'react-i18next'
import { NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData } from 'react-native'
import { TouchableOpacity } from 'react-native-gesture-handler'
import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { ColorTokens, Flex, Text, TouchableArea, useHapticFeedback, useSporeColors } from 'ui/src'
import {
ColorTokens,
Flex,
Text,
TouchableArea,
useHapticFeedback,
useIsShortMobileDevice,
useSporeColors,
} from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { fonts, spacing } from 'ui/src/theme'
......@@ -83,6 +91,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
forwardedRef,
): JSX.Element {
const { t } = useTranslation()
const isShortMobileDevice = useIsShortMobileDevice()
const {
onLayout: onInputLayout,
fontSize,
......@@ -212,7 +221,7 @@ export const FiatOnRampAmountSection = forwardRef<FiatOnRampAmountSectionRef, Fi
</Flex>
</AnimatedFlex>
{!value && predefinedAmountsSupported ? (
<Flex centered row gap="$spacing12" mt="$spacing8" pb="$spacing4">
<Flex centered row gap="$spacing12" mt={isShortMobileDevice ? 0 : '$spacing8'} pb="$spacing4">
{PREDEFINED_AMOUNTS.map((amount) => (
<PredefinedAmount
key={amount}
......
/* eslint-disable complexity */
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { ComponentProps, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
......@@ -344,7 +345,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
{isSheetReady && (
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing16" px="$spacing24" width="100%">
{isOffRampEnabled ? (
<Flex row justifyContent="center" mt="$spacing6">
<Flex row justifyContent="center" mt={isShortMobileDevice ? 0 : '$spacing6'}>
<OffRampPopover
triggerContent={
<PillMultiToggle
......@@ -400,15 +401,15 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
<AnimatedFlex
bottom={0}
exiting={FadeOutDown}
gap="$spacing8"
gap={isShortMobileDevice ? 0 : '$spacing8'}
left={0}
opacity={decimalPadReady ? 1 : 0}
pb="$spacing24"
pb={isShortMobileDevice ? '$spacing4' : '$spacing24'}
position="absolute"
px="$spacing24"
right={0}
>
{quoteCurrency.currencyInfo && formattedAmount && (
{quoteCurrency.currencyInfo && (
<TokenSelectorBalanceDisplay
disabled={notAvailableInThisRegion}
formattedAmount={formattedAmount}
......
import { refitChartContentAtom } from 'components/Charts/TimeSelector'
import { PROTOCOL_LEGEND_ELEMENT_ID, SeriesDataItemType } from 'components/Charts/types'
import { formatTickMarks } from 'components/Charts/utils'
import { MissingDataBars } from 'components/Table/icons'
import { useScreenSize } from 'hooks/screenSize'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { atom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'
import { DefaultTheme, useTheme } from 'lib/styled-components'
import {
......@@ -25,6 +25,8 @@ import { Trans } from 'uniswap/src/i18n'
import { useFormatter } from 'utils/formatNumbers'
import { v4 as uuidv4 } from 'uuid'
export const refitChartContentAtom = atom<(() => void) | undefined>(undefined)
interface ChartUtilParams<TDataType extends SeriesDataItemType> {
locale: string
theme: DefaultTheme
......
import { DISPLAYS, ORDERED_TIMES } from 'components/Tokens/TokenTable/TimeSelector'
import { TimePeriod } from 'graphql/data/util'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { Flex, Text, styled } from 'ui/src'
export const refitChartContentAtom = atom<(() => void) | undefined>(undefined)
const DEFAULT_TIME_SELECTOR_OPTIONS = ORDERED_TIMES.map((time: TimePeriod) => ({ time, display: DISPLAYS[time] }))
const TimeOptionsContainer = styled(Flex, {
justifyContent: 'flex-end',
gap: '$gap4',
borderRadius: '$rounded16',
height: 24,
px: '$spacing4',
width: 'fit-content',
overflow: 'visible',
$md: {
width: '100%',
justifyContent: 'space-between',
borderWidth: 0,
},
})
const TimeButton = styled(Flex, {
flexGrow: 1,
flexShrink: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: 24,
width: 24,
borderRadius: '$roundedFull',
cursor: 'pointer',
animation: 'fast',
borderWidth: 0,
variants: {
active: {
true: {
backgroundColor: '$surface3',
hoverStyle: {
opacity: 1,
},
},
false: {
backgroundColor: 'transparent',
hoverStyle: {
opacity: 0.6,
},
},
},
} as const,
})
interface TimePeriodSelectorOption {
time: TimePeriod // Value to be selected/stored, used as default display value
display: string // Value to be displayed
}
export default function TimePeriodSelector({
options = DEFAULT_TIME_SELECTOR_OPTIONS,
timePeriod,
onChangeTimePeriod,
className,
}: {
options?: TimePeriodSelectorOption[]
timePeriod: TimePeriod
onChangeTimePeriod: (t: TimePeriod) => void
className?: string
}) {
const refitChartContent = useAtomValue(refitChartContentAtom)
return (
<TimeOptionsContainer row className={className}>
{options.map(({ time, display }) => (
<TimeButton
key={display}
active={timePeriod === time}
onPress={() => {
if (timePeriod === time) {
refitChartContent?.()
} else {
onChangeTimePeriod(time)
}
}}
>
<Text
fontWeight="$medium"
fontSize={14}
lineHeight={14}
color={timePeriod === time ? '$neutral1' : '$neutral2'}
>
{display}
</Text>
</TimeButton>
))}
</TimeOptionsContainer>
)
}
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { ChartHeader } from 'components/Charts/ChartHeader'
import { Chart } from 'components/Charts/ChartModel'
import { Chart, refitChartContentAtom } from 'components/Charts/ChartModel'
import { LiquidityBarChartModel, useLiquidityBarData } from 'components/Charts/LiquidityChart'
import { LiquidityBarData } from 'components/Charts/LiquidityChart/renderer'
import { ChartSkeleton } from 'components/Charts/LoadingState'
import { PriceChartData, PriceChartDelta, PriceChartModel } from 'components/Charts/PriceChart'
import { refitChartContentAtom } from 'components/Charts/TimeSelector'
import { VolumeChart } from 'components/Charts/VolumeChart'
import { SingleHistogramData } from 'components/Charts/VolumeChart/renderer'
import { ChartType, PriceChartType } from 'components/Charts/utils'
import { usePDPPriceChartData, usePDPVolumeChartData } from 'components/Pools/PoolDetails/ChartSection/hooks'
import PillMultiToggle from 'components/Toggle/PillMultiToggle'
import { ChartActionsContainer, DEFAULT_PILL_TIME_SELECTOR_OPTIONS } from 'components/Tokens/TokenDetails/ChartSection'
import { ChartTypeDropdown } from 'components/Tokens/TokenDetails/ChartSection/ChartTypeSelector'
import { ChartQueryResult, DataQuality } from 'components/Tokens/TokenDetails/ChartSection/util'
import { LoadingChart } from 'components/Tokens/TokenDetails/Skeleton'
import { DISPLAYS, TimePeriodDisplay, getTimePeriodFromDisplay } from 'components/Tokens/TokenTable/TimeSelector'
import {
DISPLAYS,
TimePeriodDisplay,
getTimePeriodFromDisplay,
} from 'components/Tokens/TokenTable/VolumeTimeFrameSelector'
import { PoolData } from 'graphql/data/pools/usePoolData'
import { TimePeriod, gqlToCurrency, supportedChainIdFromGQLChain, toHistoryDuration } from 'graphql/data/util'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
......@@ -25,6 +27,7 @@ import styled, { useTheme } from 'lib/styled-components'
import { useMemo, useState } from 'react'
import { EllipsisStyle, ThemedText } from 'theme/components'
import { textFadeIn } from 'theme/styles'
import { SegmentedControl } from 'ui/src'
import { Chain, ProtocolVersion } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { Trans, t } from 'uniswap/src/i18n'
import { InterfaceChainId, UniverseChainId } from 'uniswap/src/types/chains'
......@@ -216,9 +219,9 @@ export default function ChartSection(props: ChartSectionProps) {
/>
{activeQuery.chartType !== ChartType.LIQUIDITY && (
<TimePeriodSelectorContainer>
<PillMultiToggle
<SegmentedControl
options={filteredTimeOptions.options}
currentSelected={filteredTimeOptions.selected}
selectedOption={filteredTimeOptions.selected}
onSelectOption={(option) => {
const time = getTimePeriodFromDisplay(option as TimePeriodDisplay)
if (time === timePeriod) {
......
......@@ -2,7 +2,46 @@
exports[`PoolDetailsHeader renders link for pool address 1`] = `
<DocumentFragment>
.c0 {
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 10px;
height: 20px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 10px 0 0 10px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 10px 10px 0;
object-position: 100% 0;
}
.c6 {
width: 10px;
height: 20px;
border-radius: 50%;
}
.c4 {
position: relative;
top: 0;
left: 0;
}
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
......@@ -42,6 +81,11 @@ exports[`PoolDetailsHeader renders link for pool address 1`] = `
justify-content: flex-start;
}
.c11 {
display: inline-block;
height: inherit;
}
.c7 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -50,50 +94,6 @@ exports[`PoolDetailsHeader renders link for pool address 1`] = `
letter-spacing: -0.01em;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 10px;
height: 20px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 10px 0 0 10px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 10px 10px 0;
object-position: 100% 0;
}
.c6 {
width: 10px;
height: 20px;
border-radius: 50%;
}
.c4 {
position: relative;
top: 0;
left: 0;
}
.c11 {
display: inline-block;
height: inherit;
}
.c14 {
-webkit-text-decoration: none;
text-decoration: none;
......@@ -340,7 +340,52 @@ exports[`PoolDetailsHeader renders link for pool address 1`] = `
exports[`PoolDetailsHeader renders link for token address 1`] = `
<DocumentFragment>
.c0 {
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c4 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c6 img {
width: 20px;
height: 20px;
border-radius: 50%;
}
.c7 {
width: 10px;
height: 20px;
border-radius: 50%;
}
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
......@@ -398,6 +443,11 @@ exports[`PoolDetailsHeader renders link for token address 1`] = `
gap: 4px;
}
.c13 {
display: inline-block;
height: inherit;
}
.c8 {
color: #222222;
-webkit-letter-spacing: -0.01em;
......@@ -406,56 +456,6 @@ exports[`PoolDetailsHeader renders link for token address 1`] = `
letter-spacing: -0.01em;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c4 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c6 img {
width: 20px;
height: 20px;
border-radius: 50%;
}
.c7 {
width: 10px;
height: 20px;
border-radius: 50%;
}
.c13 {
display: inline-block;
height: inherit;
}
.c16 {
-webkit-text-decoration: none;
text-decoration: none;
......
......@@ -16,6 +16,39 @@ exports[`PoolDetailsPositionsTable renders with PositionStatus Closed 1`] = `
justify-content: flex-start;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 8px;
height: 16px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 8px 0 0 8px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 8px 8px 0;
object-position: 100% 0;
}
.c6 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c3 {
box-sizing: border-box;
margin: 0;
......@@ -80,39 +113,6 @@ exports[`PoolDetailsPositionsTable renders with PositionStatus Closed 1`] = `
letter-spacing: -0.01em;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 8px;
height: 16px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 8px 0 0 8px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 8px 8px 0;
object-position: 100% 0;
}
.c6 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c1 {
gap: 24px;
margin-top: 24px;
......@@ -327,6 +327,39 @@ exports[`PoolDetailsPositionsTable renders with PositionStatus In Range 1`] = `
justify-content: flex-start;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 8px;
height: 16px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 8px 0 0 8px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 8px 8px 0;
object-position: 100% 0;
}
.c6 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c3 {
box-sizing: border-box;
margin: 0;
......@@ -391,39 +424,6 @@ exports[`PoolDetailsPositionsTable renders with PositionStatus In Range 1`] = `
letter-spacing: -0.01em;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 8px;
height: 16px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 8px 0 0 8px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 8px 8px 0;
object-position: 100% 0;
}
.c6 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c1 {
gap: 24px;
margin-top: 24px;
......@@ -613,6 +613,39 @@ exports[`PoolDetailsPositionsTable renders with PositionStatus Out Of Range 1`]
justify-content: flex-start;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 8px;
height: 16px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 8px 0 0 8px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 8px 8px 0;
object-position: 100% 0;
}
.c6 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c3 {
box-sizing: border-box;
margin: 0;
......@@ -677,39 +710,6 @@ exports[`PoolDetailsPositionsTable renders with PositionStatus Out Of Range 1`]
letter-spacing: -0.01em;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c5 img {
width: 8px;
height: 16px;
object-fit: cover;
}
.c5 img:first-child {
border-radius: 8px 0 0 8px;
object-position: 0 0;
}
.c5 img:last-child {
border-radius: 0 8px 8px 0;
object-position: 100% 0;
}
.c6 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c1 {
gap: 24px;
margin-top: 24px;
......
......@@ -16,6 +16,51 @@ exports[`PoolDetailsStats pool balance chart not visible on mobile 1`] = `
justify-content: flex-start;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c11 img {
width: 20px;
height: 20px;
border-radius: 50%;
}
.c12 {
width: 10px;
height: 20px;
border-radius: 50%;
}
.c10 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c9 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c5 {
box-sizing: border-box;
margin: 0;
......@@ -69,6 +114,10 @@ exports[`PoolDetailsStats pool balance chart not visible on mobile 1`] = `
padding: 4px 0px;
}
.c18 {
color: #FF5F52;
}
.c4 {
color: #7D7D7D;
-webkit-letter-spacing: -0.01em;
......@@ -77,55 +126,6 @@ exports[`PoolDetailsStats pool balance chart not visible on mobile 1`] = `
letter-spacing: -0.01em;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c11 img {
width: 20px;
height: 20px;
border-radius: 50%;
}
.c12 {
width: 10px;
height: 20px;
border-radius: 50%;
}
.c10 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c9 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c18 {
color: #FF5F52;
}
.c2 {
font-weight: 485;
font-size: 24px;
......@@ -499,6 +499,51 @@ exports[`PoolDetailsStats renders stats text correctly 1`] = `
justify-content: flex-start;
}
.c12 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c12 img {
width: 16px;
height: 16px;
border-radius: 50%;
}
.c13 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c10 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c5 {
box-sizing: border-box;
margin: 0;
......@@ -552,6 +597,10 @@ exports[`PoolDetailsStats renders stats text correctly 1`] = `
padding: 4px 0px;
}
.c20 {
color: #FF5F52;
}
.c4 {
color: #7D7D7D;
-webkit-letter-spacing: -0.01em;
......@@ -560,55 +609,6 @@ exports[`PoolDetailsStats renders stats text correctly 1`] = `
letter-spacing: -0.01em;
}
.c12 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c12 img {
width: 16px;
height: 16px;
border-radius: 50%;
}
.c13 {
width: 8px;
height: 16px;
border-radius: 50%;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c10 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c20 {
color: #FF5F52;
}
.c2 {
font-weight: 485;
font-size: 24px;
......
......@@ -425,7 +425,21 @@ exports[`PoolTable renders data filled state 1`] = `
exports[`PoolTable renders error state 1`] = `
<DocumentFragment>
.c6 {
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c6 {
color: #222222;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
......@@ -441,20 +455,6 @@ exports[`PoolTable renders error state 1`] = `
letter-spacing: -0.01em;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CommonBases renders without crashing 1`] = `
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c2 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin: -4px;
}
.c2 > * {
margin: 4px !important;
}
.c6 {
display: -webkit-box;
display: -webkit-flex;
......@@ -81,6 +46,41 @@ exports[`CommonBases renders without crashing 1`] = `
display: flex;
}
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c2 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin: -4px;
}
.c2 > * {
margin: 4px !important;
}
.c3 {
border: 1px solid #22222212;
border-radius: 18px;
......
import styled from 'lib/styled-components'
import { createRef, useCallback, useEffect, useMemo, useState } from 'react'
import { Z_INDEX } from 'theme/zIndex'
const togglePadding = 4
const OptionsSelector = styled.div`
display: flex;
position: relative;
justify-content: flex-end;
gap: 12px;
border: 1px solid ${({ theme }) => theme.surface3};
border-radius: 20px;
padding: ${togglePadding}px;
width: 100%;
`
const ActivePill = styled.div<{ activePillColor?: string }>`
position: absolute;
height: calc(100% - ${togglePadding * 2}px);
top: ${togglePadding}px;
background-color: ${({ theme, activePillColor }) => activePillColor || theme.neutral3};
border-radius: 16px;
transition:
left 0.3s ease,
width 0.3s ease;
`
const OptionButton = styled.button<{ active: boolean; activeTextColor?: string }>`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
font-weight: 535;
font-size: 16px;
border-radius: 15px;
line-height: 20px;
border: none;
cursor: pointer;
outline: none;
color: ${({ theme, active, activeTextColor }) => (active ? activeTextColor || theme.neutral1 : theme.neutral2)};
transition-duration: ${({ theme }) => theme.transition.duration.fast};
z-index: ${Z_INDEX.active};
transition: all 0.2s;
:hover {
${({ active, theme }) =>
!active &&
`
opacity: ${theme.opacity.hover};
background: ${theme.surface3};
`}
}
`
function getPillMultiToggleOption(option: PillMultiToggleOption | string): PillMultiToggleOption {
if (typeof option === 'string') {
return { value: option }
}
return option
}
export interface PillMultiToggleOption {
value: string | number // Value to be selected/stored, used as default display value
display?: JSX.Element // Optional custom display element
}
interface PillMultiToggleProps {
options: readonly (PillMultiToggleOption | string)[]
currentSelected: string | number
onSelectOption: (option: string | number) => void
activePillColor?: string
activeTextColor?: string
}
export default function PillMultiToggle({
options,
currentSelected,
onSelectOption,
activePillColor,
activeTextColor,
}: PillMultiToggleProps) {
const buttonRefs = useMemo(() => options.map(() => createRef<HTMLButtonElement>()), [options])
const [style, setStyle] = useState({})
const findActiveIndex = useCallback(() => {
return options.map((o) => getPillMultiToggleOption(o).value).indexOf(currentSelected)
}, [options, currentSelected])
const [activeIndex, setActiveIndex] = useState(findActiveIndex())
// set activeIndex if options or selectedOption changes
useEffect(() => {
setActiveIndex(findActiveIndex())
}, [findActiveIndex, setActiveIndex])
useEffect(() => {
const current = buttonRefs[activeIndex] ? buttonRefs[activeIndex].current : undefined
setStyle(
current
? {
left: current?.offsetLeft,
width: current?.offsetWidth,
}
: { display: 'none' },
)
}, [buttonRefs, activeIndex])
return (
<OptionsSelector>
<ActivePill style={{ ...style }} activePillColor={activePillColor} />
{options.map((option, i) => {
const { value, display } = getPillMultiToggleOption(option)
const ref = buttonRefs[i]
return (
<OptionButton
ref={ref}
key={value}
active={currentSelected === value}
activeTextColor={activeTextColor}
onClick={() => {
setActiveIndex(i)
onSelectOption(value)
}}
>
{display ?? value}
</OptionButton>
)
})}
</OptionsSelector>
)
}
import { refitChartContentAtom } from 'components/Charts/ChartModel'
import { ChartSkeleton } from 'components/Charts/LoadingState'
import { PriceChart, PriceChartData } from 'components/Charts/PriceChart'
import { LineChart, StackedLineData } from 'components/Charts/StackedLineChart'
import { refitChartContentAtom } from 'components/Charts/TimeSelector'
import { VolumeChart } from 'components/Charts/VolumeChart'
import { SingleHistogramData } from 'components/Charts/VolumeChart/renderer'
import { ChartType, PriceChartType } from 'components/Charts/utils'
import PillMultiToggle, { PillMultiToggleOption } from 'components/Toggle/PillMultiToggle'
import { AdvancedPriceChartToggle } from 'components/Tokens/TokenDetails/ChartSection/AdvancedPriceChartToggle'
import { ChartTypeDropdown } from 'components/Tokens/TokenDetails/ChartSection/ChartTypeSelector'
import {
......@@ -19,12 +18,13 @@ import {
ORDERED_TIMES,
TimePeriodDisplay,
getTimePeriodFromDisplay,
} from 'components/Tokens/TokenTable/TimeSelector'
} from 'components/Tokens/TokenTable/VolumeTimeFrameSelector'
import { TimePeriod, toHistoryDuration } from 'graphql/data/util'
import { useScreenSize } from 'hooks/screenSize'
import { useAtomValue } from 'jotai/utils'
import { useTDPContext } from 'pages/TokenDetails/TDPContext'
import { useMemo, useState } from 'react'
import { Flex, styled } from 'ui/src'
import { Flex, SegmentedControl, SegmentedControlOption, styled } from 'ui/src'
import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { Trans } from 'uniswap/src/i18n'
......@@ -34,7 +34,7 @@ type TokenDetailsChartType = (typeof TDP_CHART_SELECTOR_OPTIONS)[number]
export const DEFAULT_PILL_TIME_SELECTOR_OPTIONS = ORDERED_TIMES.map((time: TimePeriod) => ({
value: DISPLAYS[time],
})) as PillMultiToggleOption[]
})) as SegmentedControlOption[]
export const ChartActionsContainer = styled(Flex, {
flexDirection: 'row-reverse',
......@@ -158,6 +158,7 @@ function ChartControls() {
disableCandlestickUI,
} = useTDPContext().chartState
const refitChartContent = useAtomValue(refitChartContentAtom)
const isMediumScreen = !useScreenSize()['md']
return (
<ChartActionsContainer>
......@@ -191,9 +192,10 @@ function ChartControls() {
/>
</Flex>
<Flex $md={{ width: '100%' }}>
<PillMultiToggle
<SegmentedControl
fullWidth={isMediumScreen}
options={DEFAULT_PILL_TIME_SELECTOR_OPTIONS}
currentSelected={DISPLAYS[timePeriod]}
selectedOption={DISPLAYS[timePeriod]}
onSelectOption={(option) => {
const time = getTimePeriodFromDisplay(option as TimePeriodDisplay)
if (time === timePeriod) {
......
......@@ -383,7 +383,21 @@ exports[`TDPPoolTable renders data filled state 1`] = `
exports[`TDPPoolTable renders error state 1`] = `
<DocumentFragment>
.c6 {
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c6 {
color: #222222;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
......@@ -399,20 +413,6 @@ exports[`TDPPoolTable renders error state 1`] = `
letter-spacing: -0.01em;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
......
......@@ -58,7 +58,7 @@ const StyledDropdown = {
} satisfies FlexProps
// TODO: change this to reflect data pipeline
export default function TimeSelector() {
export default function VolumeTimeFrameSelector() {
const { t } = useTranslation()
const theme = useTheme()
const [isMenuOpen, toggleMenu] = useState(false)
......
......@@ -57,11 +57,11 @@ export function usePollQueryWhileMounted<T, K extends OperationVariables>(
}
export enum TimePeriod {
HOUR,
DAY,
WEEK,
MONTH,
YEAR,
HOUR = 'H',
DAY = 'D',
WEEK = 'W',
MONTH = 'M',
YEAR = 'Y',
}
export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
......
......@@ -2,7 +2,21 @@
exports[`NftCard renders correctly 1`] = `
<DocumentFragment>
.c2 {
.c9 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
box-sizing: border-box;
margin: 0;
min-width: 0;
......@@ -33,20 +47,6 @@ exports[`NftCard renders correctly 1`] = `
letter-spacing: -0.01em;
}
.c9 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c1 {
position: relative;
pointer-events: auto;
......
import { ChartHeader } from 'components/Charts/ChartHeader'
import { Chart } from 'components/Charts/ChartModel'
import { Chart, refitChartContentAtom } from 'components/Charts/ChartModel'
import { ChartSkeleton } from 'components/Charts/LoadingState'
import { TVLChartModel } from 'components/Charts/StackedLineChart'
import TimePeriodSelector from 'components/Charts/TimeSelector'
import { formatHistoryDuration } from 'components/Charts/VolumeChart'
import { CustomVolumeChartModel } from 'components/Charts/VolumeChart/CustomVolumeChartModel'
import { StackedHistogramData } from 'components/Charts/VolumeChart/renderer'
......@@ -14,6 +13,7 @@ import { SupportedInterfaceChainId, chainIdToBackendChain, useChainFromUrlParam
import { useDailyProtocolTVL, useHistoricalProtocolVolume } from 'graphql/data/protocolStats'
import { TimePeriod, getProtocolColor, getProtocolGradient, getSupportedGraphQlChain } from 'graphql/data/util'
import { useScreenSize } from 'hooks/screenSize'
import { useAtomValue } from 'jotai/utils'
import { useTheme } from 'lib/styled-components'
import { ReactNode, useMemo, useState } from 'react'
import {
......@@ -21,7 +21,7 @@ import {
useHistoricalProtocolVolume as useRestHistoricalProtocolVolume,
} from 'state/explore/protocolStats'
import { EllipsisTamaguiStyle } from 'theme/components'
import { Flex, Text, styled } from 'ui/src'
import { Flex, SegmentedControl, Text, styled } from 'ui/src'
import { HistoryDuration, PriceSource } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag, useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks'
......@@ -31,11 +31,7 @@ import { NumberType, useFormatter } from 'utils/formatNumbers'
const EXPLORE_CHART_HEIGHT_PX = 368
const EXPLORE_PRICE_SOURCES = [PriceSource.SubgraphV2, PriceSource.SubgraphV3]
const TIME_SELECTOR_OPTIONS = [
{ time: TimePeriod.DAY, display: 'D' },
{ time: TimePeriod.WEEK, display: 'W' },
{ time: TimePeriod.MONTH, display: 'M' },
]
const TIME_SELECTOR_OPTIONS = [{ value: TimePeriod.DAY }, { value: TimePeriod.WEEK }, { value: TimePeriod.MONTH }]
const ChartsContainer = styled(Flex, {
row: true,
......@@ -81,6 +77,7 @@ function VolumeChartSection({ chainId }: { chainId: SupportedInterfaceChainId })
FeatureFlags.MultichainExplore,
)
const isMultichainExploreEnabled = isMultichainExploreEnabledLoaded || isMultichainExploreLoading
const refitChartContent = useAtomValue(refitChartContentAtom)
function timeGranularityToHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
// note: timePeriod on the Explore Page represents the GRANULARITY, not the timespan of data shown.
......@@ -152,10 +149,17 @@ function VolumeChartSection({ chainId }: { chainId: SupportedInterfaceChainId })
<SectionTitle>
<Trans i18nKey="explore.uniVolume" />
</SectionTitle>
<TimePeriodSelector
<SegmentedControl
options={TIME_SELECTOR_OPTIONS}
timePeriod={timePeriod}
onChangeTimePeriod={setTimePeriod}
selectedOption={timePeriod}
onSelectOption={(option) => {
if (option === timePeriod) {
refitChartContent?.()
} else {
setTimePeriod(option)
}
}}
size="small"
/>
</Flex>
{(() => {
......
......@@ -3,7 +3,7 @@ import { TopPoolTable } from 'components/Pools/PoolTable/PoolTable'
import { TopTokensTable } from 'components/Tokens/TokenTable'
import TableNetworkFilter from 'components/Tokens/TokenTable/NetworkFilter'
import SearchBar from 'components/Tokens/TokenTable/SearchBar'
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
import VolumeTimeFrameSelector from 'components/Tokens/TokenTable/VolumeTimeFrameSelector'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
import { useChainFromUrlParam } from 'constants/chains'
import { manualChainOutageAtom } from 'featureFlags/flags/outageBanner'
......@@ -185,7 +185,7 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => {
</Flex>
<Flex row gap="$spacing8" height="$spacing40" justifyContent="flex-start">
<TableNetworkFilter />
{currentKey === ExploreTab.Tokens && <TimeSelector />}
{currentKey === ExploreTab.Tokens && <VolumeTimeFrameSelector />}
{currentKey !== ExploreTab.Transactions && <SearchBar tab={currentKey} />}
</Flex>
</Flex>
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SendCurrencyInputform renders input in fiat correctly 1`] = `
.c23 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c23 img {
width: 18px;
height: 36px;
object-fit: cover;
}
.c23 img:first-child {
border-radius: 18px 0 0 18px;
object-position: 0 0;
}
.c23 img:last-child {
border-radius: 0 18px 18px 0;
object-position: 100% 0;
}
.c24 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c22 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c4 {
box-sizing: border-box;
margin: 0;
......@@ -108,53 +155,6 @@ exports[`SendCurrencyInputform renders input in fiat correctly 1`] = `
letter-spacing: -0.01em;
}
.c23 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c23 img {
width: 18px;
height: 36px;
object-fit: cover;
}
.c23 img:first-child {
border-radius: 18px 0 0 18px;
object-position: 0 0;
}
.c23 img:last-child {
border-radius: 0 18px 18px 0;
object-position: 100% 0;
}
.c24 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c22 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c0 {
display: -webkit-box;
display: -webkit-flex;
......@@ -565,6 +565,53 @@ exports[`SendCurrencyInputform renders input in fiat correctly 1`] = `
`;
exports[`SendCurrencyInputform renders input in token amount correctly 1`] = `
.c22 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c22 img {
width: 18px;
height: 36px;
object-fit: cover;
}
.c22 img:first-child {
border-radius: 18px 0 0 18px;
object-position: 0 0;
}
.c22 img:last-child {
border-radius: 0 18px 18px 0;
object-position: 100% 0;
}
.c23 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c21 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c4 {
box-sizing: border-box;
margin: 0;
......@@ -672,53 +719,6 @@ exports[`SendCurrencyInputform renders input in token amount correctly 1`] = `
letter-spacing: -0.01em;
}
.c22 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c22 img {
width: 18px;
height: 36px;
object-fit: cover;
}
.c22 img:first-child {
border-radius: 18px 0 0 18px;
object-position: 0 0;
}
.c22 img:last-child {
border-radius: 0 18px 18px 0;
object-position: 100% 0;
}
.c23 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c21 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c0 {
display: -webkit-box;
display: -webkit-flex;
......@@ -1113,6 +1113,53 @@ exports[`SendCurrencyInputform renders input in token amount correctly 1`] = `
`;
exports[`SendCurrencyInputform should render placeholder values 1`] = `
.c23 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c23 img {
width: 18px;
height: 36px;
object-fit: cover;
}
.c23 img:first-child {
border-radius: 18px 0 0 18px;
object-position: 0 0;
}
.c23 img:last-child {
border-radius: 0 18px 18px 0;
object-position: 100% 0;
}
.c24 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c22 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c4 {
box-sizing: border-box;
margin: 0;
......@@ -1220,53 +1267,6 @@ exports[`SendCurrencyInputform should render placeholder values 1`] = `
letter-spacing: -0.01em;
}
.c23 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c23 img {
width: 18px;
height: 36px;
object-fit: cover;
}
.c23 img:first-child {
border-radius: 18px 0 0 18px;
object-position: 0 0;
}
.c23 img:last-child {
border-radius: 0 18px 18px 0;
object-position: 100% 0;
}
.c24 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c22 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c0 {
display: -webkit-box;
display: -webkit-flex;
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SendCurrencyInputform should render input in fiat correctly 1`] = `
.c17 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c17 img {
width: 36px;
height: 36px;
border-radius: 50%;
}
.c18 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c19 {
height: 36px;
width: 36px;
}
.c19 path {
stroke: #CECECE;
background: #7D7D7D;
fill: #CECECE;
}
.c20 {
-webkit-animation: 2s fvtopB linear infinite;
animation: 2s fvtopB linear infinite;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c6 {
box-sizing: border-box;
margin: 0;
......@@ -117,59 +170,6 @@ exports[`SendCurrencyInputform should render input in fiat correctly 1`] = `
letter-spacing: -0.01em;
}
.c17 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c17 img {
width: 36px;
height: 36px;
border-radius: 50%;
}
.c18 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c19 {
height: 36px;
width: 36px;
}
.c19 path {
stroke: #CECECE;
background: #7D7D7D;
fill: #CECECE;
}
.c20 {
-webkit-animation: 2s fvtopB linear infinite;
animation: 2s fvtopB linear infinite;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c8 {
color: #222222;
cursor: pointer;
......@@ -640,6 +640,59 @@ exports[`SendCurrencyInputform should render input in fiat correctly 1`] = `
`;
exports[`SendCurrencyInputform should render input in token amount correctly 1`] = `
.c17 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c17 img {
width: 36px;
height: 36px;
border-radius: 50%;
}
.c18 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c19 {
height: 36px;
width: 36px;
}
.c19 path {
stroke: #CECECE;
background: #7D7D7D;
fill: #CECECE;
}
.c20 {
-webkit-animation: 2s fvtopB linear infinite;
animation: 2s fvtopB linear infinite;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c6 {
box-sizing: border-box;
margin: 0;
......@@ -756,59 +809,6 @@ exports[`SendCurrencyInputform should render input in token amount correctly 1`]
letter-spacing: -0.01em;
}
.c17 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c17 img {
width: 36px;
height: 36px;
border-radius: 50%;
}
.c18 {
width: 18px;
height: 36px;
border-radius: 50%;
}
.c19 {
height: 36px;
width: 36px;
}
.c19 path {
stroke: #CECECE;
background: #7D7D7D;
fill: #CECECE;
}
.c20 {
-webkit-animation: 2s fvtopB linear infinite;
animation: 2s fvtopB linear infinite;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
top: 0;
left: 0;
}
.c8 {
color: #222222;
cursor: pointer;
......
import Row from 'components/Row'
import PillMultiToggle from 'components/Toggle/PillMultiToggle'
import { atom, useAtom } from 'jotai'
import { atomWithStorage, useAtomValue, useUpdateAtom } from 'jotai/utils'
import styled, { useTheme } from 'lib/styled-components'
import ms from 'ms'
import { useCallback, useEffect, useMemo } from 'react'
import { Moon, Sun } from 'react-feather'
import { ThemedText } from 'theme/components/text'
import { Flex, SegmentedControl, Text, styled, useSporeColors } from 'ui/src'
import { Moon as MoonFilled } from 'ui/src/components/icons/Moon'
import { Sun as SunFilled } from 'ui/src/components/icons/Sun'
import { Trans, useTranslation } from 'uniswap/src/i18n'
......@@ -16,24 +13,25 @@ const THEME_UPDATE_DELAY = ms(`0.1s`)
const DARKMODE_MEDIA_QUERY = window.matchMedia('(prefers-color-scheme: dark)')
export enum ThemeMode {
LIGHT = 0,
DARK,
AUTO,
LIGHT = 'Light',
DARK = 'Dark',
AUTO = 'Auto',
}
const OptionPill = styled.div`
padding: 6px 10px;
display: flex;
justify-content: center;
align-items: center;
`
const CompactOptionPill = styled.div`
height: 28px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 8px;
`
const OptionPill = styled(Flex, {
py: '$padding6',
px: '$padding10',
row: true,
justifyContent: 'center',
alignItems: 'center',
})
const CompactOptionPill = styled(Flex, {
px: '$padding8',
height: '$spacing28',
justifyContent: 'center',
alignItems: 'center',
})
// Tracks the device theme
const systemThemeAtom = atom<ThemeMode.LIGHT | ThemeMode.DARK>(
......@@ -97,13 +95,9 @@ export function useDarkModeManager(): [boolean, (mode: ThemeMode) => void] {
}, [isDarkMode, setMode])
}
const ThemePillMultiToggleContainer = styled.div`
width: fit;
`
export function ThemeSelector({ disabled, compact = false }: { disabled?: boolean; compact?: boolean }) {
const theme = useTheme()
const { t } = useTranslation()
const colors = useSporeColors()
const [mode, setMode] = useAtom(themeModeAtom)
const switchMode = useCallback(
(mode: string | number) => {
......@@ -117,14 +111,16 @@ export function ThemeSelector({ disabled, compact = false }: { disabled?: boolea
{
value: ThemeMode.AUTO,
display: (
<CompactOptionPill data-testid="theme-auto">{t('settings.setting.appearance.option.auto')}</CompactOptionPill>
<CompactOptionPill data-testid="theme-auto">
<Text variant="buttonLabel3">{t('settings.setting.appearance.option.auto')}</Text>
</CompactOptionPill>
),
},
{
value: ThemeMode.LIGHT,
display: (
<CompactOptionPill data-testid="theme-light">
<SunFilled size="$icon.20" />
<SunFilled size="$icon.20" color={colors.neutral1.get()} />
</CompactOptionPill>
),
},
......@@ -132,7 +128,7 @@ export function ThemeSelector({ disabled, compact = false }: { disabled?: boolea
value: ThemeMode.DARK,
display: (
<CompactOptionPill data-testid="theme-dark">
<MoonFilled size="$icon.20" />
<MoonFilled size="$icon.20" color={colors.neutral1.get()} />
</CompactOptionPill>
),
},
......@@ -141,13 +137,17 @@ export function ThemeSelector({ disabled, compact = false }: { disabled?: boolea
const defaultOptions = [
{
value: ThemeMode.AUTO,
display: <OptionPill data-testid="theme-auto">{t('settings.setting.appearance.option.auto')}</OptionPill>,
display: (
<OptionPill data-testid="theme-auto">
<Text variant="buttonLabel3">{t('settings.setting.appearance.option.auto')}</Text>
</OptionPill>
),
},
{
value: ThemeMode.LIGHT,
display: (
<OptionPill data-testid="theme-light">
<Sun size="20" />
<Sun size="20" color={colors.neutral1.val} />
</OptionPill>
),
},
......@@ -155,34 +155,34 @@ export function ThemeSelector({ disabled, compact = false }: { disabled?: boolea
value: ThemeMode.DARK,
display: (
<OptionPill data-testid="theme-dark">
<Moon size="20" />
<Moon size="20" color={colors.neutral1.val} />
</OptionPill>
),
},
]
return (
<ThemePillMultiToggleContainer>
<PillMultiToggle
<Flex width="fit">
<SegmentedControl
key={mode} // force re-render when mode changes to avoid visual glitch
options={compact ? compactOptions : defaultOptions}
currentSelected={mode}
selectedOption={mode}
onSelectOption={switchMode}
activePillColor={theme.accent2}
activeTextColor={theme.accent1}
size="large"
/>
</ThemePillMultiToggleContainer>
</Flex>
)
}
export default function ThemeToggle({ disabled }: { disabled?: boolean }) {
return (
<Row align="center" justify="space-between">
<Row width="40%">
<ThemedText.SubHeaderSmall color="primary">
<Flex row alignItems="center" justifyContent="space-between">
<Flex row width="40%">
<Text variant="body3" color="$neutral1">
<Trans i18nKey="themeToggle.theme" />
</ThemedText.SubHeaderSmall>
</Row>
</Text>
</Flex>
<ThemeSelector disabled={disabled} />
</Row>
</Flex>
)
}
import { cloneElement, useState } from 'react'
import { AnimatePresence, ColorTokens, TabLayout, Tabs, TabsTabProps, styled } from 'tamagui'
import { Flex } from 'ui/src/components/layout/Flex'
import { Text } from 'ui/src/components/text/Text'
import { assert } from 'utilities/src/errors'
import { isMobileApp } from 'utilities/src/platform'
const TOGGLE_PADDING = 4
const OptionsSelector = styled(Tabs.List, {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderColor: '$surface3',
outlineWidth: 0,
borderWidth: '$spacing1',
gap: '$gap8',
overflow: 'hidden',
p: TOGGLE_PADDING,
variants: {
fullWidth: {
true: {
width: '100%',
},
},
size: {
small: {
height: 30,
gap: '$spacing6',
borderRadius: '$rounded16',
},
default: {
height: 34,
gap: '$gap8',
borderRadius: '$rounded20',
},
large: {
height: 42,
gap: '$gap12',
borderRadius: '$rounded24',
},
},
} as const,
})
const TabsRovingIndicator = styled(Flex, {
animation: 'fast',
backgroundColor: '$surface3',
borderRadius: '$roundedFull',
position: 'absolute',
cursor: 'pointer',
zIndex: '$mask',
enterStyle: {
opacity: 0,
},
exitStyle: {
opacity: 0,
},
hoverStyle: {
backgroundColor: '$surface3Hovered',
},
variants: {
disabled: {
true: {
backgroundColor: '$surface2',
},
false: {
backgroundColor: '$surface3',
},
},
} as const,
})
const OptionButton = styled(Tabs.Tab, {
unstyled: true,
role: 'button',
tabIndex: 0,
disableActiveTheme: true,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
borderRadius: '$roundedFull',
cursor: 'pointer',
animation: 'fast',
px: '$spacing8',
pressStyle: {
backgroundColor: '$surface3',
},
variants: {
fullWidth: {
true: {
flex: 1,
},
},
active: {
true: {
focusStyle: {
outlineColor: '$surface1',
},
},
false: {
focusStyle: {
outlineColor: '$surface3',
},
},
},
size: {
small: {
height: '$spacing20',
py: '$spacing2',
px: '$padding6',
},
default: {
height: '$spacing24',
py: '$spacing2',
px: '$padding8',
},
large: {
height: '$spacing32',
py: '$padding8',
px: '$padding12',
},
},
disabled: {
true: {
cursor: 'unset',
},
false: {
cursor: 'pointer',
},
},
} as const,
})
export interface SegmentedControlOption<T extends string = string> {
// String value to be selected/stored, used as default display value
value: T
// Optional custom display element
display?: JSX.Element
// Optional wrapper around the display element
wrapper?: JSX.Element
}
type SegmentedControlSize = 'small' | 'default' | 'large'
interface SegmentedControlProps<T extends string = string> {
options: readonly SegmentedControlOption<T>[]
selectedOption: T
onSelectOption: (option: T) => void
size?: SegmentedControlSize
disabled?: boolean
fullWidth?: boolean
}
/**
* Spore segmented control component, for selecting between multiple options.
*
* @param options - An array of options to display in the segmented control - must have between 2 and 6 options.
*
* Note: options can be just text (i.e. their value), or a value with a custom display element.
* If you are defining custom display elements, you must ensure that each option fits within the vertical bounds of the SegmentedControl.
*
* For reference, the heights of the container are as follows (each with top and bottom padding of 4px):
* - small: 30px
* - default: 34px
* - large: 42px
*
* @param selectedOption - The value of the currently selected option.
* @param onSelectOption - Callback function to be called when an option is selected.
* @param size - The size of the segmented control which affects the height and padding.
* @param disabled - Whether the segmented control is disabled.
*/
export function SegmentedControl<T extends string = string>({
options,
selectedOption,
onSelectOption,
size = 'default',
disabled,
fullWidth,
}: SegmentedControlProps<T>): JSX.Element {
assert(options.length >= 2 && options.length <= 6, 'Segmented control must have between 2 and 6 options, inclusive.')
const [tabState, setTabState] = useState<{
/**
* Layout of the Tab user selected
*/
activeAt: TabLayout | null
}>({
activeAt: null,
})
const [hoveredIndex, setHoveredIndex] = useState<number>()
const setActiveIndicator = (activeAt: TabLayout | null): void => setTabState({ ...tabState, activeAt })
const { activeAt } = tabState
const handleOnInteraction: TabsTabProps['onInteraction'] = (type, layout) => {
if (type === 'select') {
setActiveIndicator(layout)
}
}
const activeIndicatorXAdjustment = isMobileApp ? 2 : 0
const activeIndicatorYAdjustment = isMobileApp ? -1 : 0
return (
<Tabs
unstyled
activationMode="manual"
orientation="horizontal"
value={selectedOption}
onValueChange={(option) => {
onSelectOption(option as T)
}}
>
<OptionsSelector
disablePassBorderRadius
unstyled
backgroundColor="transparent"
fullWidth={fullWidth}
loop={false}
size={size}
>
{options.map((option, index) => {
const { value, display, wrapper } = option
const optionButton = (
<OptionButton
key={value}
active={selectedOption === value}
disabled={disabled}
fullWidth={fullWidth}
size={size}
value={value}
onInteraction={handleOnInteraction}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(undefined)}
onPress={() => {
onSelectOption(value)
}}
>
{display ?? (
<Text
color={getOptionTextColor(selectedOption === value, hoveredIndex === index, disabled)}
userSelect="none"
variant={size === 'large' ? 'buttonLabel3' : 'buttonLabel4'}
>
{value}
</Text>
)}
</OptionButton>
)
if (wrapper) {
// To avoid perf issues, we expect the callsite to pass an instance of a component,
// not a functional component. As a result we can't render it with typical JSX and need
// to clone it here.
return cloneElement(wrapper, {
children: optionButton,
})
}
return optionButton
})}
<AnimatePresence>
{activeAt && (
<TabsRovingIndicator
height={activeAt.height}
width={activeAt.width}
x={activeAt.x - TOGGLE_PADDING + activeIndicatorXAdjustment}
y={activeAt.y - TOGGLE_PADDING + activeIndicatorYAdjustment}
/>
)}
</AnimatePresence>
</OptionsSelector>
</Tabs>
)
}
function getOptionTextColor(active: boolean, hovered: boolean, disabled = false): ColorTokens {
if (disabled) {
return active ? '$neutral2' : '$neutral3'
}
if (active || hovered) {
return '$neutral1'
}
return '$neutral2'
}
......@@ -50,6 +50,7 @@ export type {
export { LinearGradient } from 'tamagui/linear-gradient'
export * from 'ui/src/animations'
export { QRCodeDisplay } from './components/QRCode/QRCodeDisplay'
export * from './components/SegmentedControl/SegmentedControl'
export { Unicon } from './components/Unicon'
export * from './components/Unicon/utils'
export * from './components/UniversalImage/UniversalImage'
......
......@@ -10,7 +10,7 @@ import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
interface TokenSelectorBalanceDisplayProps {
onPress: () => void
selectedCurrencyInfo: CurrencyInfo
formattedAmount: string
formattedAmount?: string
disabled?: boolean
loading?: boolean
chevronDirection?: ComponentProps<typeof RotatableChevron>['direction']
......@@ -20,7 +20,7 @@ interface TokenSelectorBalanceDisplayProps {
export function TokenSelectorBalanceDisplay({
selectedCurrencyInfo,
onPress,
formattedAmount,
formattedAmount = '-',
disabled,
loading,
chevronDirection = 'end',
......
......@@ -251,7 +251,7 @@ export const TopAndBottomGradient = (): JSX.Element => {
const SCREEN_WIDTH_BUFFER = 50
// Used for initial layout larger than all screen sizes
const MAX_DEVICE_WIDTH = isWeb ? undefined : 1000
const MAX_DEVICE_WIDTH = 1000
type AnimatedNumberProps = {
loadingPlaceholderText: string
......
......@@ -104,14 +104,13 @@ function GasRow({ gasInfo }: { gasInfo: DebouncedGasInfo }): JSX.Element | null
<Flex centered row animation="quick" enterStyle={{ opacity: 0 }} opacity={gasInfo.isLoading ? 0.6 : 1}>
<NetworkFeeWarning
gasFeeHighRelativeToValue={gasInfo.isHighRelativeToValue}
placement="bottom"
tooltipTrigger={
<Flex centered row gap="$spacing4">
{body}
</Flex>
}
tooltipTrigger={null}
uniswapXGasFeeInfo={gasInfo.uniswapXGasFeeInfo}
/>
>
<Flex centered row gap="$spacing4">
{body}
</Flex>
</NetworkFeeWarning>
</Flex>
)
} else {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment