Files
exodevices/medcenterphysio/src/components/FormField.tsx
T
Yuriy PanovandClaude Opus 5 23986ff5ad medcenterphysio: apply the v3 design and add the callback dialog
v3 differs from legacy/new only by the callback dialog; the new phone
number was already in the React landing.

The header «Звонок» button opens the «Перезвоним вам» dialog from the
fitness landing (name, phone, consent), ported the same way as in
medcenterstart: the phone field keeps this landing's +7 mask and refuses
fewer than 11 digits, as the mockup does. The server accepts form
'callback', names the deal «Обратный звонок — <имя>» and tags it
«обратный звонок». The phone link inside the dialog and the call button
in the mobile action bar still report a call click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:46:16 +06:00

117 lines
3.3 KiB
TypeScript

import type { ComponentPropsWithRef, ReactNode } from 'react'
import { cx } from '../lib/cx'
/**
* `glass` — controls on the navy CTA panel; `light` — on the white callback
* dialog, the same fields as the fitness landing's.
*/
type Tone = 'glass' | 'light'
const controlClass: Record<Tone, string> = {
glass:
'w-full rounded-[14px] border border-white/[0.18] bg-white/[0.08] px-[15px] text-white outline-none ' +
'transition-[border-color,background-color,box-shadow] duration-200 placeholder:text-white/[0.42] ' +
'focus:border-teal focus:bg-white/[0.12] focus:shadow-[0_0_0_4px_rgb(0_196_180/0.12)]',
light:
'w-full rounded-[15px] border border-navy/15 bg-field px-[15px] py-[14px] text-ink outline-none ' +
'transition-[border-color,box-shadow,background-color] duration-200 ' +
'focus:border-teal focus:bg-white focus:shadow-[0_0_0_4px_rgb(0_196_180/0.10)]',
}
const labelClass: Record<Tone, string> = {
glass: 'text-[12px] font-[750] text-white/[0.7]',
light: 'text-[11px] font-extrabold text-muted',
}
function Field({
id,
label,
full,
tone,
children,
}: {
id: string
label: string
full?: boolean
tone: Tone
children: ReactNode
}) {
return (
<div className={cx('grid gap-[7px]', full && 'sm:col-span-full')}>
<label htmlFor={id} className={labelClass[tone]}>
{label}
</label>
{children}
</div>
)
}
export function TextField({
id,
label,
full,
tone = 'glass',
...rest
}: { id: string; label: string; full?: boolean; tone?: Tone } & ComponentPropsWithRef<'input'>) {
return (
<Field id={id} label={label} full={full} tone={tone}>
<input id={id} className={cx(controlClass[tone], tone === 'glass' ? 'min-h-[52px]' : 'h-[51px]')} {...rest} />
</Field>
)
}
export function TextareaField({
id,
label,
full,
...rest
}: { id: string; label: string; full?: boolean } & ComponentPropsWithRef<'textarea'>) {
return (
<Field id={id} label={label} full={full} tone="glass">
<textarea id={id} className={cx(controlClass.glass, 'min-h-[104px] resize-y pt-[14px]')} {...rest} />
</Field>
)
}
/** Bot trap. Invisible to people, but reachable by naive form-filling scripts. */
export function Honeypot({
id = 'website',
value,
onChange,
}: {
/** Must differ per form: the request form and the callback dialog can be on screen together. */
id?: string
value: string
onChange: (value: string) => void
}) {
return (
<div aria-hidden="true" className="honeypot">
<label htmlFor={id}>Не заполнять</label>
<input
autoComplete="off"
id={id}
name="website"
tabIndex={-1}
type="text"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</div>
)
}
export function ConsentCheckbox({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) {
return (
<label className="my-[13px] grid grid-cols-[18px_1fr] gap-[9px] text-[10px] text-muted">
<input
type="checkbox"
required
checked={checked}
onChange={(event) => onChange(event.target.checked)}
className="mt-[2px] mb-[3px] accent-teal"
/>
<span>Согласен на обработку персональных данных и получение обратного звонка.</span>
</label>
)
}