merge: PR #167 监控自定义信号全显 + 信息条弹窗性能优化

冲突解法: SignalPicker 采用 options 形态 (PR 重构) + 保留 main 的
intradayDisabledSignals/intradayDisabledHint 变量 (含自选分组作用域
禁用分时穿越的增强语义, 自动合并会被 PR 旧内联表达式悄悄丢掉)
+ filterCustomByKind: false (监控场景显示全部自定义信号)。
验证: tsc 零错误, pnpm build 通过。
This commit is contained in:
shy3130
2026-09-02 10:56:49 +08:00
5 changed files with 113 additions and 64 deletions
@@ -6,7 +6,7 @@
* - 下半区「内置列」:按业务分组折叠 * - 下半区「内置列」:按业务分组折叠
* - 底部「扩展数据列」:复用 ext_data schema,按需添加字段 * - 底部「扩展数据列」:复用 ext_data schema,按需添加字段
*/ */
import React, { useState, useCallback, useEffect, useMemo } from 'react' import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { import {
DndContext, closestCenter, KeyboardSensor, PointerSensor, DndContext, closestCenter, KeyboardSensor, PointerSensor,
@@ -39,6 +39,13 @@ interface ListColumnCustomizerProps {
showExtColumns?: boolean showExtColumns?: boolean
/** 是否显示「单独显示」勾选项(默认 false;仅信息条场景启用,让某列独占一行)。 */ /** 是否显示「单独显示」勾选项(默认 false;仅信息条场景启用,让某列独占一行)。 */
showStandaloneToggle?: boolean showStandaloneToggle?: boolean
/**
* 是否禁用遮罩背景模糊 (backdrop-blur)。默认 false。
* 信息条场景 (宿主含 Canvas K线/分时图) 传 trueblur 会对 Canvas 做逐帧 GPU 合成,
* 是抽屉打开/滑动卡顿的主因;改纯半透明遮罩后合成层锐减。
* 选股/自选页背后是 DOM 表格,blur 开销可忽略,保持默认。
*/
disableBackdropBlur?: boolean
} }
/** 判断扩展数据字段类型是否为数字(int/float/double/number/decimal 等)。 /** 判断扩展数据字段类型是否为数字(int/float/double/number/decimal 等)。
@@ -54,16 +61,19 @@ function isNumericFieldType(ft?: string): boolean {
return true return true
} }
/** 模块级空数组常量:extSchema 未加载时用,避免 `?? []` 每次创建新引用导致 useMemo 失效。 */
const EMPTY_EXT_TABLES: readonly { id: string; label: string; mode: string; columns: { name: string; label: string; type: string }[] }[] = []
function SortableActiveCol({ col, onRemove, onConfig, configOpen, extTableLabel, extConfig, candleConfig: candlePanel, intradayConfig: intradayPanel, strategiesConfig, showStandaloneToggle, onToggleStandalone }: { function SortableActiveCol({ col, onRemove, onConfig, configOpen, extTableLabel, extConfig, candleConfig: candlePanel, intradayConfig: intradayPanel, strategiesConfig, showStandaloneToggle, onToggleStandalone }: {
col: ColumnConfig col: ColumnConfig
onRemove: (id: string) => void onRemove: (id: string) => void
onConfig: (id: string | null) => void onConfig: (id: string | null) => void
configOpen: boolean configOpen: boolean
extTableLabel: string extTableLabel: string
extConfig: React.ReactNode extConfig: () => React.ReactNode
candleConfig: React.ReactNode candleConfig: () => React.ReactNode
intradayConfig: React.ReactNode intradayConfig: () => React.ReactNode
strategiesConfig: React.ReactNode strategiesConfig: () => React.ReactNode
showStandaloneToggle?: boolean showStandaloneToggle?: boolean
onToggleStandalone?: (id: string) => void onToggleStandalone?: (id: string) => void
}) { }) {
@@ -132,7 +142,7 @@ function SortableActiveCol({ col, onRemove, onConfig, configOpen, extTableLabel,
<EyeOff className="h-3 w-3" /> <EyeOff className="h-3 w-3" />
</button> </button>
</div> </div>
{hasConfig && configOpen && (isExt ? extConfig : isCandle ? candlePanel : isIntraday ? intradayPanel : strategiesConfig)} {hasConfig && configOpen && (isExt ? extConfig() : isCandle ? candlePanel() : isIntraday ? intradayPanel() : strategiesConfig())}
</> </>
) )
} }
@@ -149,6 +159,7 @@ export function ListColumnCustomizer({
extFieldFilter, extFieldFilter,
showExtColumns = true, showExtColumns = true,
showStandaloneToggle = false, showStandaloneToggle = false,
disableBackdropBlur = false,
}: ListColumnCustomizerProps) { }: ListColumnCustomizerProps) {
const extSchema = useQuery({ const extSchema = useQuery({
queryKey: QK.extDataSchemaAll, queryKey: QK.extDataSchemaAll,
@@ -158,6 +169,14 @@ export function ListColumnCustomizer({
}) })
const backdrop = useDialogBackdrop(onClose) const backdrop = useDialogBackdrop(onClose)
// columns/onChange 用 ref 镜像,让下方多数 useCallback 空依赖(addExtColumn 例外,
// 依赖 props.extColumnAlign)。callback 引用稳定后,columns 变更不再导致
// SortableActiveCol 等子组件因 props 变化而无谓重渲染。
const columnsRef = useRef(columns)
columnsRef.current = columns
const onChangeRef = useRef(onChange)
onChangeRef.current = onChange
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set()) const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set())
const [configOpenId, setConfigOpenId] = useState<string | null>(null) const [configOpenId, setConfigOpenId] = useState<string | null>(null)
@@ -175,16 +194,18 @@ export function ListColumnCustomizer({
const activeCols = useMemo(() => columns.filter(c => !c.pinned && c.visible), [columns]) const activeCols = useMemo(() => columns.filter(c => !c.pinned && c.visible), [columns])
const toggleVisible = useCallback((colId: string) => { const toggleVisible = useCallback((colId: string) => {
onChange(columns.map(c => const cols = columnsRef.current
onChangeRef.current(cols.map(c =>
c.id === colId && !c.pinned ? { ...c, visible: !c.visible } : c c.id === colId && !c.pinned ? { ...c, visible: !c.visible } : c
)) ))
}, [columns, onChange]) }, [])
const toggleStandalone = useCallback((colId: string) => { const toggleStandalone = useCallback((colId: string) => {
onChange(columns.map(c => const cols = columnsRef.current
onChangeRef.current(cols.map(c =>
c.id === colId ? { ...c, standalone: !c.standalone } : c c.id === colId ? { ...c, standalone: !c.standalone } : c
)) ))
}, [columns, onChange]) }, [])
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
@@ -194,19 +215,21 @@ export function ListColumnCustomizer({
const handleDragEnd = useCallback((event: DragEndEvent) => { const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event const { active, over } = event
if (!over || active.id === over.id) return if (!over || active.id === over.id) return
const reorderCols = columns.filter(c => !c.pinned) const cols = columnsRef.current
const pinnedCols = columns.filter(c => c.pinned) const reorderCols = cols.filter(c => !c.pinned)
const pinnedCols = cols.filter(c => c.pinned)
const ids = reorderCols.map(c => c.id) const ids = reorderCols.map(c => c.id)
const oldIdx = ids.indexOf(active.id as string) const oldIdx = ids.indexOf(active.id as string)
const newIdx = ids.indexOf(over.id as string) const newIdx = ids.indexOf(over.id as string)
if (oldIdx < 0 || newIdx < 0) return if (oldIdx < 0 || newIdx < 0) return
const reordered = arrayMove(reorderCols, oldIdx, newIdx) const reordered = arrayMove(reorderCols, oldIdx, newIdx)
onChange([...pinnedCols, ...reordered]) onChangeRef.current([...pinnedCols, ...reordered])
}, [columns, onChange]) }, [])
const addExtColumn = useCallback((configId: string, fieldName: string, fieldLabel?: string, fieldType?: string) => { const addExtColumn = useCallback((configId: string, fieldName: string, fieldLabel?: string, fieldType?: string) => {
const cols = columnsRef.current
const colId = `ext:${configId}:${fieldName}` const colId = `ext:${configId}:${fieldName}`
if (columns.some(c => c.id === colId)) { if (cols.some(c => c.id === colId)) {
toggleVisible(colId) toggleVisible(colId)
return return
} }
@@ -217,64 +240,71 @@ export function ListColumnCustomizer({
visible: true, visible: true,
align: extColumnAlign, align: extColumnAlign,
} }
const actionIdx = columns.findIndex(c => c.id === 'builtin:action') const actionIdx = cols.findIndex(c => c.id === 'builtin:action')
if (actionIdx >= 0) { if (actionIdx >= 0) {
const next = [...columns] const next = [...cols]
next.splice(actionIdx, 0, newCol) next.splice(actionIdx, 0, newCol)
onChange(next) onChangeRef.current(next)
} else { } else {
onChange([...columns, newCol]) onChangeRef.current([...cols, newCol])
} }
}, [columns, extColumnAlign, onChange, toggleVisible]) }, [extColumnAlign, toggleVisible])
const hideColumn = useCallback((colId: string) => { const hideColumn = useCallback((colId: string) => {
onChange(columns.map(c => c.id === colId ? { ...c, visible: false } : c)) const cols = columnsRef.current
}, [columns, onChange]) onChangeRef.current(cols.map(c => c.id === colId ? { ...c, visible: false } : c))
}, [])
const updateExtDisplay = useCallback((colId: string, patch: Partial<ExtColumnDisplayConfig>) => { const updateExtDisplay = useCallback((colId: string, patch: Partial<ExtColumnDisplayConfig>) => {
onChange(columns.map(c => { const cols = columnsRef.current
onChangeRef.current(cols.map(c => {
if (c.id !== colId) return c if (c.id !== colId) return c
return { ...c, extDisplay: { displayMode: 'tag', ...(c.extDisplay || {}), ...patch } } return { ...c, extDisplay: { displayMode: 'tag', ...(c.extDisplay || {}), ...patch } }
})) }))
}, [columns, onChange]) }, [])
const resetExtDisplay = useCallback((colId: string) => { const resetExtDisplay = useCallback((colId: string) => {
onChange(columns.map(c => { const cols = columnsRef.current
onChangeRef.current(cols.map(c => {
if (c.id !== colId) return c if (c.id !== colId) return c
const { extDisplay, ...rest } = c const { extDisplay, ...rest } = c
return rest return rest
})) }))
}, [columns, onChange]) }, [])
const updateCandleConfig = useCallback((colId: string, patch: Partial<CandleColumnConfig>) => { const updateCandleConfig = useCallback((colId: string, patch: Partial<CandleColumnConfig>) => {
onChange(columns.map(c => { const cols = columnsRef.current
onChangeRef.current(cols.map(c => {
if (c.id !== colId) return c if (c.id !== colId) return c
return { ...c, candleConfig: { ...c.candleConfig, ...patch } } return { ...c, candleConfig: { ...c.candleConfig, ...patch } }
})) }))
}, [columns, onChange]) }, [])
const resetCandleConfig = useCallback((colId: string) => { const resetCandleConfig = useCallback((colId: string) => {
onChange(columns.map(c => { const cols = columnsRef.current
onChangeRef.current(cols.map(c => {
if (c.id !== colId) return c if (c.id !== colId) return c
const { candleConfig, ...rest } = c const { candleConfig, ...rest } = c
return rest return rest
})) }))
}, [columns, onChange]) }, [])
const updateIntradayConfig = useCallback((colId: string, patch: Partial<IntradayColumnConfig>) => { const updateIntradayConfig = useCallback((colId: string, patch: Partial<IntradayColumnConfig>) => {
onChange(columns.map(c => { const cols = columnsRef.current
onChangeRef.current(cols.map(c => {
if (c.id !== colId) return c if (c.id !== colId) return c
return { ...c, intradayConfig: { ...c.intradayConfig, ...patch } } return { ...c, intradayConfig: { ...c.intradayConfig, ...patch } }
})) }))
}, [columns, onChange]) }, [])
const resetIntradayConfig = useCallback((colId: string) => { const resetIntradayConfig = useCallback((colId: string) => {
onChange(columns.map(c => { const cols = columnsRef.current
onChangeRef.current(cols.map(c => {
if (c.id !== colId) return c if (c.id !== colId) return c
const { intradayConfig, ...rest } = c const { intradayConfig, ...rest } = c
return rest return rest
})) }))
}, [columns, onChange]) }, [])
const toggleGroup = useCallback((groupId: string) => { const toggleGroup = useCallback((groupId: string) => {
setExpandedGroups(prev => { setExpandedGroups(prev => {
@@ -294,8 +324,8 @@ export function ListColumnCustomizer({
}) })
}, []) }, [])
const extTables = extSchema.data?.items ?? [] const extTables = extSchema.data?.items ?? EMPTY_EXT_TABLES
const extTableLabelMap = new Map(extTables.map(t => [t.id, t.label])) const extTableLabelMap = useMemo(() => new Map(extTables.map(t => [t.id, t.label])), [extTables])
const query = searchQuery.trim().toLowerCase() const query = searchQuery.trim().toLowerCase()
const filteredGroups = useMemo(() => { const filteredGroups = useMemo(() => {
@@ -722,7 +752,7 @@ export function ListColumnCustomizer({
<motion.div <motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
className="absolute inset-0 bg-black/50 backdrop-blur-sm" className={`absolute inset-0 bg-black/50 ${disableBackdropBlur ? '' : 'backdrop-blur-sm'}`}
{...backdrop} {...backdrop}
/> />
<motion.div <motion.div
@@ -768,10 +798,10 @@ export function ListColumnCustomizer({
onConfig={setConfigOpenId} onConfig={setConfigOpenId}
configOpen={configOpenId === col.id} configOpen={configOpenId === col.id}
extTableLabel={col.source.type === 'ext' ? (extTableLabelMap.get(col.source.configId) || col.source.configId) : ''} extTableLabel={col.source.type === 'ext' ? (extTableLabelMap.get(col.source.configId) || col.source.configId) : ''}
extConfig={renderExtConfig(col)} extConfig={() => renderExtConfig(col)}
candleConfig={renderCandleConfig(col)} candleConfig={() => renderCandleConfig(col)}
intradayConfig={renderIntradayConfig(col)} intradayConfig={() => renderIntradayConfig(col)}
strategiesConfig={renderStrategiesConfig(col)} strategiesConfig={() => renderStrategiesConfig(col)}
showStandaloneToggle={showStandaloneToggle} showStandaloneToggle={showStandaloneToggle}
onToggleStandalone={toggleStandalone} onToggleStandalone={toggleStandalone}
/> />
+1
View File
@@ -300,6 +300,7 @@ export function StockInfoBar({
builtinSectionLabel="可选指标" builtinSectionLabel="可选指标"
extColumnAlign="left" extColumnAlign="left"
showStandaloneToggle showStandaloneToggle
disableBackdropBlur
/> />
</div> </div>
) )
+12 -6
View File
@@ -479,9 +479,12 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
signals={selectedSignals} signals={selectedSignals}
onChange={onSignalPickerChange} onChange={onSignalPickerChange}
kind="entry" kind="entry"
builtinSignals={pickerSignals} options={{
disabledSignals={intradaySupport?.available === false ? MONITOR_INTRADAY_SIGNAL_OPTIONS : []} builtinSignals: pickerSignals,
disabledSignalHint={intradaySupport?.reason} disabledSignals: intradayDisabledSignals,
disabledSignalHint: intradayDisabledHint,
filterCustomByKind: false,
}}
/> />
{hasIntradaySignal && ( {hasIntradaySignal && (
<div className={`mt-2 text-[10px] ${intradaySupport?.available === false ? 'text-danger' : 'text-muted'}`}> <div className={`mt-2 text-[10px] ${intradaySupport?.available === false ? 'text-danger' : 'text-muted'}`}>
@@ -1271,9 +1274,12 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
signals={selectedSignals} signals={selectedSignals}
onChange={onSignalPickerChange} onChange={onSignalPickerChange}
kind="entry" kind="entry"
builtinSignals={pickerSignals} options={{
disabledSignals={intradayDisabledSignals} builtinSignals: pickerSignals,
disabledSignalHint={intradayDisabledHint} disabledSignals: intradayDisabledSignals,
disabledSignalHint: intradayDisabledHint,
filterCustomByKind: false,
}}
/> />
{hasIntradaySignal && ( {hasIntradaySignal && (
<div className={`mt-2 text-[10px] ${intradaySupport?.available === false ? 'text-danger' : 'text-muted'}`}> <div className={`mt-2 text-[10px] ${intradaySupport?.available === false ? 'text-danger' : 'text-muted'}`}>
@@ -1,9 +1,25 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { PenLine } from 'lucide-react'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { QK } from '@/lib/queryKeys' import { QK } from '@/lib/queryKeys'
import { SIGNAL_OPTIONS, cnSignal } from '@/lib/signals' import { SIGNAL_OPTIONS, cnSignal } from '@/lib/signals'
/** 展示与过滤可选项 (全部可选, 有默认值) */
interface SignalPickerOptions {
/** 渲染尺寸: dialog = 选股弹窗紧凑样式; panel = 回测页设置抽屉样式 */
variant?: 'dialog' | 'panel'
builtinSignals?: { key: string; label: string }[]
disabledSignals?: string[]
disabledSignalHint?: string
/**
* 是否按 kind 过滤自定义信号 (csg_*)。默认 true (选股/回测: 入场区只显示 entry)。
* 监控规则页设 false: 报警语义是"命中即报", 不分入场出场, 自定义信号全部显示
* (与内置信号一致: builtinSignals 透传时本就不按 kind 过滤)。
*/
filterCustomByKind?: boolean
}
interface Props { interface Props {
/** 当前选中的信号 ID 列表 */ /** 当前选中的信号 ID 列表 */
signals: string[] signals: string[]
@@ -11,30 +27,27 @@ interface Props {
onChange: (next: string[]) => void onChange: (next: string[]) => void
/** 买点 / 卖点 — 决定自定义信号的过滤与配色主题 */ /** 买点 / 卖点 — 决定自定义信号的过滤与配色主题 */
kind: 'entry' | 'exit' kind: 'entry' | 'exit'
/** 渲染尺寸: dialog = 选股弹窗紧凑样式; panel = 回测页设置抽屉样式 */ options?: SignalPickerOptions
variant?: 'dialog' | 'panel'
builtinSignals?: { key: string; label: string }[]
disabledSignals?: string[]
disabledSignalHint?: string
} }
/** /**
* 买卖触发器信号选择 — 选股页弹窗 / 回测页共用。 * 买卖触发器信号选择 — 选股页弹窗 / 回测页共用。
* *
* - 内置信号 (signal_*): 全部展示 * - 内置信号 (signal_*): 全部展示
* - 自定义信号 (csg_*): 按 kind 过滤 (entry / exit / both) * - 自定义信号 (csg_*): 按 kind 过滤 (entry / exit / both), 除非 filterCustomByKind=false
* - entry 蓝色主题, exit 橙色主题 * - entry 蓝色主题, exit 橙色主题; 自定义信号边框配色与内置一致, 右上角 PenLine 角标区分
*/ */
export function SignalPicker({ signals, onChange, kind, variant = 'panel', builtinSignals, disabledSignals = [], disabledSignalHint }: Props) { export function SignalPicker({ signals, onChange, kind, options }: Props) {
const { variant = 'panel', builtinSignals, disabledSignals = [], disabledSignalHint, filterCustomByKind = true } = options ?? {}
const customSignalsQuery = useQuery({ queryKey: QK.customSignals, queryFn: api.customSignalsList }) const customSignalsQuery = useQuery({ queryKey: QK.customSignals, queryFn: api.customSignalsList })
const customOptions = useMemo(() => { const customOptions = useMemo(() => {
const list = (customSignalsQuery.data?.signals ?? []) const list = (customSignalsQuery.data?.signals ?? [])
.filter(s => s.enabled && (s.kind === kind || s.kind === 'both')) .filter(s => s.enabled && (!filterCustomByKind || s.kind === kind || s.kind === 'both'))
const names: Record<string, string> = {} const names: Record<string, string> = {}
for (const cs of list) names[`csg_${cs.id}`] = cs.name for (const cs of list) names[`csg_${cs.id}`] = cs.name
return { list, names } return { list, names }
}, [customSignalsQuery.data, kind]) }, [customSignalsQuery.data, kind, filterCustomByKind])
const toggle = (sig: string) => { const toggle = (sig: string) => {
const next = signals.includes(sig) ? signals.filter(x => x !== sig) : [...signals, sig] const next = signals.includes(sig) ? signals.filter(x => x !== sig) : [...signals, sig]
@@ -49,10 +62,6 @@ export function SignalPicker({ signals, onChange, kind, variant = 'panel', built
const idle = variant === 'dialog' const idle = variant === 'dialog'
? 'border-border bg-base text-muted hover:border-accent/40' ? 'border-border bg-base text-muted hover:border-accent/40'
: 'border-border bg-base text-muted hover:border-accent/40' : 'border-border bg-base text-muted hover:border-accent/40'
const customActive = isEntry
? 'border-accent/50 bg-accent/10 text-accent'
: 'border-warning/50 bg-warning/10 text-warning'
const customIdle = 'border-amber-400/30 bg-amber-400/5 text-secondary hover:border-amber-400/50 hover:text-amber-400'
const btnCls = variant === 'dialog' const btnCls = variant === 'dialog'
? 'rounded px-1.5 py-0.5 text-[10px] font-medium border transition-colors cursor-pointer' ? 'rounded px-1.5 py-0.5 text-[10px] font-medium border transition-colors cursor-pointer'
@@ -84,9 +93,12 @@ export function SignalPicker({ signals, onChange, kind, variant = 'panel', built
type="button" type="button"
onClick={() => toggle(id)} onClick={() => toggle(id)}
title="自定义信号" title="自定义信号"
className={`${btnCls} ${signals.includes(id) ? customActive : customIdle}`} className={`${btnCls} relative ${signals.includes(id) ? active : idle}`}
> >
{customOptions.names[id]} {customOptions.names[id]}
<span className="pointer-events-none absolute -top-1 -right-1 rounded-full border border-border bg-base p-px">
<PenLine className="h-2 w-2 text-muted" />
</span>
</button> </button>
) )
})} })}
@@ -582,7 +582,7 @@ export function StrategySettingsDialog({ strategyId, onClose, onSaved, onAiModif
defaultOpen={false} defaultOpen={false}
extra={<SignalTriggerActions kind="entry" signals={entrySignals} onChange={setEntrySignals} buttonClassName="rounded-md border border-border bg-base p-1 text-muted transition-colors cursor-pointer" iconClassName="h-3 w-3" />} extra={<SignalTriggerActions kind="entry" signals={entrySignals} onChange={setEntrySignals} buttonClassName="rounded-md border border-border bg-base p-1 text-muted transition-colors cursor-pointer" iconClassName="h-3 w-3" />}
> >
<SignalPicker signals={entrySignals} onChange={setEntrySignals} kind="entry" variant="dialog" /> <SignalPicker signals={entrySignals} onChange={setEntrySignals} kind="entry" options={{ variant: 'dialog' }} />
<div className="text-[10px] leading-4 text-muted/70"></div> <div className="text-[10px] leading-4 text-muted/70"></div>
</Section> </Section>
@@ -593,7 +593,7 @@ export function StrategySettingsDialog({ strategyId, onClose, onSaved, onAiModif
defaultOpen={false} defaultOpen={false}
extra={<SignalTriggerActions kind="exit" signals={exitSignals} onChange={setExitSignals} buttonClassName="rounded-md border border-border bg-base p-1 text-muted transition-colors cursor-pointer" iconClassName="h-3 w-3" />} extra={<SignalTriggerActions kind="exit" signals={exitSignals} onChange={setExitSignals} buttonClassName="rounded-md border border-border bg-base p-1 text-muted transition-colors cursor-pointer" iconClassName="h-3 w-3" />}
> >
<SignalPicker signals={exitSignals} onChange={setExitSignals} kind="exit" variant="dialog" /> <SignalPicker signals={exitSignals} onChange={setExitSignals} kind="exit" options={{ variant: 'dialog' }} />
<div className="text-[10px] leading-4 text-muted/70"></div> <div className="text-[10px] leading-4 text-muted/70"></div>
</Section> </Section>