diff --git a/packages/kit/src/components/TradingView/TradingViewV2/components/calendarControls/CalendarPanelPopover.tsx b/packages/kit/src/components/TradingView/TradingViewV2/components/calendarControls/CalendarPanelPopover.tsx index 7fb882352b9f..70442011ff1e 100644 --- a/packages/kit/src/components/TradingView/TradingViewV2/components/calendarControls/CalendarPanelPopover.tsx +++ b/packages/kit/src/components/TradingView/TradingViewV2/components/calendarControls/CalendarPanelPopover.tsx @@ -649,7 +649,7 @@ export function CalendarPanelPopover({ onOpenChange={handleOpenChange} showHeader={false} usingSheet={false} - placement="bottom-end" + placement="bottom-start" floatingPanelProps={{ width: 328, }} diff --git a/packages/kit/src/components/TradingView/TradingViewV2/components/chartType/ChartTypeSelect.tsx b/packages/kit/src/components/TradingView/TradingViewV2/components/chartType/ChartTypeSelect.tsx index 1628e72234a8..dcaecca4fc13 100644 --- a/packages/kit/src/components/TradingView/TradingViewV2/components/chartType/ChartTypeSelect.tsx +++ b/packages/kit/src/components/TradingView/TradingViewV2/components/chartType/ChartTypeSelect.tsx @@ -56,7 +56,7 @@ export function ChartTypeSelect({ onChartTypeChange(chartType); } }} - placement="bottom-end" + placement="bottom-start" floatingPanelProps={{ width: '$56', }} diff --git a/packages/kit/src/components/TradingView/TradingViewV2/components/indicatorControls/NativeIndicatorControls.tsx b/packages/kit/src/components/TradingView/TradingViewV2/components/indicatorControls/NativeIndicatorControls.tsx index b3c540aa37f8..1978658eef60 100644 --- a/packages/kit/src/components/TradingView/TradingViewV2/components/indicatorControls/NativeIndicatorControls.tsx +++ b/packages/kit/src/components/TradingView/TradingViewV2/components/indicatorControls/NativeIndicatorControls.tsx @@ -16,7 +16,12 @@ import { import { ETranslations } from '@onekeyhq/shared/src/locale'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; -import { HEADER_ICON_BUTTON_STYLE_PROPS } from '../utils/NativeChartControlsShared'; +import { + HEADER_ICON_BUTTON_STYLE_PROPS, + NATIVE_CHART_OPTION_GRID_GAP, + NATIVE_CHART_OPTION_PILL_LAYOUT_PROPS, + getNativeChartOptionPillColors, +} from '../utils/NativeChartControlsShared'; import { canToggleTradingViewNativeIndicatorOn, @@ -33,14 +38,6 @@ import type { import type { GestureResponderEvent } from 'react-native'; const INDICATOR_GRID_COLUMN_COUNT = 4; -const INDICATOR_GRID_ITEM_LAYOUT_PROPS = { - flex: 1, - flexBasis: 0, - h: 32, - minWidth: 0, - px: '$2', - borderWidth: 1, -} as const; export const TRADING_VIEW_NATIVE_INDICATOR_QUICK_BAR_HEIGHT = 31; const INDICATOR_QUICK_BAR_VERTICAL_PAN_THRESHOLD = 4; @@ -87,17 +84,19 @@ function IndicatorPill({ isDisabled: boolean; onPress?: () => void; }) { + const { color: textColor, ...pillColors } = getNativeChartOptionPillColors({ + isHighlighted: isActive, + isDisabled, + }); + return ( - + {indicator.label} @@ -146,11 +139,14 @@ function IndicatorGrid({ }, [indicators]); return ( - + {rows.map((row, rowIndex) => { const placeholderCount = INDICATOR_GRID_COLUMN_COUNT - row.length; return ( - + {row.map((indicator) => { const isDisabled = !canToggleTradingViewNativeIndicatorOn({ indicatorValue: indicator.value, @@ -171,8 +167,8 @@ function IndicatorGrid({ {Array.from({ length: placeholderCount }).map((_, index) => ( @@ -547,7 +543,7 @@ function IndicatorListPopoverContent({ ); return ( - + void; }) { - const isHighlighted = isActive || Boolean(isSelected); - let textColor = '$textSubdued'; - if (disabled) { - textColor = '$textDisabled'; - } else if (isHighlighted) { - textColor = '$text'; - } + const { color: textColor, ...pillColors } = getNativeChartOptionPillColors({ + isHighlighted: isActive || Boolean(isSelected), + isDisabled: disabled, + }); return ( - + {displayLabel} {showCheckMark && !disabled ? ( - + {/* Checkmark1Small fills ~45% of its 24px viewBox, so $3 draws the + 5.7px glyph the design specifies. */} + ) : null} @@ -131,11 +128,14 @@ export function IntervalGrid({ }, [options]); return ( - + {rows.map((row, rowIndex) => { const placeholderCount = INTERVAL_GRID_COLUMN_COUNT - row.length; return ( - + {row.map((option) => { const isSelected = selectedValues?.has(option.value) ?? false; const isDisabled = @@ -167,8 +167,8 @@ export function IntervalGrid({ {Array.from({ length: placeholderCount }).map((_, index) => ( @@ -192,7 +192,7 @@ export function IntervalsDialogSection({ return ( - + {title} {action} diff --git a/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalSelector.tsx b/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalSelector.tsx index 03303b9375f4..32c281687fac 100644 --- a/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalSelector.tsx +++ b/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalSelector.tsx @@ -127,6 +127,9 @@ export const TradingViewNativeIntervalSelector = memo( title: intl.formatMessage({ id: ETranslations.market_intervals }), showFooter: false, testID: 'trading-view-native-intervals-dialog', + // IntervalsDialogContent owns its own padding so the popover and the + // dialog share one layout; drop the default Dialog content padding. + contentContainerProps: { px: '$0', pb: '$0' }, onClose: () => { handleIntervalsDialogClose(dialogInstance); }, @@ -174,6 +177,7 @@ export const TradingViewNativeIntervalSelector = memo( onIntervalChange={onIntervalChange} onPreferredValuesChange={handlePreferredValuesChange} onClose={closeIntervalsPopover} + mode={intervalControlMode} maxPreferredIntervalCount={ intervalControlMode === 'popover' ? null @@ -202,6 +206,9 @@ export const TradingViewNativeIntervalSelector = memo( onOpenChange={handleIntervalsPopoverOpenChange} floatingPanelProps={{ width: 360, + // Without this the focus scope moves focus to the Edit button on + // open and it renders with a focus ring the user never asked for. + onOpenAutoFocus: (event) => event.preventDefault(), }} renderTrigger={ ) : null} diff --git a/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalUtils.ts b/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalUtils.ts index 981bd509f1ea..5c2dba0744f5 100644 --- a/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalUtils.ts +++ b/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalUtils.ts @@ -7,14 +7,6 @@ import type { IntlShape } from 'react-intl'; export const MAX_VISIBLE_INTERVAL_COUNT = 4; export const MAX_PREFERRED_INTERVAL_COUNT = 4; export const INTERVAL_GRID_COLUMN_COUNT = 4; -export const INTERVAL_GRID_ITEM_LAYOUT_PROPS = { - flex: 1, - flexBasis: 0, - h: 32, - minWidth: 0, - px: '$3', - borderWidth: 1, -} as const; const PREFERRED_INTERVAL_STORAGE_KEY = 'trading_view_native_preferred_intervals_v1'; @@ -152,7 +144,14 @@ export function formatIntervalOptionDisplayLabel( return label; } -export function getAllIntervalOptions(options: ITradingViewIntervalOption[]) { +/** + * Orders the intervals the chart reported by the canonical template order and + * appends anything it reported outside that list. The chart owns the interval + * list, so a resolution it never reported is never surfaced. + */ +export function getOrderedIntervalOptions( + options: ITradingViewIntervalOption[], +) { const optionsByLabel = new Map(); const optionsByValue = new Map(); options.forEach((option) => { @@ -170,27 +169,23 @@ export function getAllIntervalOptions(options: ITradingViewIntervalOption[]) { const seenValues = new Set(); const seenLabels = new Set(); - const allOptions: ITradingViewIntervalOption[] = - ALL_INTERVAL_OPTION_TEMPLATES.map((template) => { - const normalizedLabel = normalizeIntervalLabel(template.label); - const matchedOption = - optionsByLabel.get(normalizedLabel) ?? - optionsByValue.get(template.fallbackValue); - const option = matchedOption - ? { - ...matchedOption, - label: template.label, - } - : { - label: template.label, - value: template.fallbackValue, - disabled: true, - }; - - seenLabels.add(normalizedLabel); - seenValues.add(option.value); - return option; + const allOptions: ITradingViewIntervalOption[] = []; + ALL_INTERVAL_OPTION_TEMPLATES.forEach((template) => { + const normalizedLabel = normalizeIntervalLabel(template.label); + const matchedOption = + optionsByLabel.get(normalizedLabel) ?? + optionsByValue.get(template.fallbackValue); + if (!matchedOption) { + return; + } + + seenLabels.add(normalizedLabel); + seenValues.add(matchedOption.value); + allOptions.push({ + ...matchedOption, + label: template.label, }); + }); options.forEach((option) => { const normalizedLabel = normalizeIntervalLabel(option.label); diff --git a/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalsDialogContent.tsx b/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalsDialogContent.tsx index 528e130e9e63..2dcd390b546d 100644 --- a/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalsDialogContent.tsx +++ b/packages/kit/src/components/TradingView/TradingViewV2/components/intervalSelector/NativeIntervalsDialogContent.tsx @@ -1,19 +1,16 @@ import { useIntl } from 'react-intl'; -import { - Button, - Icon, - SizableText, - XStack, - YStack, -} from '@onekeyhq/components'; +import { Button, SizableText, XStack, YStack } from '@onekeyhq/components'; import type { IButtonProps } from '@onekeyhq/components'; import { ETranslations } from '@onekeyhq/shared/src/locale'; +import { NATIVE_CHART_SECTION_ACTION_BUTTON_PROPS } from '../utils/NativeChartControlsShared'; + import { useNativeIntervalsDialogState } from './hooks/useNativeIntervalsDialogState'; import { IntervalGrid, IntervalsDialogSection } from './NativeIntervalGrid'; import { MAX_PREFERRED_INTERVAL_COUNT } from './NativeIntervalUtils'; +import type { ITradingViewNativeIntervalControlMode } from './hooks/useNativeIntervalSelector'; import type { ITradingViewIntervalOption } from '../../types'; export function IntervalsDialogContent({ @@ -27,6 +24,7 @@ export function IntervalsDialogContent({ onClose, maxPreferredIntervalCount = MAX_PREFERRED_INTERVAL_COUNT, footerButtonSize = 'large', + mode = 'dialog', }: { options: ITradingViewIntervalOption[]; editableOptions: ITradingViewIntervalOption[]; @@ -38,8 +36,16 @@ export function IntervalsDialogContent({ onClose: () => void; maxPreferredIntervalCount?: number | null; footerButtonSize?: IButtonProps['size']; + mode?: ITradingViewNativeIntervalControlMode; }) { const intl = useIntl(); + // The desktop popover is tighter than the mobile dialog: it has no header of + // its own, so it pads evenly and keeps the two groups closer together. + const isPopover = mode === 'popover'; + const contentPadding = isPopover + ? ({ p: '$5' } as const) + : ({ px: '$5', pt: '$2', pb: '$8' } as const); + const sectionGap = isPopover ? '$5' : '$8'; const { draftPreferredValueSet, editTitle, @@ -64,22 +70,32 @@ export function IntervalsDialogContent({ }); if (isEditing) { + // The dialog keeps the 32px footer gap the indicator dialog uses; the + // popover is tighter at 20px. return ( - - - {editTitle} - - - + + {/* Same title/grid rhythm as IntervalsDialogSection on the list page. */} + + + {editTitle} + + + + + ); + return ( - + {preferredOptions.length ? ( - - {intl.formatMessage({ - id: ETranslations.market_edit_preferred_intervals, - })} - - - - } > getAllIntervalOptions(options), + () => getOrderedIntervalOptions(options), [options], ); diff --git a/packages/kit/src/components/TradingView/TradingViewV2/components/utils/NativeChartControlsShared.ts b/packages/kit/src/components/TradingView/TradingViewV2/components/utils/NativeChartControlsShared.ts index 8392a9706858..4f8b7846987d 100644 --- a/packages/kit/src/components/TradingView/TradingViewV2/components/utils/NativeChartControlsShared.ts +++ b/packages/kit/src/components/TradingView/TradingViewV2/components/utils/NativeChartControlsShared.ts @@ -13,6 +13,48 @@ export type ITradingViewNativeIndicatorControlMode = 'dialog' | 'popover'; export type ITradingViewNativePriceMarketCapControlMode = 'settings' | 'select'; export type ITradingViewNativeControlsLayoutMode = 'mobile' | 'desktop'; +/** + * Shared chrome for the option grids in the interval and indicator popovers so + * the two stay visually identical. + */ +export const NATIVE_CHART_OPTION_GRID_GAP = '$2'; +export const NATIVE_CHART_OPTION_PILL_LAYOUT_PROPS = { + flex: 1, + flexBasis: 0, + h: 32, + minWidth: 0, + px: '$2.5', + borderWidth: 1, + borderRadius: '$2', + borderCurve: 'continuous', +} as const; + +export function getNativeChartOptionPillColors({ + isHighlighted, + isDisabled, +}: { + isHighlighted: boolean; + isDisabled?: boolean; +}) { + const showsHighlight = isHighlighted && !isDisabled; + return { + bg: showsHighlight ? ('$bgActive' as const) : ('$bgStrong' as const), + borderColor: showsHighlight + ? ('$borderActive' as const) + : ('$transparent' as const), + color: isDisabled ? ('$textDisabled' as const) : ('$text' as const), + }; +} + +/** Section header action that reads as a link rather than a filled button. */ +export const NATIVE_CHART_SECTION_ACTION_BUTTON_PROPS = { + size: 'small', + variant: 'tertiary', + hoverStyle: { + bg: '$transparent', + }, +} as const; + export const HEADER_ICON_BUTTON_STYLE_PROPS = { m: '$0', bg: '$transparent',