Initial ui-kit: Button, дизайн-токены, сборка Vite, Storybook
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
@import "../tokens/tokens";
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $r-sm;
|
||||
font-family: $font-sans;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.15px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: 0.18s;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sm {
|
||||
padding: 7px 14px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: $navy-800;
|
||||
border-color: $navy-800;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: $navy-700;
|
||||
box-shadow: $sh-2;
|
||||
}
|
||||
}
|
||||
|
||||
.accent {
|
||||
background: $grad-gold;
|
||||
border-color: $gold-500;
|
||||
color: $navy-950;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $sh-gold;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: $white;
|
||||
border-color: $gray-300;
|
||||
color: $navy-800;
|
||||
|
||||
&:hover {
|
||||
border-color: $gold-500;
|
||||
color: $navy-900;
|
||||
}
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: transparent;
|
||||
color: $navy-700;
|
||||
|
||||
&:hover {
|
||||
background: $navy-100;
|
||||
}
|
||||
}
|
||||
|
||||
.danger {
|
||||
background: $white;
|
||||
border-color: $gray-300;
|
||||
color: $red-500;
|
||||
|
||||
&:hover {
|
||||
border-color: $red-500;
|
||||
background: $red-50;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Button } from './Button';
|
||||
|
||||
// meta — описание компонента для Storybook: где показать в дереве слева,
|
||||
// какой компонент и какими контролами крутить пропсы в панели справа.
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'], // автоматически сгенерит страницу-документацию
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'accent', 'secondary', 'ghost', 'danger']
|
||||
},
|
||||
size: { control: 'radio', options: ['md', 'sm'] },
|
||||
fullWidth: { control: 'boolean' },
|
||||
disabled: { control: 'boolean' }
|
||||
},
|
||||
args: {
|
||||
children: 'Кнопка',
|
||||
variant: 'primary'
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
// Каждый export — отдельная «история» (состояние компонента) в дереве Storybook.
|
||||
export const Primary: Story = { args: { variant: 'primary' } };
|
||||
export const Accent: Story = { args: { variant: 'accent' } };
|
||||
export const Secondary: Story = { args: { variant: 'secondary' } };
|
||||
export const Ghost: Story = { args: { variant: 'ghost' } };
|
||||
export const Danger: Story = { args: { variant: 'danger', children: 'Удалить' } };
|
||||
export const Small: Story = { args: { size: 'sm' } };
|
||||
export const Disabled: Story = { args: { disabled: true } };
|
||||
|
||||
// render позволяет показать сразу несколько кнопок — удобно для обзора палитры.
|
||||
export const AllVariants: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="accent">Accent</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="danger">Danger</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import styles from './Button.module.scss';
|
||||
|
||||
type ButtonVariant = 'primary' | 'accent' | 'secondary' | 'ghost' | 'danger';
|
||||
type ButtonSize = 'md' | 'sm';
|
||||
|
||||
interface IButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const variantClass: Record<ButtonVariant, string> = {
|
||||
primary: styles.primary,
|
||||
accent: styles.accent,
|
||||
secondary: styles.secondary,
|
||||
ghost: styles.ghost,
|
||||
danger: styles.danger
|
||||
};
|
||||
|
||||
export const Button: React.FC<IButtonProps> = ({
|
||||
variant = 'secondary',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
type = 'button',
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}) => {
|
||||
const classes = [
|
||||
styles.btn,
|
||||
variantClass[variant],
|
||||
size === 'sm' ? styles.sm : '',
|
||||
fullWidth ? styles.full : '',
|
||||
className
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
// type по умолчанию 'button', чтобы кнопки внутри формы не сабмитили её случайно.
|
||||
return (
|
||||
<button type={type} className={classes} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module '*.module.scss' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Button } from "./Button/Button";
|
||||
@@ -0,0 +1,146 @@
|
||||
// ============================================
|
||||
// Tansu Construction — Design Tokens (V5: Executive Premium)
|
||||
// Партиал: только SCSS-переменные и миксины, БЕЗ вывода CSS.
|
||||
// Импортируется в каждый *.module.scss:
|
||||
// @import '<путь>/styles/design-tokens';
|
||||
// Здесь намеренно нет блока :root — чтобы импорт в десятки
|
||||
// модулей не дублировал CSS-переменные в бандле. Значения
|
||||
// используем напрямую через SCSS-переменные ($navy-800 и т.д.).
|
||||
// ============================================
|
||||
|
||||
// ---- Navy (Primary) ----
|
||||
$navy-950: #0A1B30;
|
||||
$navy-900: #0F2440;
|
||||
$navy-800: #1B3A5C;
|
||||
$navy-700: #244B73;
|
||||
$navy-600: #2D5C8A;
|
||||
$navy-100: #E8EEF4;
|
||||
$navy-50: #F4F7FA;
|
||||
|
||||
// ---- Gold (Accent) ----
|
||||
$gold-600: #B8881A;
|
||||
$gold-500: #D4A01E;
|
||||
$gold-400: #E5B635;
|
||||
$gold-300: #F0CC5E;
|
||||
$gold-100: #FDF5E0;
|
||||
$gold-50: #FEFBF0;
|
||||
|
||||
// ---- Gray (Neutral) ----
|
||||
$gray-900: #111827;
|
||||
$gray-700: #374151;
|
||||
$gray-500: #6B7280;
|
||||
$gray-400: #9CA3AF;
|
||||
$gray-300: #D1D5DB;
|
||||
$gray-200: #E5E7EB;
|
||||
$gray-100: #F3F4F6;
|
||||
$gray-50: #F9FAFB;
|
||||
|
||||
// ---- Semantic ----
|
||||
$red-500: #DC2626;
|
||||
$red-50: #FEF2F2;
|
||||
$green-600: #15803D;
|
||||
$green-50: #F0FDF4;
|
||||
$blue-600: #2563EB;
|
||||
$blue-50: #EFF6FF;
|
||||
$amber-700: #854D0E;
|
||||
|
||||
$white: #fff;
|
||||
|
||||
// ---- Typography ----
|
||||
// Заголовки страниц и секций — Lora (serif). Остальной текст — Inter (sans).
|
||||
$font-serif: 'Lora', Georgia, serif;
|
||||
$font-sans: 'Inter', -apple-system, 'Segoe UI', sans-serif;
|
||||
|
||||
$fs-page-title: 24px; // Lora 600
|
||||
$fs-form-title: 22px; // Lora 600
|
||||
$fs-section-title: 15px; // Lora 600
|
||||
$fs-body: 14px; // Inter 400
|
||||
$fs-sm: 13px;
|
||||
$fs-label: 12px; // Inter 500
|
||||
$fs-caption: 11px;
|
||||
$fs-overline: 10.5px; // uppercase, letter-spacing 1.4-1.8px
|
||||
|
||||
// ---- Radius ----
|
||||
$r-sm: 6px;
|
||||
$r-md: 10px;
|
||||
$r-lg: 14px;
|
||||
$r-full: 100px;
|
||||
|
||||
// ---- Shadows ----
|
||||
$sh-1: 0 1px 2px rgba(15,36,64,.04), 0 1px 3px rgba(15,36,64,.06);
|
||||
$sh-2: 0 4px 16px rgba(15,36,64,.07);
|
||||
$sh-3: 0 12px 32px rgba(15,36,64,.10);
|
||||
$sh-gold: 0 4px 20px rgba(212,160,30,.18);
|
||||
$sh-modal: 0 24px 70px rgba(10,27,48,.25);
|
||||
|
||||
// ---- Gradients (фирменные) ----
|
||||
$grad-header: linear-gradient(135deg, $navy-950 0%, $navy-800 85%);
|
||||
$grad-gold: linear-gradient(135deg, $gold-400, $gold-500);
|
||||
$grad-gold-v: linear-gradient(180deg, $gold-400, $gold-600);
|
||||
$grad-hairline: linear-gradient(90deg, $gold-400, transparent 70%);
|
||||
|
||||
// ---- Spacing ----
|
||||
$sp-1: 4px; $sp-2: 8px; $sp-3: 12px;
|
||||
$sp-4: 16px; $sp-5: 20px; $sp-6: 24px;
|
||||
$sp-7: 28px; $sp-8: 32px;
|
||||
|
||||
// ---- Готовые миксины ----
|
||||
@mixin page-header {
|
||||
background: $grad-header;
|
||||
border-radius: $r-lg;
|
||||
padding: 26px 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, $gold-500, transparent 60%);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin card-hover-gold {
|
||||
transition: .22s;
|
||||
&:hover {
|
||||
box-shadow: $sh-3;
|
||||
transform: translateY(-2px);
|
||||
border-color: $gold-300;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin btn-accent {
|
||||
background: $grad-gold;
|
||||
color: $navy-950;
|
||||
font-weight: 600;
|
||||
border: 1px solid $gold-500;
|
||||
&:hover { box-shadow: $sh-gold; transform: translateY(-1px); }
|
||||
}
|
||||
|
||||
@mixin input-focus-gold {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: $gold-500;
|
||||
box-shadow: 0 0 0 3px rgba(212,160,30,.12);
|
||||
}
|
||||
}
|
||||
|
||||
// Вид поля «только чтение» (режим согласующего / отправленная заявка):
|
||||
// приглушённый фон, серый текст, без золотого фокуса — визуально «неактивно».
|
||||
// $sel — псевдокласс состояния поля (:read-only для input/textarea, :disabled для select).
|
||||
@mixin readonly-field($sel) {
|
||||
&#{$sel},
|
||||
&#{$sel}:focus {
|
||||
background: $gray-50;
|
||||
color: $gray-500;
|
||||
border-color: $gray-200;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin hairline {
|
||||
height: 1px;
|
||||
background: $grad-hairline;
|
||||
border: 0;
|
||||
}
|
||||
Reference in New Issue
Block a user