@de-kit/st-ui 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -17
- package/dist/components/button/button.cjs +37 -17
- package/dist/components/button/button.cjs.map +1 -1
- package/dist/components/button/button.d.cts +7 -2
- package/dist/components/button/button.d.ts +7 -2
- package/dist/components/button/button.js +37 -17
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/button.scss +65 -27
- package/dist/components/checkbox/checkbox.cjs +50 -8
- package/dist/components/checkbox/checkbox.cjs.map +1 -1
- package/dist/components/checkbox/checkbox.d.cts +11 -4
- package/dist/components/checkbox/checkbox.d.ts +11 -4
- package/dist/components/checkbox/checkbox.js +51 -9
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/checkbox/checkbox.scss +55 -26
- package/dist/components/field/field-context.cjs +9 -0
- package/dist/components/field/field-context.cjs.map +1 -0
- package/dist/components/field/field-context.js +8 -0
- package/dist/components/field/field-context.js.map +1 -0
- package/dist/components/field/field.cjs +105 -0
- package/dist/components/field/field.cjs.map +1 -0
- package/dist/components/field/field.d.cts +24 -0
- package/dist/components/field/field.d.ts +24 -0
- package/dist/components/field/field.js +105 -0
- package/dist/components/field/field.js.map +1 -0
- package/dist/components/field/field.scss +43 -0
- package/dist/components/field/index.d.cts +2 -0
- package/dist/components/field/index.d.ts +2 -0
- package/dist/components/input/index.d.cts +2 -2
- package/dist/components/input/index.d.ts +2 -2
- package/dist/components/input/input.cjs +88 -8
- package/dist/components/input/input.cjs.map +1 -1
- package/dist/components/input/input.d.cts +8 -1
- package/dist/components/input/input.d.ts +8 -1
- package/dist/components/input/input.js +90 -10
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/input/input.scss +77 -14
- package/dist/components/radio/index.d.cts +2 -2
- package/dist/components/radio/index.d.ts +2 -2
- package/dist/components/radio/radio.cjs +47 -5
- package/dist/components/radio/radio.cjs.map +1 -1
- package/dist/components/radio/radio.d.cts +12 -2
- package/dist/components/radio/radio.d.ts +12 -2
- package/dist/components/radio/radio.js +48 -7
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/radio/radio.scss +50 -15
- package/dist/components/spinner/spinner.cjs +38 -16
- package/dist/components/spinner/spinner.cjs.map +1 -1
- package/dist/components/spinner/spinner.d.cts +5 -3
- package/dist/components/spinner/spinner.d.ts +5 -3
- package/dist/components/spinner/spinner.js +39 -17
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/spinner/spinner.scss +18 -1
- package/dist/components/textarea/index.d.cts +2 -2
- package/dist/components/textarea/index.d.ts +2 -2
- package/dist/components/textarea/textarea.cjs +68 -9
- package/dist/components/textarea/textarea.cjs.map +1 -1
- package/dist/components/textarea/textarea.d.cts +4 -2
- package/dist/components/textarea/textarea.d.ts +4 -2
- package/dist/components/textarea/textarea.js +69 -10
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/textarea/textarea.scss +50 -23
- package/dist/icons/index.cjs +2 -2
- package/dist/icons/index.js +2 -2
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/styles/global.scss +33 -27
- package/dist/styles/mixins.scss +37 -174
- package/dist/styles/vars.scss +73 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Input,
|
|
13
13
|
TextArea,
|
|
14
14
|
CheckboxInput,
|
|
15
|
+
RadioGroup,
|
|
15
16
|
RadioInput,
|
|
16
17
|
Spinner,
|
|
17
18
|
} from '@de-kit/st-ui';
|
|
@@ -30,13 +31,31 @@ import { IconClose, LogoVisa } from '@de-kit/st-ui/icons';
|
|
|
30
31
|
|
|
31
32
|
`global.scss` — база приложения (body, заголовки, таблицы, `.wrapper`). В чужой странице его не подключать. Шрифт Inter библиотека не везёт, его по-прежнему грузит фронтенд.
|
|
32
33
|
|
|
33
|
-
Токены подключаются один раз вместе с пакетом (`import './styles/vars.scss'` в `src/index.ts`). В своих scss это обычные `var()`, `@use` не нужен. `@use '@de-kit/st-ui/vars.scss'` — только если стили собираются без JS.
|
|
34
|
+
Токены подключаются один раз вместе с пакетом (`import './styles/vars.scss'` в `src/index.ts`). В своих scss это обычные `var()`, `@use` не нужен. `@use '@de-kit/st-ui/vars.scss'` — только если стили собираются без JS.
|
|
35
|
+
|
|
36
|
+
Миксины — только брейкпоинты для адаптивной вёрстки:
|
|
34
37
|
|
|
35
38
|
```scss
|
|
36
39
|
@use '@de-kit/st-ui/mixins.scss' as mixins;
|
|
40
|
+
|
|
41
|
+
.card {
|
|
42
|
+
padding: var(--space-2);
|
|
43
|
+
|
|
44
|
+
@include mixins.min-width(md) {
|
|
45
|
+
padding: var(--space-3);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@include mixins.max-width(sm) {
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
37
52
|
```
|
|
38
53
|
|
|
39
|
-
`
|
|
54
|
+
`sm` 480, `md` 680, `lg` 1000, `xl` 1200. Это единственный источник: `--breakpoint-*` в `vars.scss` генерируются из того же списка, так что новый брейкпоинт в `$breakpoints` сразу даёт и миксин, и токен. Отдельный список нужен потому, что в `@media` custom properties не работают. `min-width` включает брейкпоинт, `max-width` — нет (граница `679.98px`), поэтому на ровно 680px срабатывает только `min-width`. Вместо имени можно передать размер: `min-width(900px)`. Список доступен как `mixins.$breakpoints`, значение — `mixins.breakpoint(md)`.
|
|
55
|
+
|
|
56
|
+
`var(--color-foreground)`, `var(--color-primary)`, `var(--font-sans)`, `var(--radius-sm)`, `var(--breakpoint-md)`. Цвета — роли как у Tailwind v4, не пигмент: `--color-foreground` текст, `--color-background` страница, `--color-primary` / `--color-primary-foreground` залитая кнопка и текст на ней. Outline берёт `--color-foreground` (текст и рамка, фон прозрачный). `--color-accent` синий, `--color-destructive` ошибка, `--color-success` успех, `--color-warning` жёлтый — из него разбавлен фон `--color-recommended`. Тема — переопределение этих переменных, компоненты не перекрашивать. `--color-overlay` и `--color-shadow` из темы не выводятся: вуаль и тень остаются тёмными и в светлой, и в тёмной, нужен другой оттенок — переопределять их отдельно.
|
|
57
|
+
|
|
58
|
+
Текст — пара токенов на размер, других размеров в splittest нет: `--text-xs` (11/15), `--text-sm` (14/20, по умолчанию почти везде), `--text-base` (16/20), `--text-lg` (18/20), `--text-2xl` (28/30), `--text-3xl` (45/50); высота строки — `--text-*--line-height`. Заголовки `global.scss` собраны из них: h1 и h2 — `2xl` (h1 с 680px — `3xl`), h3 — `lg`, h4 — `sm`. Размеры `Button` `sm` (12/16) и `lg` (16/24) в шкалу не попадают и остаются литералами. Отступы — сетка 5px: `--space-1` … `--space-6`; паддинги кнопки и инпута сняты с макета вне сетки и остаются литералами. Обводка фокуса — `--focus-ring-width`, `--focus-ring-color` и `--focus-ring-offset`, одна на `box-shadow` инпута и `outline` остальных контролов.
|
|
40
59
|
|
|
41
60
|
Компонентный scss собирает sass хоста. Класс в DOM буквальный: `button`, `input`, `checkbox`, `radio`, `textarea`, `spinner`. Корень и модификаторы одинарные (`.button`, `.button--outline`). Внутренние элементы — BEM: `checkbox__input`, `textarea__counter`.
|
|
42
61
|
|
|
@@ -60,20 +79,18 @@ import { IconClose, LogoVisa } from '@de-kit/st-ui/icons';
|
|
|
60
79
|
Дальше
|
|
61
80
|
<IconArrowRight width={20} height={20} />
|
|
62
81
|
</Button>
|
|
63
|
-
<Button>
|
|
64
|
-
<IconSpinner className="button__icon--spin" width={20} height={20} />
|
|
65
|
-
Сохранение
|
|
66
|
-
</Button>
|
|
82
|
+
<Button loading>Сохранение</Button>
|
|
67
83
|
```
|
|
68
84
|
|
|
69
|
-
Иконка — обычный `children`, слева или справа, какой набор нужен.
|
|
85
|
+
Иконка — обычный `children`, слева или справа, какой набор нужен. `loading` оставляет подпись в потоке (`opacity: 0`) и кладёт спиннер поверх, поэтому ширина не прыгает. Ставит `aria-busy`. Пока `loading`, `<button>` получает `disabled`, у ссылки клик отменяется.
|
|
70
86
|
|
|
71
|
-
| Prop | По умолчанию |
|
|
72
|
-
| ----------- | ------------ |
|
|
73
|
-
| `variant` | `default` | `default` заливка, `outline` обводка, `link` текст
|
|
74
|
-
| `size` | `md` | `sm` компактная, `md` обычная, `lg` крупная
|
|
75
|
-
| `fullWidth` | `false` | ширина 100%, не зависит от `size`
|
|
76
|
-
| `href` | — | рендер `<a>`, не `<Link>`
|
|
87
|
+
| Prop | По умолчанию | |
|
|
88
|
+
| ----------- | ------------ | --------------------------------------------------- |
|
|
89
|
+
| `variant` | `default` | `default` заливка, `outline` обводка, `link` текст |
|
|
90
|
+
| `size` | `md` | `sm` компактная, `md` обычная, `lg` крупная |
|
|
91
|
+
| `fullWidth` | `false` | ширина 100%, не зависит от `size` |
|
|
92
|
+
| `href` | — | рендер `<a>`, не `<Link>` |
|
|
93
|
+
| `loading` | `false` | `aria-busy`, клик блокируется, ширина как у подписи |
|
|
77
94
|
|
|
78
95
|
`variant="link"` — это вид кнопки (`font-weight: 400`), не тип элемента. Ссылка — отдельный проп `href`.
|
|
79
96
|
|
|
@@ -82,14 +99,39 @@ import { IconClose, LogoVisa } from '@de-kit/st-ui/icons';
|
|
|
82
99
|
```tsx
|
|
83
100
|
<Input recommended />
|
|
84
101
|
<Input error />
|
|
102
|
+
<Input size="sm" />
|
|
85
103
|
<TextArea maxLength={200} />
|
|
86
|
-
<TextArea maxLength={200}
|
|
87
|
-
<CheckboxInput
|
|
88
|
-
<
|
|
104
|
+
<TextArea maxLength={200} counter={false} />
|
|
105
|
+
<CheckboxInput label="18-24" />
|
|
106
|
+
<CheckboxInput label="Согласен" error />
|
|
107
|
+
<RadioGroup label="Возраст" name="age" required>
|
|
108
|
+
<RadioInput label="18-24" />
|
|
109
|
+
</RadioGroup>
|
|
89
110
|
<Spinner position="top" />
|
|
111
|
+
<Spinner label="Считаем" size={24} />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`size` у `Input` и `TextArea` — те же `sm`, `md`, `lg`, что у кнопки. У `Input` высота совпадает с кнопкой ровно: 30, 40 и 52px, включая `type="date"`. У `TextArea` размер меняет паддинги и `min-height`: 120 / 160 / 200px, с 680px — 60 / 80 / 100px. Текст во всех размерах остаётся `--text-sm`. У `Input` проп перекрывает нативный `size` (ширину в символах). `counter={false}` заменяет опечатку `noMaxMoty`. Счётчик `TextArea` показывает остаток символов и связан с полем через `aria-describedby`, сам `maxLength` по-прежнему режет ввод. `CheckboxInput` сам ставит `id`, если его не передали. Общий `name` у радио задаёт `RadioGroup`, она же рисует `fieldset` с `role="radiogroup"`. Её `required` делает обязательной всю группу и вне `Field`: `aria-required` уезжает на группу, нативный `required` — на пункты. Красная рамка у `CheckboxInput` и `RadioInput` — это проп `error`; один `required` больше не красит, он только обязывает. `Spinner` тянется по родителю, `position` раскладывает его по высоте, а `label` (по умолчанию «Загрузка») — подпись для скринридера.
|
|
115
|
+
|
|
116
|
+
## Поле
|
|
117
|
+
|
|
118
|
+
`Field` оборачивает один контрол: ставит лейбл, подсказку и ошибку и связывает их через `aria-describedby`, `aria-errormessage` и `role="alert"`.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
<Field label="Имя" hint="Нельзя изменить после проверки">
|
|
122
|
+
<Input />
|
|
123
|
+
</Field>
|
|
124
|
+
<Field label="Почта" required error="Проверьте адрес">
|
|
125
|
+
<Input type="email" />
|
|
126
|
+
</Field>
|
|
127
|
+
<Field label="Возраст" required>
|
|
128
|
+
<RadioGroup name="age">
|
|
129
|
+
<RadioInput label="18-24" value="18" />
|
|
130
|
+
</RadioGroup>
|
|
131
|
+
</Field>
|
|
90
132
|
```
|
|
91
133
|
|
|
92
|
-
`
|
|
134
|
+
Контрол внутри должен быть один: id он берёт у поля, поэтому свой `id` на ребёнке перебивается, а два контрола получат одинаковый. `required` ставит `aria-required` на контрол и показывает ошибку «Поле «Имя» обязательно» (свой текст — проп `requiredMessage`) в двух случаях: когда в поле что-то было, а потом не стало (стёрли текст, сняли галочку) и когда форму не пустила проверка браузера — `Field` ловит `invalid` в фазе захвата. Звёздочки у лейбла нет, обязательность видно по сообщению. У радиогруппы момента «стёрли» не бывает, поэтому её держит нативный `required` на пунктах: форма не отправится, ошибка появится после неудачной отправки и уйдёт, как только выберут вариант. Отсюда и граница: `required` поля работает с нативными контролами — `Input`, `TextArea`, `CheckboxInput`, `RadioGroup`. Произвольный ребёнок не отдаёт ни значения, ни `invalid`, и поле о его пустоте не узнает. `RadioGroup` внутри `Field` легенду не рисует: именем группы становится лейбл поля.
|
|
93
135
|
|
|
94
136
|
## Разработка
|
|
95
137
|
|
|
@@ -1,44 +1,64 @@
|
|
|
1
|
+
const require_icon_spinner = require("../../icons/icon-spinner.cjs");
|
|
1
2
|
const require_cn = require("../../lib/cn.cjs");
|
|
2
3
|
let react = require("react");
|
|
3
|
-
require("./button.scss");
|
|
4
4
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
require("./button.scss");
|
|
5
6
|
// src/components/button/button.tsx
|
|
6
7
|
const styleKeys = [
|
|
7
8
|
"variant",
|
|
8
9
|
"size",
|
|
9
10
|
"fullWidth",
|
|
11
|
+
"loading",
|
|
10
12
|
"className"
|
|
11
13
|
];
|
|
12
|
-
const getClassName = ({ variant = "default", size = "md", fullWidth = false,
|
|
14
|
+
const getClassName = ({ variant = "default", size = "md", fullWidth = false, loading = false, className }) => require_cn.cn("button", className, require_cn.when(variant !== "default", `button--${variant}`), require_cn.when(size !== "md", `button--${size}`), require_cn.when(fullWidth, "button--full-width"), require_cn.when(loading, "button--loading"));
|
|
13
15
|
const omitStyleProps = (props) => {
|
|
14
16
|
const next = {};
|
|
15
17
|
const skipped = new Set(styleKeys);
|
|
16
18
|
for (const [key, value] of Object.entries(props)) if (!skipped.has(key)) next[key] = value;
|
|
17
19
|
return next;
|
|
18
20
|
};
|
|
21
|
+
const renderContent = (children, loading) => {
|
|
22
|
+
if (!loading) return children;
|
|
23
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24
|
+
className: "button__label",
|
|
25
|
+
children
|
|
26
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
27
|
+
className: "button__spinner",
|
|
28
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icon_spinner.IconSpinner, { className: "button__icon--spin" })
|
|
29
|
+
})] });
|
|
30
|
+
};
|
|
19
31
|
const Button = (0, react.forwardRef)(function Button(props, ref) {
|
|
32
|
+
const { loading } = props;
|
|
20
33
|
const className = getClassName(props);
|
|
21
34
|
const { children, href, ...domProps } = omitStyleProps(props);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
const content = renderContent(children, loading);
|
|
36
|
+
if (typeof href === "string") {
|
|
37
|
+
const anchorProps = domProps;
|
|
38
|
+
const handleClick = (event) => {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
};
|
|
41
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
42
|
+
...anchorProps,
|
|
43
|
+
ref,
|
|
44
|
+
className,
|
|
45
|
+
"aria-busy": loading || void 0,
|
|
46
|
+
"aria-disabled": loading || void 0,
|
|
47
|
+
onClick: loading ? handleClick : anchorProps.onClick,
|
|
48
|
+
href,
|
|
49
|
+
children: content
|
|
50
|
+
});
|
|
51
|
+
}
|
|
29
52
|
const { type = "button", disabled, onClick, ...buttonProps } = domProps;
|
|
30
|
-
const handleClick = (event) => {
|
|
31
|
-
if (disabled) return;
|
|
32
|
-
onClick?.(event);
|
|
33
|
-
};
|
|
34
53
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
54
|
+
...buttonProps,
|
|
35
55
|
ref,
|
|
36
56
|
className,
|
|
37
57
|
type,
|
|
38
|
-
disabled,
|
|
39
|
-
onClick
|
|
40
|
-
|
|
41
|
-
children
|
|
58
|
+
disabled: disabled || loading,
|
|
59
|
+
onClick,
|
|
60
|
+
"aria-busy": loading || void 0,
|
|
61
|
+
children: content
|
|
42
62
|
});
|
|
43
63
|
});
|
|
44
64
|
exports.Button = Button;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.cjs","names":["cn","when","forwardRef"],"sources":["../../../src/components/button/button.tsx"],"sourcesContent":["import {\n forwardRef,\n type AnchorHTMLAttributes,\n type ButtonHTMLAttributes,\n type MouseEventHandler,\n type ReactNode,\n type Ref,\n} from 'react';\n\nimport { cn, when } from '
|
|
1
|
+
{"version":3,"file":"button.cjs","names":["cn","when","IconSpinner","forwardRef"],"sources":["../../../src/components/button/button.tsx"],"sourcesContent":["import {\n forwardRef,\n type AnchorHTMLAttributes,\n type ButtonHTMLAttributes,\n type ForwardedRef,\n type MouseEventHandler,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefAttributes,\n} from 'react';\n\nimport { IconSpinner } from '@/icons/icon-spinner';\nimport { cn, when } from '@/lib/cn';\nimport './button.scss';\n\nexport type ButtonVariant = 'default' | 'outline' | 'link';\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\ninterface ButtonStyleProps {\n variant?: ButtonVariant;\n size?: ButtonSize;\n fullWidth?: boolean;\n loading?: boolean;\n className?: string;\n children?: ReactNode;\n}\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>, ButtonStyleProps {\n href?: undefined;\n}\n\nexport interface ButtonLinkProps\n extends\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>,\n ButtonStyleProps {\n href: string;\n}\n\nexport type ButtonComponentProps = ButtonProps | ButtonLinkProps;\n\ninterface ButtonComponent {\n (\n props: ButtonLinkProps & RefAttributes<HTMLAnchorElement>,\n ): ReactElement | null;\n (props: ButtonProps & RefAttributes<HTMLButtonElement>): ReactElement | null;\n}\n\nconst styleKeys = [\n 'variant',\n 'size',\n 'fullWidth',\n 'loading',\n 'className',\n] as const;\n\nconst getClassName = ({\n variant = 'default',\n size = 'md',\n fullWidth = false,\n loading = false,\n className,\n}: ButtonStyleProps) =>\n cn(\n 'button',\n className,\n when(variant !== 'default', `button--${variant}`),\n when(size !== 'md', `button--${size}`),\n when(fullWidth, 'button--full-width'),\n when(loading, 'button--loading'),\n );\n\nconst omitStyleProps = <T extends ButtonStyleProps>(\n props: T,\n): Omit<T, (typeof styleKeys)[number]> => {\n const next: Record<string, unknown> = {};\n const skipped = new Set<string>(styleKeys);\n\n for (const [key, value] of Object.entries(props)) {\n if (!skipped.has(key)) next[key] = value;\n }\n\n return next as Omit<T, (typeof styleKeys)[number]>;\n};\n\nconst renderContent = (children: ReactNode, loading?: boolean) => {\n if (!loading) return children;\n\n return (\n <>\n <span className=\"button__label\">{children}</span>\n <span className=\"button__spinner\">\n <IconSpinner className=\"button__icon--spin\" />\n </span>\n </>\n );\n};\n\n// forwardRef остаётся: в React 18 ref ещё не обычный проп, peer — ^18.3 || ^19.\nexport const Button = forwardRef(function Button(\n props: ButtonComponentProps,\n ref: ForwardedRef<HTMLButtonElement | HTMLAnchorElement>,\n) {\n const { loading } = props;\n const className = getClassName(props);\n const { children, href, ...domProps } = omitStyleProps(props);\n const content = renderContent(children, loading);\n\n if (typeof href === 'string') {\n const anchorProps = domProps as AnchorHTMLAttributes<HTMLAnchorElement>;\n const handleClick: MouseEventHandler<HTMLAnchorElement> = event => {\n event.preventDefault();\n };\n\n return (\n <a\n {...anchorProps}\n ref={ref as Ref<HTMLAnchorElement>}\n className={className}\n aria-busy={loading || undefined}\n // Ссылку нельзя выключить нативно: клик гасим, состояние объявляем.\n aria-disabled={loading || undefined}\n onClick={loading ? handleClick : anchorProps.onClick}\n href={href}\n >\n {content}\n </a>\n );\n }\n\n const {\n type = 'button',\n disabled,\n onClick,\n ...buttonProps\n } = domProps as ButtonHTMLAttributes<HTMLButtonElement>;\n\n return (\n <button\n {...buttonProps}\n ref={ref as Ref<HTMLButtonElement>}\n className={className}\n type={type}\n disabled={disabled || loading}\n onClick={onClick}\n aria-busy={loading || undefined}\n >\n {content}\n </button>\n );\n}) as ButtonComponent;\n"],"mappings":";;;;;;AAiDA,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,gBAAgB,EACpB,UAAU,WACV,OAAO,MACP,YAAY,OACZ,UAAU,OACV,gBAEAA,WAAAA,GACE,UACA,WACAC,WAAAA,KAAK,YAAY,WAAW,WAAW,SAAS,GAChDA,WAAAA,KAAK,SAAS,MAAM,WAAW,MAAM,GACrCA,WAAAA,KAAK,WAAW,oBAAoB,GACpCA,WAAAA,KAAK,SAAS,iBAAiB,CACjC;AAEF,MAAM,kBACJ,UACwC;CACxC,MAAM,OAAgC,CAAC;CACvC,MAAM,UAAU,IAAI,IAAY,SAAS;CAEzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,KAAK,OAAO;CAGrC,OAAO;AACT;AAEA,MAAM,iBAAiB,UAAqB,YAAsB;CAChE,IAAI,CAAC,SAAS,OAAO;CAErB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAA,kBAAA,UAAA,EAAA,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;EAAM,WAAU;EAAiB;CAAe,CAAA,GAChD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;EAAM,WAAU;EACd,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,qBAAAA,aAAD,EAAa,WAAU,qBAAsB,CAAA;CACzC,CAAA,CACN,EAAA,CAAA;AAEN;AAGA,MAAa,UAAA,GAASC,MAAAA,WAAAA,CAAW,SAAS,OACxC,OACA,KACA;CACA,MAAM,EAAE,YAAY;CACpB,MAAM,YAAY,aAAa,KAAK;CACpC,MAAM,EAAE,UAAU,MAAM,GAAG,aAAa,eAAe,KAAK;CAC5D,MAAM,UAAU,cAAc,UAAU,OAAO;CAE/C,IAAI,OAAO,SAAS,UAAU;EAC5B,MAAM,cAAc;EACpB,MAAM,eAAoD,UAAS;GACjE,MAAM,eAAe;EACvB;EAEA,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;GACE,GAAI;GACC;GACM;GACX,aAAW,WAAW,KAAA;GAEtB,iBAAe,WAAW,KAAA;GAC1B,SAAS,UAAU,cAAc,YAAY;GACvC;GAEL,UAAA;EACA,CAAA;CAEP;CAEA,MAAM,EACJ,OAAO,UACP,UACA,SACA,GAAG,gBACD;CAEJ,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;EACE,GAAI;EACC;EACM;EACL;EACN,UAAU,YAAY;EACb;EACT,aAAW,WAAW,KAAA;EAErB,UAAA;CACK,CAAA;AAEZ,CAAC"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
|
|
1
|
+
import { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactElement, ReactNode, RefAttributes } from "react";
|
|
2
2
|
export type ButtonVariant = 'default' | 'outline' | 'link';
|
|
3
3
|
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
4
4
|
interface ButtonStyleProps {
|
|
5
5
|
variant?: ButtonVariant;
|
|
6
6
|
size?: ButtonSize;
|
|
7
7
|
fullWidth?: boolean;
|
|
8
|
+
loading?: boolean;
|
|
8
9
|
className?: string;
|
|
9
10
|
children?: ReactNode;
|
|
10
11
|
}
|
|
@@ -15,6 +16,10 @@ export interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorEle
|
|
|
15
16
|
href: string;
|
|
16
17
|
}
|
|
17
18
|
export type ButtonComponentProps = ButtonProps | ButtonLinkProps;
|
|
18
|
-
|
|
19
|
+
interface ButtonComponent {
|
|
20
|
+
(props: ButtonLinkProps & RefAttributes<HTMLAnchorElement>): ReactElement | null;
|
|
21
|
+
(props: ButtonProps & RefAttributes<HTMLButtonElement>): ReactElement | null;
|
|
22
|
+
}
|
|
23
|
+
export declare const Button: ButtonComponent;
|
|
19
24
|
export {};
|
|
20
25
|
//# sourceMappingURL=button.d.cts.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
|
|
1
|
+
import { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactElement, ReactNode, RefAttributes } from "react";
|
|
2
2
|
export type ButtonVariant = 'default' | 'outline' | 'link';
|
|
3
3
|
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
4
4
|
interface ButtonStyleProps {
|
|
5
5
|
variant?: ButtonVariant;
|
|
6
6
|
size?: ButtonSize;
|
|
7
7
|
fullWidth?: boolean;
|
|
8
|
+
loading?: boolean;
|
|
8
9
|
className?: string;
|
|
9
10
|
children?: ReactNode;
|
|
10
11
|
}
|
|
@@ -15,6 +16,10 @@ export interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorEle
|
|
|
15
16
|
href: string;
|
|
16
17
|
}
|
|
17
18
|
export type ButtonComponentProps = ButtonProps | ButtonLinkProps;
|
|
18
|
-
|
|
19
|
+
interface ButtonComponent {
|
|
20
|
+
(props: ButtonLinkProps & RefAttributes<HTMLAnchorElement>): ReactElement | null;
|
|
21
|
+
(props: ButtonProps & RefAttributes<HTMLButtonElement>): ReactElement | null;
|
|
22
|
+
}
|
|
23
|
+
export declare const Button: ButtonComponent;
|
|
19
24
|
export {};
|
|
20
25
|
//# sourceMappingURL=button.d.ts.map
|
|
@@ -1,44 +1,64 @@
|
|
|
1
|
+
import { IconSpinner } from "../../icons/icon-spinner.js";
|
|
1
2
|
import { cn, when } from "../../lib/cn.js";
|
|
2
3
|
import { forwardRef } from "react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
5
|
import "./button.scss";
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
5
6
|
// src/components/button/button.tsx
|
|
6
7
|
const styleKeys = [
|
|
7
8
|
"variant",
|
|
8
9
|
"size",
|
|
9
10
|
"fullWidth",
|
|
11
|
+
"loading",
|
|
10
12
|
"className"
|
|
11
13
|
];
|
|
12
|
-
const getClassName = ({ variant = "default", size = "md", fullWidth = false,
|
|
14
|
+
const getClassName = ({ variant = "default", size = "md", fullWidth = false, loading = false, className }) => cn("button", className, when(variant !== "default", `button--${variant}`), when(size !== "md", `button--${size}`), when(fullWidth, "button--full-width"), when(loading, "button--loading"));
|
|
13
15
|
const omitStyleProps = (props) => {
|
|
14
16
|
const next = {};
|
|
15
17
|
const skipped = new Set(styleKeys);
|
|
16
18
|
for (const [key, value] of Object.entries(props)) if (!skipped.has(key)) next[key] = value;
|
|
17
19
|
return next;
|
|
18
20
|
};
|
|
21
|
+
const renderContent = (children, loading) => {
|
|
22
|
+
if (!loading) return children;
|
|
23
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
24
|
+
className: "button__label",
|
|
25
|
+
children
|
|
26
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
27
|
+
className: "button__spinner",
|
|
28
|
+
children: /* @__PURE__ */ jsx(IconSpinner, { className: "button__icon--spin" })
|
|
29
|
+
})] });
|
|
30
|
+
};
|
|
19
31
|
const Button = forwardRef(function Button(props, ref) {
|
|
32
|
+
const { loading } = props;
|
|
20
33
|
const className = getClassName(props);
|
|
21
34
|
const { children, href, ...domProps } = omitStyleProps(props);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
const content = renderContent(children, loading);
|
|
36
|
+
if (typeof href === "string") {
|
|
37
|
+
const anchorProps = domProps;
|
|
38
|
+
const handleClick = (event) => {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
};
|
|
41
|
+
return /* @__PURE__ */ jsx("a", {
|
|
42
|
+
...anchorProps,
|
|
43
|
+
ref,
|
|
44
|
+
className,
|
|
45
|
+
"aria-busy": loading || void 0,
|
|
46
|
+
"aria-disabled": loading || void 0,
|
|
47
|
+
onClick: loading ? handleClick : anchorProps.onClick,
|
|
48
|
+
href,
|
|
49
|
+
children: content
|
|
50
|
+
});
|
|
51
|
+
}
|
|
29
52
|
const { type = "button", disabled, onClick, ...buttonProps } = domProps;
|
|
30
|
-
const handleClick = (event) => {
|
|
31
|
-
if (disabled) return;
|
|
32
|
-
onClick?.(event);
|
|
33
|
-
};
|
|
34
53
|
return /* @__PURE__ */ jsx("button", {
|
|
54
|
+
...buttonProps,
|
|
35
55
|
ref,
|
|
36
56
|
className,
|
|
37
57
|
type,
|
|
38
|
-
disabled,
|
|
39
|
-
onClick
|
|
40
|
-
|
|
41
|
-
children
|
|
58
|
+
disabled: disabled || loading,
|
|
59
|
+
onClick,
|
|
60
|
+
"aria-busy": loading || void 0,
|
|
61
|
+
children: content
|
|
42
62
|
});
|
|
43
63
|
});
|
|
44
64
|
export { Button };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","names":[],"sources":["../../../src/components/button/button.tsx"],"sourcesContent":["import {\n forwardRef,\n type AnchorHTMLAttributes,\n type ButtonHTMLAttributes,\n type MouseEventHandler,\n type ReactNode,\n type Ref,\n} from 'react';\n\nimport { cn, when } from '
|
|
1
|
+
{"version":3,"file":"button.js","names":[],"sources":["../../../src/components/button/button.tsx"],"sourcesContent":["import {\n forwardRef,\n type AnchorHTMLAttributes,\n type ButtonHTMLAttributes,\n type ForwardedRef,\n type MouseEventHandler,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefAttributes,\n} from 'react';\n\nimport { IconSpinner } from '@/icons/icon-spinner';\nimport { cn, when } from '@/lib/cn';\nimport './button.scss';\n\nexport type ButtonVariant = 'default' | 'outline' | 'link';\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\ninterface ButtonStyleProps {\n variant?: ButtonVariant;\n size?: ButtonSize;\n fullWidth?: boolean;\n loading?: boolean;\n className?: string;\n children?: ReactNode;\n}\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>, ButtonStyleProps {\n href?: undefined;\n}\n\nexport interface ButtonLinkProps\n extends\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>,\n ButtonStyleProps {\n href: string;\n}\n\nexport type ButtonComponentProps = ButtonProps | ButtonLinkProps;\n\ninterface ButtonComponent {\n (\n props: ButtonLinkProps & RefAttributes<HTMLAnchorElement>,\n ): ReactElement | null;\n (props: ButtonProps & RefAttributes<HTMLButtonElement>): ReactElement | null;\n}\n\nconst styleKeys = [\n 'variant',\n 'size',\n 'fullWidth',\n 'loading',\n 'className',\n] as const;\n\nconst getClassName = ({\n variant = 'default',\n size = 'md',\n fullWidth = false,\n loading = false,\n className,\n}: ButtonStyleProps) =>\n cn(\n 'button',\n className,\n when(variant !== 'default', `button--${variant}`),\n when(size !== 'md', `button--${size}`),\n when(fullWidth, 'button--full-width'),\n when(loading, 'button--loading'),\n );\n\nconst omitStyleProps = <T extends ButtonStyleProps>(\n props: T,\n): Omit<T, (typeof styleKeys)[number]> => {\n const next: Record<string, unknown> = {};\n const skipped = new Set<string>(styleKeys);\n\n for (const [key, value] of Object.entries(props)) {\n if (!skipped.has(key)) next[key] = value;\n }\n\n return next as Omit<T, (typeof styleKeys)[number]>;\n};\n\nconst renderContent = (children: ReactNode, loading?: boolean) => {\n if (!loading) return children;\n\n return (\n <>\n <span className=\"button__label\">{children}</span>\n <span className=\"button__spinner\">\n <IconSpinner className=\"button__icon--spin\" />\n </span>\n </>\n );\n};\n\n// forwardRef остаётся: в React 18 ref ещё не обычный проп, peer — ^18.3 || ^19.\nexport const Button = forwardRef(function Button(\n props: ButtonComponentProps,\n ref: ForwardedRef<HTMLButtonElement | HTMLAnchorElement>,\n) {\n const { loading } = props;\n const className = getClassName(props);\n const { children, href, ...domProps } = omitStyleProps(props);\n const content = renderContent(children, loading);\n\n if (typeof href === 'string') {\n const anchorProps = domProps as AnchorHTMLAttributes<HTMLAnchorElement>;\n const handleClick: MouseEventHandler<HTMLAnchorElement> = event => {\n event.preventDefault();\n };\n\n return (\n <a\n {...anchorProps}\n ref={ref as Ref<HTMLAnchorElement>}\n className={className}\n aria-busy={loading || undefined}\n // Ссылку нельзя выключить нативно: клик гасим, состояние объявляем.\n aria-disabled={loading || undefined}\n onClick={loading ? handleClick : anchorProps.onClick}\n href={href}\n >\n {content}\n </a>\n );\n }\n\n const {\n type = 'button',\n disabled,\n onClick,\n ...buttonProps\n } = domProps as ButtonHTMLAttributes<HTMLButtonElement>;\n\n return (\n <button\n {...buttonProps}\n ref={ref as Ref<HTMLButtonElement>}\n className={className}\n type={type}\n disabled={disabled || loading}\n onClick={onClick}\n aria-busy={loading || undefined}\n >\n {content}\n </button>\n );\n}) as ButtonComponent;\n"],"mappings":";;;;;;AAiDA,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,gBAAgB,EACpB,UAAU,WACV,OAAO,MACP,YAAY,OACZ,UAAU,OACV,gBAEA,GACE,UACA,WACA,KAAK,YAAY,WAAW,WAAW,SAAS,GAChD,KAAK,SAAS,MAAM,WAAW,MAAM,GACrC,KAAK,WAAW,oBAAoB,GACpC,KAAK,SAAS,iBAAiB,CACjC;AAEF,MAAM,kBACJ,UACwC;CACxC,MAAM,OAAgC,CAAC;CACvC,MAAM,UAAU,IAAI,IAAY,SAAS;CAEzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,KAAK,OAAO;CAGrC,OAAO;AACT;AAEA,MAAM,iBAAiB,UAAqB,YAAsB;CAChE,IAAI,CAAC,SAAS,OAAO;CAErB,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EAAM,WAAU;EAAiB;CAAe,CAAA,GAChD,oBAAC,QAAD;EAAM,WAAU;EACd,UAAA,oBAAC,aAAD,EAAa,WAAU,qBAAsB,CAAA;CACzC,CAAA,CACN,EAAA,CAAA;AAEN;AAGA,MAAa,SAAS,WAAW,SAAS,OACxC,OACA,KACA;CACA,MAAM,EAAE,YAAY;CACpB,MAAM,YAAY,aAAa,KAAK;CACpC,MAAM,EAAE,UAAU,MAAM,GAAG,aAAa,eAAe,KAAK;CAC5D,MAAM,UAAU,cAAc,UAAU,OAAO;CAE/C,IAAI,OAAO,SAAS,UAAU;EAC5B,MAAM,cAAc;EACpB,MAAM,eAAoD,UAAS;GACjE,MAAM,eAAe;EACvB;EAEA,OACE,oBAAC,KAAD;GACE,GAAI;GACC;GACM;GACX,aAAW,WAAW,KAAA;GAEtB,iBAAe,WAAW,KAAA;GAC1B,SAAS,UAAU,cAAc,YAAY;GACvC;GAEL,UAAA;EACA,CAAA;CAEP;CAEA,MAAM,EACJ,OAAO,UACP,UACA,SACA,GAAG,gBACD;CAEJ,OACE,oBAAC,UAAD;EACE,GAAI;EACC;EACM;EACL;EACN,UAAU,YAAY;EACb;EACT,aAAW,WAAW,KAAA;EAErB,UAAA;CACK,CAAA;AAEZ,CAAC"}
|
|
@@ -3,44 +3,66 @@
|
|
|
3
3
|
width: fit-content;
|
|
4
4
|
display: flex;
|
|
5
5
|
align-items: center;
|
|
6
|
-
padding:
|
|
6
|
+
padding: 8px 18px;
|
|
7
7
|
border-radius: 30px;
|
|
8
|
-
font-size:
|
|
9
|
-
line-height:
|
|
10
|
-
background-color: var(--color-
|
|
11
|
-
color: var(--color-
|
|
12
|
-
fill: var(--color-
|
|
8
|
+
font-size: var(--text-sm);
|
|
9
|
+
line-height: var(--text-sm--line-height);
|
|
10
|
+
background-color: var(--color-primary);
|
|
11
|
+
color: var(--color-primary-foreground);
|
|
12
|
+
fill: var(--color-primary-foreground);
|
|
13
13
|
font-weight: bold;
|
|
14
14
|
font-family: var(--font-sans);
|
|
15
|
-
border: 2px solid var(--color-
|
|
15
|
+
border: 2px solid var(--color-primary);
|
|
16
16
|
flex-shrink: 0;
|
|
17
17
|
cursor: pointer;
|
|
18
18
|
transition: none;
|
|
19
|
-
gap:
|
|
19
|
+
gap: 8px;
|
|
20
20
|
text-decoration: none;
|
|
21
21
|
|
|
22
|
+
&:hover:not(:active):not(:disabled) {
|
|
23
|
+
background-color: var(--color-primary-hover);
|
|
24
|
+
border-color: var(--color-primary-hover);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&:focus:not(:focus-visible) {
|
|
28
|
+
outline: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&:focus-visible {
|
|
32
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
33
|
+
outline-offset: var(--focus-ring-offset);
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
&:active {
|
|
23
|
-
background-color: var(--color-
|
|
24
|
-
color: var(--color-
|
|
37
|
+
background-color: var(--color-muted);
|
|
38
|
+
color: var(--color-muted-foreground);
|
|
25
39
|
fill: currentColor;
|
|
26
|
-
border-color: var(--color-
|
|
40
|
+
border-color: var(--color-muted);
|
|
27
41
|
}
|
|
28
42
|
|
|
43
|
+
// pointer-events не гасим: нативный disabled и так не пускает клик, а так
|
|
44
|
+
// на кнопку можно навести тултип «почему недоступна».
|
|
29
45
|
&:disabled {
|
|
30
|
-
background-color: var(--color-
|
|
31
|
-
color: var(--color-
|
|
46
|
+
background-color: var(--color-muted);
|
|
47
|
+
color: var(--color-muted-foreground);
|
|
32
48
|
fill: currentColor;
|
|
33
|
-
border-color: var(--color-
|
|
49
|
+
border-color: var(--color-muted);
|
|
34
50
|
cursor: default;
|
|
35
|
-
pointer-events: none;
|
|
36
51
|
user-select: none;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
&--outline {
|
|
40
55
|
background-color: transparent;
|
|
41
|
-
color: var(--color-
|
|
56
|
+
color: var(--color-foreground);
|
|
42
57
|
fill: currentColor;
|
|
43
|
-
border: 2px solid var(--color-
|
|
58
|
+
border: 2px solid var(--color-foreground);
|
|
59
|
+
|
|
60
|
+
&:hover:not(:active):not(:disabled) {
|
|
61
|
+
background-color: var(--color-secondary);
|
|
62
|
+
border-color: var(--color-foreground);
|
|
63
|
+
color: var(--color-foreground);
|
|
64
|
+
fill: currentColor;
|
|
65
|
+
}
|
|
44
66
|
|
|
45
67
|
&:disabled {
|
|
46
68
|
background-color: transparent;
|
|
@@ -50,35 +72,51 @@
|
|
|
50
72
|
&--link {
|
|
51
73
|
border: none;
|
|
52
74
|
background-color: transparent;
|
|
53
|
-
color: var(--color-
|
|
75
|
+
color: var(--color-foreground);
|
|
54
76
|
fill: currentColor;
|
|
55
77
|
border-radius: 0;
|
|
56
78
|
padding: 0;
|
|
57
79
|
|
|
80
|
+
&:hover:not(:active):not(:disabled) {
|
|
81
|
+
background-color: transparent;
|
|
82
|
+
text-decoration: underline;
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
&:active,
|
|
59
86
|
&:disabled {
|
|
60
87
|
background-color: transparent;
|
|
61
88
|
}
|
|
62
89
|
}
|
|
63
90
|
|
|
64
|
-
&--
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
&--loading {
|
|
92
|
+
position: relative;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&__label {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: inherit;
|
|
99
|
+
opacity: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&__spinner {
|
|
103
|
+
position: absolute;
|
|
104
|
+
inset: 0;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
70
108
|
pointer-events: none;
|
|
71
|
-
user-select: none;
|
|
72
109
|
}
|
|
73
110
|
|
|
111
|
+
// 12/16 и 16/24 у sm и lg сняты со старого макета и в шкалу --text-* не попадают.
|
|
74
112
|
&--sm {
|
|
75
|
-
padding:
|
|
113
|
+
padding: 5px 12px;
|
|
76
114
|
font-size: 12px;
|
|
77
115
|
line-height: 16px;
|
|
78
116
|
}
|
|
79
117
|
|
|
80
118
|
&--lg {
|
|
81
|
-
padding:
|
|
119
|
+
padding: 12px 24px;
|
|
82
120
|
font-size: 16px;
|
|
83
121
|
line-height: 24px;
|
|
84
122
|
}
|
|
@@ -1,24 +1,66 @@
|
|
|
1
1
|
const require_cn = require("../../lib/cn.cjs");
|
|
2
|
+
const require_field_context = require("../field/field-context.cjs");
|
|
2
3
|
let react = require("react");
|
|
3
4
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
5
|
require("./checkbox.scss");
|
|
5
6
|
// src/components/checkbox/checkbox.tsx
|
|
6
|
-
const
|
|
7
|
+
const assignRef = (ref, node) => {
|
|
8
|
+
if (typeof ref === "function") {
|
|
9
|
+
ref(node);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (ref) ref.current = node;
|
|
13
|
+
};
|
|
14
|
+
const mergeDescribedBy = (fromField, fromProps) => {
|
|
15
|
+
return [fromField, fromProps].filter((part) => Boolean(part)).join(" ") || void 0;
|
|
16
|
+
};
|
|
17
|
+
const CheckboxInput = (0, react.forwardRef)(function CheckboxInput({ className, id, label, error, disabled, clickable = true, indeterminate = false, checked, onChange, ...props }, ref) {
|
|
18
|
+
const field = require_field_context.useFieldControl();
|
|
19
|
+
const reportValue = field?.reportValue;
|
|
7
20
|
const autoId = (0, react.useId)();
|
|
8
|
-
const inputId = id ?? autoId;
|
|
21
|
+
const inputId = field?.id ?? id ?? autoId;
|
|
22
|
+
const inputRef = (0, react.useRef)(null);
|
|
23
|
+
const isInvalid = Boolean(error) || Boolean(field?.invalid);
|
|
24
|
+
const setRef = (0, react.useCallback)((node) => {
|
|
25
|
+
inputRef.current = node;
|
|
26
|
+
assignRef(ref, node);
|
|
27
|
+
}, [ref]);
|
|
28
|
+
(0, react.useEffect)(() => {
|
|
29
|
+
if (inputRef.current) inputRef.current.indeterminate = indeterminate;
|
|
30
|
+
}, [indeterminate]);
|
|
31
|
+
(0, react.useEffect)(() => {
|
|
32
|
+
const node = inputRef.current;
|
|
33
|
+
if (node) reportValue?.(node.checked ? "on" : "");
|
|
34
|
+
}, [checked, reportValue]);
|
|
35
|
+
const handleChange = (event) => {
|
|
36
|
+
if (checked === void 0) reportValue?.(event.target.checked ? "on" : "");
|
|
37
|
+
onChange?.(event);
|
|
38
|
+
};
|
|
39
|
+
const describedBy = field ? mergeDescribedBy(field.describedBy, props["aria-describedby"]) : void 0;
|
|
40
|
+
const fieldAttrs = {};
|
|
41
|
+
if (field?.required) {
|
|
42
|
+
fieldAttrs.required = true;
|
|
43
|
+
fieldAttrs["aria-required"] = true;
|
|
44
|
+
}
|
|
45
|
+
if (isInvalid) fieldAttrs["aria-invalid"] = true;
|
|
46
|
+
if (describedBy) fieldAttrs["aria-describedby"] = describedBy;
|
|
47
|
+
if (field?.errorId) fieldAttrs["aria-errormessage"] = field.errorId;
|
|
9
48
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10
|
-
className: require_cn.cn("checkbox", className
|
|
49
|
+
className: require_cn.cn("checkbox", className),
|
|
11
50
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
12
|
-
|
|
51
|
+
...props,
|
|
52
|
+
ref: setRef,
|
|
13
53
|
type: "checkbox",
|
|
14
54
|
id: inputId,
|
|
15
55
|
disabled,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
56
|
+
...checked !== void 0 ? { checked } : {},
|
|
57
|
+
onChange: handleChange,
|
|
58
|
+
className: require_cn.cn("checkbox__input", require_cn.when(!clickable, "checkbox__input--no-events")),
|
|
59
|
+
...fieldAttrs
|
|
60
|
+
}), label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
19
61
|
className: "checkbox__label",
|
|
20
62
|
htmlFor: inputId,
|
|
21
|
-
children:
|
|
63
|
+
children: label
|
|
22
64
|
}) : null]
|
|
23
65
|
});
|
|
24
66
|
});
|