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',