feat: примитивы Checkbox, Spinner, IconButton, Dialog, Link (0.10.0)
Набор B перекраски форм: брендовые адаптивные примитивы + иконки arrow-up/arrow-down/swap/refresh + токен $amber-50. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
@import '../tokens/tokens';
|
||||
|
||||
.wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
min-height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Нативный input скрыт, но остаётся фокусируемым/доступным.
|
||||
.input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1.5px solid $gray-400;
|
||||
border-radius: 4px;
|
||||
background: $white;
|
||||
transition: background .15s, border-color .15s;
|
||||
}
|
||||
|
||||
// Выбран: navy-заливка + белая галка (контраст ~9:1).
|
||||
.input:checked + .box {
|
||||
background: $navy-800;
|
||||
border-color: $navy-800;
|
||||
}
|
||||
|
||||
.input:checked + .box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 1px;
|
||||
width: 5px;
|
||||
height: 9px;
|
||||
border: solid $white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
// Промежуточное (после :checked, чтобы перебить галку на чёрточку).
|
||||
.input:indeterminate + .box {
|
||||
background: $navy-800;
|
||||
border-color: $navy-800;
|
||||
}
|
||||
|
||||
.input:indeterminate + .box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 7px;
|
||||
width: 10px;
|
||||
height: 2px;
|
||||
border: 0;
|
||||
border-radius: 1px;
|
||||
background: $white;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
// Фокус с клавиатуры — золотое кольцо (бренд).
|
||||
.input:focus-visible + .box {
|
||||
box-shadow: 0 0 0 3px rgba(212, 160, 30, .25);
|
||||
border-color: $gold-500;
|
||||
}
|
||||
|
||||
// Ховер (когда доступен).
|
||||
.wrap:hover .input:not(:disabled) + .box {
|
||||
border-color: $navy-600;
|
||||
}
|
||||
|
||||
// Недоступен / только чтение.
|
||||
.input:disabled + .box {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.input:disabled ~ .text {
|
||||
color: $gray-400;
|
||||
}
|
||||
|
||||
.wrap:has(.input:disabled) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: $fs-sm;
|
||||
color: $gray-700;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// На узких экранах увеличиваем тач-таргет.
|
||||
@include below($bp-md) {
|
||||
.wrap { min-height: 32px; gap: 10px; }
|
||||
.box { width: 20px; height: 20px; }
|
||||
.input:checked + .box::after { left: 6px; top: 2px; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import { Checkbox } from './Checkbox';
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: 'Поля/Checkbox',
|
||||
component: Checkbox,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj<typeof Checkbox> = {
|
||||
render: () => {
|
||||
const [c, setC] = useState(false);
|
||||
return <Checkbox checked={c} onChange={setC} label="Согласен" />;
|
||||
},
|
||||
};
|
||||
|
||||
export const States: StoryObj<typeof Checkbox> = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<Checkbox checked={false} onChange={() => undefined} label="Не выбран" />
|
||||
<Checkbox checked onChange={() => undefined} label="Выбран" />
|
||||
<Checkbox checked={false} indeterminate onChange={() => undefined} label="Промежуточный (выбрать все)" />
|
||||
<Checkbox checked disabled onChange={() => undefined} label="Выбран, недоступен" />
|
||||
<Checkbox checked={false} disabled onChange={() => undefined} label="Недоступен" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** В ячейках таблиц — без видимой подписи, с ariaLabel для доступности. */
|
||||
export const InTableCell: StoryObj<typeof Checkbox> = {
|
||||
render: () => {
|
||||
const [c, setC] = useState(true);
|
||||
return <Checkbox checked={c} onChange={setC} ariaLabel="Выбрать строку" />;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
import styles from './Checkbox.module.scss';
|
||||
import { useFormReadOnly } from '../FormMode';
|
||||
|
||||
export interface ICheckboxProps {
|
||||
id?: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
/** Промежуточное состояние (шапка «выбрать все» при частичном выборе). */
|
||||
indeterminate?: boolean;
|
||||
/** Недоступен (заблокирован). */
|
||||
disabled?: boolean;
|
||||
/** Только чтение (режим согласующего). По умолчанию берётся из FormMode-контекста. */
|
||||
readOnly?: boolean;
|
||||
/** Подпись справа от флажка. */
|
||||
label?: string;
|
||||
/** a11y-подпись для флажков без видимого label (в ячейках таблиц). */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
/** Флажок (SharePoint Boolean / выбор строк). Управляемый: checked внутрь, onChange наружу. */
|
||||
export const Checkbox: React.FC<ICheckboxProps> = ({
|
||||
id,
|
||||
checked,
|
||||
onChange,
|
||||
indeterminate,
|
||||
disabled,
|
||||
readOnly,
|
||||
label,
|
||||
ariaLabel
|
||||
}) => {
|
||||
const contextReadOnly = useFormReadOnly();
|
||||
const effectiveDisabled = disabled || (readOnly ?? contextReadOnly);
|
||||
const ref = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.indeterminate = !!indeterminate;
|
||||
}
|
||||
}, [indeterminate]);
|
||||
|
||||
return (
|
||||
<label className={styles.wrap}>
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
type="checkbox"
|
||||
className={styles.input}
|
||||
checked={checked}
|
||||
disabled={effectiveDisabled}
|
||||
aria-label={ariaLabel}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
<span className={styles.box} aria-hidden="true" />
|
||||
{label && <span className={styles.text}>{label}</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user