This commit is contained in:
2026-06-06 16:23:55 +03:00
parent 517059d53e
commit 8487ac5ca0
12 changed files with 373 additions and 222 deletions

View File

@@ -44,45 +44,6 @@
margin-top: -12px;
}
/* Селект срока ожидания — стилизован под FormField input */
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.fieldLabel {
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
letter-spacing: 0.08em;
}
.select {
width: 100%;
height: 48px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: var(--text-primary);
font-size: 14px;
font-family: var(--font-sans);
padding: 0 16px;
outline: none;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
}
.select:focus {
border-color: var(--interactive);
box-shadow: 0 0 0 3px rgba(74, 109, 255, 0.15);
}
.select option {
background: var(--glass-bg, #1a1a2e);
color: var(--text-primary);
}
/* Панель условий / комиссии */
.infoCol {
display: flex;

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { FormField } from '@shared/ui'
import { FormField, Select } from '@shared/ui'
import styles from './LegalConverterPage.module.css'
const MIN_ORDER = 500_000
@@ -70,23 +70,16 @@ export function LegalConverterPage() {
</p>
)}
<div className={styles.field}>
<label className={styles.fieldLabel} htmlFor="term">
Срок ожидания операции
</label>
<select
id="term"
className={styles.select}
value={days}
onChange={(e) => setDays(Number(e.target.value))}
>
{TERM_OPTIONS.map((o) => (
<option key={o.days} value={o.days}>
{dayLabel(o.days)} комиссия {(o.rate * 100).toFixed(1)} %
</option>
))}
</select>
</div>
<Select
id="term"
label="Срок ожидания операции"
value={days}
onChange={setDays}
options={TERM_OPTIONS.map((o) => ({
value: o.days,
label: `${dayLabel(o.days)} — комиссия ${(o.rate * 100).toFixed(1)} %`,
}))}
/>
<FormField
label="Как к вам обращаться"

View File

@@ -0,0 +1,101 @@
.field {
position: relative;
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
letter-spacing: 0.08em;
}
.trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
height: 48px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: var(--text-primary);
font-size: 14px;
font-family: var(--font-sans);
text-align: left;
padding: 0 16px;
cursor: pointer;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.trigger:focus-visible,
.triggerOpen {
border-color: var(--interactive);
box-shadow: 0 0 0 3px rgba(74, 109, 255, 0.15);
}
.value {
color: var(--text-primary);
}
.placeholder {
color: var(--text-secondary);
}
.arrow {
flex-shrink: 0;
color: var(--text-secondary);
font-size: 12px;
transition: transform 0.2s;
}
.arrowOpen {
transform: rotate(180deg);
}
.dropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
z-index: 20;
margin: 0;
padding: 6px;
list-style: none;
background: var(--bg-mid);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
max-height: 280px;
overflow-y: auto;
}
.option {
padding: 11px 12px;
border-radius: 8px;
font-size: 14px;
color: var(--text-primary);
cursor: pointer;
transition: background-color 0.15s;
}
.option:hover {
background: rgba(255, 255, 255, 0.08);
}
.optionSelected {
background: rgba(91, 61, 184, 0.35);
}
.optionSelected:hover {
background: rgba(91, 61, 184, 0.45);
}

View File

@@ -0,0 +1,92 @@
import { useEffect, useRef, useState } from 'react'
import styles from './Select.module.css'
export interface SelectOption<T extends string | number = string> {
value: T
label: string
}
interface Props<T extends string | number> {
value: T
options: SelectOption<T>[]
onChange: (value: T) => void
label?: string
placeholder?: string
id?: string
disabled?: boolean
}
export function Select<T extends string | number>({
value,
options,
onChange,
label,
placeholder = 'Выберите',
id,
disabled,
}: Props<T>) {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const selected = options.find((o) => o.value === value)
useEffect(() => {
if (!open) return
const handlePointer = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false)
}
document.addEventListener('mousedown', handlePointer)
document.addEventListener('keydown', handleKey)
return () => {
document.removeEventListener('mousedown', handlePointer)
document.removeEventListener('keydown', handleKey)
}
}, [open])
return (
<div className={styles.field} ref={ref}>
{label && (
<label className={styles.label} htmlFor={id}>
{label}
</label>
)}
<button
type="button"
id={id}
className={`${styles.trigger} ${open ? styles.triggerOpen : ''}`}
onClick={() => !disabled && setOpen((v) => !v)}
disabled={disabled}
aria-haspopup="listbox"
aria-expanded={open}
>
<span className={selected ? styles.value : styles.placeholder}>
{selected ? selected.label : placeholder}
</span>
<span className={`${styles.arrow} ${open ? styles.arrowOpen : ''}`} aria-hidden>
</span>
</button>
{open && (
<ul className={styles.dropdown} role="listbox">
{options.map((o) => (
<li
key={String(o.value)}
role="option"
aria-selected={o.value === value}
className={`${styles.option} ${o.value === value ? styles.optionSelected : ''}`}
onClick={() => {
onChange(o.value)
setOpen(false)
}}
>
{o.label}
</li>
))}
</ul>
)}
</div>
)
}

View File

@@ -0,0 +1,2 @@
export { Select } from './Select'
export type { SelectOption } from './Select'

View File

@@ -6,4 +6,6 @@ export { FormField } from './FormField'
export { Notification } from './Notification'
export { Pill } from './Pill'
export { PrimaryButton } from './PrimaryButton'
export { Select } from './Select'
export type { SelectOption } from './Select'
export { TokenIcon } from './TokenIcon'