Files
doc-manager/apps/web/src/components/ui.tsx
T
2026-05-01 10:50:25 +03:00

110 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type ButtonHTMLAttributes, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes, useEffect } from 'react';
export function Button({
variant = 'default',
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: 'default' | 'primary' | 'danger' | 'ghost' }) {
return <button {...props} className={`btn btn--${variant} ${props.className ?? ''}`} />;
}
export function Field(
props: InputHTMLAttributes<HTMLInputElement> & { label: string; error?: string | undefined },
) {
const { label, error, ...input } = props;
return (
<label className="field">
<span className="field__label">{label}</span>
<input {...input} className={`field__input ${error ? 'field__input--err' : ''}`} />
{error ? <span className="field__error">{error}</span> : null}
</label>
);
}
export function Textarea(props: TextareaHTMLAttributes<HTMLTextAreaElement> & { label: string }) {
const { label, ...textarea } = props;
return (
<label className="field">
<span className="field__label">{label}</span>
<textarea {...textarea} className="field__input field__input--area" />
</label>
);
}
export function Select<T extends string>(
props: Omit<SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'onChange'> & {
label: string;
value: T;
onChange: (v: T) => void;
options: ReadonlyArray<{ value: T; label: string }>;
},
) {
const { label, value, onChange, options, ...sel } = props;
return (
<label className="field">
<span className="field__label">{label}</span>
<select
{...sel}
className="field__input"
value={value}
onChange={(e) => onChange(e.target.value as T)}
>
{options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</label>
);
}
export function Modal({
open,
title,
onClose,
children,
footer,
}: {
open: boolean;
title: string;
onClose: () => void;
children: ReactNode;
footer?: ReactNode;
}) {
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);
if (!open) return null;
return (
<div className="modal__backdrop" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<header className="modal__header">
<h3>{title}</h3>
<button className="modal__close" onClick={onClose} aria-label="Закрыть">
×
</button>
</header>
<div className="modal__body">{children}</div>
{footer ? <footer className="modal__footer">{footer}</footer> : null}
</div>
</div>
);
}
export function EmptyState({ children }: { children: ReactNode }) {
return <div className="empty">{children}</div>;
}
export function formatRub(cents: number): string {
return (cents / 100).toLocaleString('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 2,
});
}