Files
BarevClub/packages/ui/src/index.tsx
T

212 lines
8.5 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, ReactNode, AnchorHTMLAttributes, CSSProperties } from 'react';
const cx = (...c: (string | false | null | undefined)[]) => c.filter(Boolean).join(' ');
export type Tone = 'home' | 'docs' | 'learn' | 'perks' | 'pink' | 'blue' | 'dark' | 'white';
export type CategoryCode = 'HOME' | 'DOCS' | 'LEARNING' | 'PERKS';
/** Цвет направления (раздел 9) */
export const CATEGORY_TONE: Record<CategoryCode, Tone> = { HOME: 'home', DOCS: 'docs', LEARNING: 'learn', PERKS: 'perks' };
type BtnVariant = 'dark' | 'light' | 'outline' | 'accent' | 'ghost' | 'danger';
export function Button({
variant = 'dark',
size,
block,
className,
...rest
}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: BtnVariant; size?: 'sm'; block?: boolean }) {
return <button type="button" className={cx('bc-btn', `bc-btn--${variant}`, size && `bc-btn--${size}`, block && 'bc-btn--block', className)} {...rest} />;
}
export function LinkButton({
variant = 'dark',
size,
block,
className,
...rest
}: AnchorHTMLAttributes<HTMLAnchorElement> & { variant?: BtnVariant; size?: 'sm'; block?: boolean }) {
return <a className={cx('bc-btn', `bc-btn--${variant}`, size && `bc-btn--${size}`, block && 'bc-btn--block', className)} {...rest} />;
}
export function Card({ tone, tight, className, style, children, as: As = 'div', ...rest }: { tone?: Tone; tight?: boolean; className?: string; style?: CSSProperties; children?: ReactNode; as?: 'div' | 'section' | 'article' } & Record<string, unknown>) {
return (
<As className={cx('bc-card', tight && 'bc-card--tight', tone && `bc-tone-${tone}`, className)} style={style} {...rest}>
{children}
</As>
);
}
export function Badge({ children, dot, style }: { children: ReactNode; dot?: boolean; style?: CSSProperties }) {
return (
<span className={cx('bc-badge', dot && 'bc-badge--dot')} style={style}>
{children}
</span>
);
}
export function Chip({ pressed, ...rest }: ButtonHTMLAttributes<HTMLButtonElement> & { pressed?: boolean }) {
return <button type="button" className="bc-chip" aria-pressed={pressed} {...rest} />;
}
export function Segmented<T extends string>({ value, options, onChange, label }: { value: T; options: { value: T; label: ReactNode }[]; onChange: (v: T) => void; label?: string }) {
return (
<div className="bc-segmented" role="group" aria-label={label}>
{options.map((o) => (
<button key={o.value} type="button" aria-pressed={o.value === value} onClick={() => onChange(o.value)}>
{o.label}
</button>
))}
</div>
);
}
export function Stepper({ value, min, max, onChange, decLabel = '−', incLabel = '+' }: { value: number; min: number; max: number; onChange: (v: number) => void; decLabel?: string; incLabel?: string }) {
return (
<div className="bc-stepper">
<button type="button" aria-label={decLabel} disabled={value <= min} onClick={() => onChange(Math.max(min, value - 1))}>
−
</button>
<output aria-live="polite">{value}</output>
<button type="button" aria-label={incLabel} disabled={value >= max} onClick={() => onChange(Math.min(max, value + 1))}>
+
</button>
</div>
);
}
export function Field({ label, error, children, htmlFor }: { label?: ReactNode; error?: ReactNode; children: ReactNode; htmlFor?: string }) {
return (
<div className="bc-field">
{label && (
<label className="bc-label" htmlFor={htmlFor}>
{label}
</label>
)}
{children}
{error && <div className="bc-error">{error}</div>}
</div>
);
}
export function Spinner({ label }: { label?: string }) {
return <div className="bc-spinner" role="status" aria-label={label} />;
}
export function Center({ children }: { children: ReactNode }) {
return <div className="bc-center">{children}</div>;
}
/** «Цветок» из макета: круг + 10 лепестков */
export function Flower({ size = 72, color = '#B8235A', rotate = -10, children, textColor = '#fff', fontSize }: { size?: number; color?: string; rotate?: number; children?: ReactNode; textColor?: string; fontSize?: number }) {
return (
<div className="bc-flower" style={{ width: size, height: size, transform: `rotate(${rotate}deg)` }}>
<svg aria-hidden="true" viewBox="0 0 100 100" width={size} height={size}>
<g fill={color}>
<circle cx="50" cy="50" r="34" />
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => {
const a = (i * Math.PI) / 5;
return <circle key={i} cx={50 + 32 * Math.cos(a)} cy={50 + 32 * Math.sin(a)} r="16" />;
})}
</g>
</svg>
{children !== undefined && <span style={{ color: textColor, fontSize: fontSize ?? size * 0.36, transform: `rotate(${-rotate}deg)` }}>{children}</span>}
</div>
);
}
export function Sheet({ open, onClose, children, label }: { open: boolean; onClose: () => void; children: ReactNode; label?: string }) {
if (!open) return null;
return (
<div className="bc-sheet-backdrop" onClick={onClose}>
<div className="bc-sheet" role="dialog" aria-modal="true" aria-label={label} onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
export function Steps({ items }: { items: { label: ReactNode; state: 'done' | 'current' | 'todo'; meta?: ReactNode }[] }) {
return (
<ol className="bc-steps" style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{items.map((s, i) => (
<li key={i} className={`bc-step bc-step--${s.state}`}>
<span className="bc-step__dot" aria-hidden="true">
{s.state === 'done' && <Icon name="check" size={14} />}
</span>
<span>
<span className="bc-step__label">{s.label}</span>
{s.meta && <div className="bc-small bc-muted">{s.meta}</div>}
</span>
</li>
))}
</ol>
);
}
// ───── иконки из макета (stroke 2, round) ─────
const ICONS = {
home: 'M4 11l8-7 8 7v9H4z',
docs: 'M7 3h7l5 5v13H7zM14 3v5h5M10 13h6M10 17h6',
learn: 'M4 5h6a2 2 0 0 1 2 2v13a2 2 0 0 0-2-2H4zM20 5h-6a2 2 0 0 0-2 2v13a2 2 0 0 1 2-2h6z',
gift: 'M4 8h16v4H4zM5 12h14v8H5zM12 8v12M12 8c-2-3-6-3-6 0M12 8c2-3 6-3 6 0',
check: 'M5 12l4 4 10-10',
arrow: 'M5 12h14M13 6l6 6-6 6',
back: 'M19 12H5M11 18l-6-6 6-6',
plus: 'M12 5v14M5 12h14',
grid: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
user: 'M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21a8 8 0 0 1 16 0',
clock: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2',
pin: 'M12 21s-7-6.2-7-11a7 7 0 0 1 14 0c0 4.8-7 11-7 11zM12 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z',
camera: 'M4 8h3l2-3h6l2 3h3v11H4zM12 17a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
chat: 'M4 5h16v11H8l-4 4z',
star: 'M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1L3.2 9.5l6.1-.9z',
logo: 'M12 21.5a9.5 9.5 0 1 0 0-19 9.5 9.5 0 0 0 0 19zM8 12.5l2.6 2.6L16 9.5',
close: 'M6 6l12 12M18 6L6 18',
repeat: 'M4 12a8 8 0 0 1 14-5.3L20 9M20 4v5h-5M20 12a8 8 0 0 1-14 5.3L4 15M4 20v-5h5',
globe: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM3 12h18M12 3c2.5 2.7 3.8 5.7 3.8 9s-1.3 6.3-3.8 9c-2.5-2.7-3.8-5.7-3.8-9S9.5 5.7 12 3z',
} as const;
export type IconName = keyof typeof ICONS;
export const CATEGORY_ICON: Record<CategoryCode, IconName> = { HOME: 'home', DOCS: 'docs', LEARNING: 'learn', PERKS: 'gift' };
export function Icon({ name, size = 22, strokeWidth = 2, style, label }: { name: IconName; size?: number; strokeWidth?: number; style?: CSSProperties; label?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
style={{ flex: 'none', ...style }}
aria-hidden={label ? undefined : true}
role={label ? 'img' : undefined}
aria-label={label}
>
<path d={ICONS[name]} />
</svg>
);
}
export function Logo({ size = 24, color = '#4A1028', text = 'Barev Club' }: { size?: number; color?: string; text?: string }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color }}>
<Icon name="logo" size={size} strokeWidth={2.4} />
<div style={{ fontFamily: 'var(--bc-font-head)', fontWeight: 700, fontSize: size * 0.84 }}>{text}</div>
</div>
);
}
export function Toast({ message, kind = 'info' }: { message: ReactNode; kind?: 'info' | 'error' }) {
return (
<div className={cx('bc-toast', kind === 'error' && 'bc-toast--error')} role={kind === 'error' ? 'alert' : 'status'}>
{message}
</div>
);
}