diff --git a/src/components/ui/Select.module.scss b/src/components/ui/Select.module.scss new file mode 100644 index 0000000..0fbba01 --- /dev/null +++ b/src/components/ui/Select.module.scss @@ -0,0 +1,107 @@ +@use '../../styles/mixins' as *; + +.wrap { + position: relative; + display: inline-flex; + align-items: center; + width: 100%; +} + +.trigger { + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + height: 40px; + padding: 0 12px; + border: 1px solid var(--border-color); + border-radius: $radius-md; + background-color: var(--bg-primary); + box-shadow: var(--shadow); + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + appearance: none; + text-align: left; + box-sizing: border-box; + + &:hover { + border-color: var(--border-hover); + } + + &:focus { + outline: none; + box-shadow: var(--shadow), 0 0 0 3px rgba($primary-color, 0.18); + } + + &[aria-expanded='true'] { + border-color: var(--primary-color); + box-shadow: var(--shadow), 0 0 0 3px rgba($primary-color, 0.18); + } +} + +.triggerText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.placeholder { + color: var(--text-tertiary); +} + +.triggerIcon { + display: inline-flex; + color: var(--text-secondary); + flex-shrink: 0; + transition: transform 0.2s ease; + + [aria-expanded='true'] > & { + transform: rotate(180deg); + } +} + +.dropdown { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 1000; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: $radius-lg; + padding: 6px; + box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + gap: 4px; + max-height: 240px; + overflow-y: auto; + overscroll-behavior: contain; +} + +.option { + padding: 8px 12px; + border-radius: $radius-md; + border: 1px solid transparent; + background: transparent; + color: var(--text-primary); + cursor: pointer; + text-align: left; + font-size: 13px; + font-weight: 500; + transition: background-color 0.15s ease, border-color 0.15s ease; + flex-shrink: 0; + + &:hover { + background: var(--bg-secondary); + } +} + +.optionActive { + border-color: rgba($primary-color, 0.5); + background: rgba($primary-color, 0.1); + font-weight: 600; +} diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx new file mode 100644 index 0000000..3b1e04e --- /dev/null +++ b/src/components/ui/Select.tsx @@ -0,0 +1,75 @@ +import { useState, useEffect, useRef } from 'react'; +import { IconChevronDown } from './icons'; +import styles from './Select.module.scss'; + +export interface SelectOption { + value: string; + label: string; +} + +interface SelectProps { + value: string; + options: SelectOption[]; + onChange: (value: string) => void; + placeholder?: string; + className?: string; +} + +export function Select({ value, options, onChange, placeholder, className }: SelectProps) { + const [open, setOpen] = useState(false); + const wrapRef = useRef(null); + + useEffect(() => { + if (!open) return; + const handleClickOutside = (event: MouseEvent) => { + if (!wrapRef.current?.contains(event.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [open]); + + const selected = options.find((o) => o.value === value); + const displayText = selected?.label ?? placeholder ?? ''; + const isPlaceholder = !selected && placeholder; + + return ( +
+ + {open && ( +
+ {options.map((opt) => { + const active = opt.value === value; + return ( + + ); + })} +
+ )} +
+ ); +} diff --git a/src/components/usage/ChartLineSelector.tsx b/src/components/usage/ChartLineSelector.tsx index f56344e..22acc79 100644 --- a/src/components/usage/ChartLineSelector.tsx +++ b/src/components/usage/ChartLineSelector.tsx @@ -1,6 +1,8 @@ +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; +import { Select } from '@/components/ui/Select'; import styles from '@/pages/UsagePage.module.scss'; export interface ChartLineSelectorProps { @@ -41,6 +43,14 @@ export function ChartLineSelector({ onChange(newLines); }; + const options = useMemo( + () => [ + { value: 'all', label: t('usage_stats.chart_line_all') }, + ...modelNames.map((name) => ({ value: name, label: name })) + ], + [modelNames, t] + ); + return ( {t(`usage_stats.chart_line_label_${index + 1}`)} - + options={options} + onChange={(value) => handleChange(index, value)} + /> {chartLines.length > 1 && (