@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.
Files changed (72) hide show
  1. package/README.md +59 -17
  2. package/dist/components/button/button.cjs +37 -17
  3. package/dist/components/button/button.cjs.map +1 -1
  4. package/dist/components/button/button.d.cts +7 -2
  5. package/dist/components/button/button.d.ts +7 -2
  6. package/dist/components/button/button.js +37 -17
  7. package/dist/components/button/button.js.map +1 -1
  8. package/dist/components/button/button.scss +65 -27
  9. package/dist/components/checkbox/checkbox.cjs +50 -8
  10. package/dist/components/checkbox/checkbox.cjs.map +1 -1
  11. package/dist/components/checkbox/checkbox.d.cts +11 -4
  12. package/dist/components/checkbox/checkbox.d.ts +11 -4
  13. package/dist/components/checkbox/checkbox.js +51 -9
  14. package/dist/components/checkbox/checkbox.js.map +1 -1
  15. package/dist/components/checkbox/checkbox.scss +55 -26
  16. package/dist/components/field/field-context.cjs +9 -0
  17. package/dist/components/field/field-context.cjs.map +1 -0
  18. package/dist/components/field/field-context.js +8 -0
  19. package/dist/components/field/field-context.js.map +1 -0
  20. package/dist/components/field/field.cjs +105 -0
  21. package/dist/components/field/field.cjs.map +1 -0
  22. package/dist/components/field/field.d.cts +24 -0
  23. package/dist/components/field/field.d.ts +24 -0
  24. package/dist/components/field/field.js +105 -0
  25. package/dist/components/field/field.js.map +1 -0
  26. package/dist/components/field/field.scss +43 -0
  27. package/dist/components/field/index.d.cts +2 -0
  28. package/dist/components/field/index.d.ts +2 -0
  29. package/dist/components/input/index.d.cts +2 -2
  30. package/dist/components/input/index.d.ts +2 -2
  31. package/dist/components/input/input.cjs +88 -8
  32. package/dist/components/input/input.cjs.map +1 -1
  33. package/dist/components/input/input.d.cts +8 -1
  34. package/dist/components/input/input.d.ts +8 -1
  35. package/dist/components/input/input.js +90 -10
  36. package/dist/components/input/input.js.map +1 -1
  37. package/dist/components/input/input.scss +77 -14
  38. package/dist/components/radio/index.d.cts +2 -2
  39. package/dist/components/radio/index.d.ts +2 -2
  40. package/dist/components/radio/radio.cjs +47 -5
  41. package/dist/components/radio/radio.cjs.map +1 -1
  42. package/dist/components/radio/radio.d.cts +12 -2
  43. package/dist/components/radio/radio.d.ts +12 -2
  44. package/dist/components/radio/radio.js +48 -7
  45. package/dist/components/radio/radio.js.map +1 -1
  46. package/dist/components/radio/radio.scss +50 -15
  47. package/dist/components/spinner/spinner.cjs +38 -16
  48. package/dist/components/spinner/spinner.cjs.map +1 -1
  49. package/dist/components/spinner/spinner.d.cts +5 -3
  50. package/dist/components/spinner/spinner.d.ts +5 -3
  51. package/dist/components/spinner/spinner.js +39 -17
  52. package/dist/components/spinner/spinner.js.map +1 -1
  53. package/dist/components/spinner/spinner.scss +18 -1
  54. package/dist/components/textarea/index.d.cts +2 -2
  55. package/dist/components/textarea/index.d.ts +2 -2
  56. package/dist/components/textarea/textarea.cjs +68 -9
  57. package/dist/components/textarea/textarea.cjs.map +1 -1
  58. package/dist/components/textarea/textarea.d.cts +4 -2
  59. package/dist/components/textarea/textarea.d.ts +4 -2
  60. package/dist/components/textarea/textarea.js +69 -10
  61. package/dist/components/textarea/textarea.js.map +1 -1
  62. package/dist/components/textarea/textarea.scss +50 -23
  63. package/dist/icons/index.cjs +2 -2
  64. package/dist/icons/index.js +2 -2
  65. package/dist/index.cjs +3 -0
  66. package/dist/index.d.cts +6 -4
  67. package/dist/index.d.ts +6 -4
  68. package/dist/index.js +3 -2
  69. package/dist/styles/global.scss +33 -27
  70. package/dist/styles/mixins.scss +37 -174
  71. package/dist/styles/vars.scss +73 -22
  72. 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
- `var(--color-black)`, `var(--font-sans)`, `var(--radius-sm)`, `var(--breakpoint-md)`. Имена как у Tailwind, значения splittest.
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`, слева или справа, какой набор нужен. Класс `button__icon--spin` крутит спиннер.
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} hideCounter />
87
- <CheckboxInput title="18-24" />
88
- <RadioInput name="age" title="18–24" />
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
- `hideCounter` заменяет опечатку `noMaxMoty`. `CheckboxInput` сам ставит `id`, если его не передали. Дефис в `title` чекбокса по-прежнему меняется на тире.
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, className, disabled }) => 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(disabled, "button--disabled"));
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
- if (typeof href === "string") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
23
- ref,
24
- href,
25
- className,
26
- ...domProps,
27
- children
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: handleClick,
40
- ...buttonProps,
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 '../../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 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\nconst styleKeys = ['variant', 'size', 'fullWidth', 'className'] as const;\n\nconst getClassName = ({\n variant = 'default',\n size = 'md',\n fullWidth = false,\n className,\n disabled,\n}: ButtonStyleProps & { disabled?: boolean }) =>\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(disabled, 'button--disabled'),\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\nexport const Button = forwardRef<\n HTMLButtonElement | HTMLAnchorElement,\n ButtonComponentProps\n>(function Button(props, ref) {\n const className = getClassName(props);\n const { children, href, ...domProps } = omitStyleProps(props);\n\n if (typeof href === 'string') {\n return (\n <a\n ref={ref as Ref<HTMLAnchorElement>}\n href={href}\n className={className}\n {...(domProps as AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n {children}\n </a>\n );\n }\n\n const {\n type = 'button',\n disabled,\n onClick,\n ...buttonProps\n } = domProps as ButtonHTMLAttributes<HTMLButtonElement>;\n\n const handleClick: MouseEventHandler<HTMLButtonElement> = event => {\n if (disabled) return;\n onClick?.(event);\n };\n\n return (\n <button\n ref={ref as Ref<HTMLButtonElement>}\n className={className}\n type={type}\n disabled={disabled}\n onClick={handleClick}\n {...buttonProps}\n >\n {children}\n </button>\n );\n});\n"],"mappings":";;;;;AAqCA,MAAM,YAAY;CAAC;CAAW;CAAQ;CAAa;AAAW;AAE9D,MAAM,gBAAgB,EACpB,UAAU,WACV,OAAO,MACP,YAAY,OACZ,WACA,eAEAA,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,UAAU,kBAAkB,CACnC;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,MAAa,UAAA,GAASC,MAAAA,WAAAA,CAGpB,SAAS,OAAO,OAAO,KAAK;CAC5B,MAAM,YAAY,aAAa,KAAK;CACpC,MAAM,EAAE,UAAU,MAAM,GAAG,aAAa,eAAe,KAAK;CAE5D,IAAI,OAAO,SAAS,UAClB,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;EACO;EACC;EACK;EACX,GAAK;EAEJ;CACA,CAAA;CAIP,MAAM,EACJ,OAAO,UACP,UACA,SACA,GAAG,gBACD;CAEJ,MAAM,eAAoD,UAAS;EACjE,IAAI,UAAU;EACd,UAAU,KAAK;CACjB;CAEA,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;EACO;EACM;EACL;EACI;EACV,SAAS;EACT,GAAI;EAEH;CACK,CAAA;AAEZ,CAAC"}
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
- export declare const Button: import("react").ForwardRefExoticComponent<ButtonComponentProps & import("react").RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
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
- export declare const Button: import("react").ForwardRefExoticComponent<ButtonComponentProps & import("react").RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
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, className, disabled }) => cn("button", className, when(variant !== "default", `button--${variant}`), when(size !== "md", `button--${size}`), when(fullWidth, "button--full-width"), when(disabled, "button--disabled"));
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
- if (typeof href === "string") return /* @__PURE__ */ jsx("a", {
23
- ref,
24
- href,
25
- className,
26
- ...domProps,
27
- children
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: handleClick,
40
- ...buttonProps,
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 '../../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 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\nconst styleKeys = ['variant', 'size', 'fullWidth', 'className'] as const;\n\nconst getClassName = ({\n variant = 'default',\n size = 'md',\n fullWidth = false,\n className,\n disabled,\n}: ButtonStyleProps & { disabled?: boolean }) =>\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(disabled, 'button--disabled'),\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\nexport const Button = forwardRef<\n HTMLButtonElement | HTMLAnchorElement,\n ButtonComponentProps\n>(function Button(props, ref) {\n const className = getClassName(props);\n const { children, href, ...domProps } = omitStyleProps(props);\n\n if (typeof href === 'string') {\n return (\n <a\n ref={ref as Ref<HTMLAnchorElement>}\n href={href}\n className={className}\n {...(domProps as AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n {children}\n </a>\n );\n }\n\n const {\n type = 'button',\n disabled,\n onClick,\n ...buttonProps\n } = domProps as ButtonHTMLAttributes<HTMLButtonElement>;\n\n const handleClick: MouseEventHandler<HTMLButtonElement> = event => {\n if (disabled) return;\n onClick?.(event);\n };\n\n return (\n <button\n ref={ref as Ref<HTMLButtonElement>}\n className={className}\n type={type}\n disabled={disabled}\n onClick={handleClick}\n {...buttonProps}\n >\n {children}\n </button>\n );\n});\n"],"mappings":";;;;;AAqCA,MAAM,YAAY;CAAC;CAAW;CAAQ;CAAa;AAAW;AAE9D,MAAM,gBAAgB,EACpB,UAAU,WACV,OAAO,MACP,YAAY,OACZ,WACA,eAEA,GACE,UACA,WACA,KAAK,YAAY,WAAW,WAAW,SAAS,GAChD,KAAK,SAAS,MAAM,WAAW,MAAM,GACrC,KAAK,WAAW,oBAAoB,GACpC,KAAK,UAAU,kBAAkB,CACnC;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,MAAa,SAAS,WAGpB,SAAS,OAAO,OAAO,KAAK;CAC5B,MAAM,YAAY,aAAa,KAAK;CACpC,MAAM,EAAE,UAAU,MAAM,GAAG,aAAa,eAAe,KAAK;CAE5D,IAAI,OAAO,SAAS,UAClB,OACE,oBAAC,KAAD;EACO;EACC;EACK;EACX,GAAK;EAEJ;CACA,CAAA;CAIP,MAAM,EACJ,OAAO,UACP,UACA,SACA,GAAG,gBACD;CAEJ,MAAM,eAAoD,UAAS;EACjE,IAAI,UAAU;EACd,UAAU,KAAK;CACjB;CAEA,OACE,oBAAC,UAAD;EACO;EACM;EACL;EACI;EACV,SAAS;EACT,GAAI;EAEH;CACK,CAAA;AAEZ,CAAC"}
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: 10px 20px;
6
+ padding: 8px 18px;
7
7
  border-radius: 30px;
8
- font-size: 14px;
9
- line-height: 20px;
10
- background-color: var(--color-black);
11
- color: var(--color-white);
12
- fill: var(--color-white);
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-black);
15
+ border: 2px solid var(--color-primary);
16
16
  flex-shrink: 0;
17
17
  cursor: pointer;
18
18
  transition: none;
19
- gap: 5px;
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-neutral-200);
24
- color: var(--color-neutral-500);
37
+ background-color: var(--color-muted);
38
+ color: var(--color-muted-foreground);
25
39
  fill: currentColor;
26
- border-color: var(--color-neutral-200);
40
+ border-color: var(--color-muted);
27
41
  }
28
42
 
43
+ // pointer-events не гасим: нативный disabled и так не пускает клик, а так
44
+ // на кнопку можно навести тултип «почему недоступна».
29
45
  &:disabled {
30
- background-color: var(--color-neutral-200);
31
- color: var(--color-neutral-500);
46
+ background-color: var(--color-muted);
47
+ color: var(--color-muted-foreground);
32
48
  fill: currentColor;
33
- border-color: var(--color-neutral-200);
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-black);
56
+ color: var(--color-foreground);
42
57
  fill: currentColor;
43
- border: 2px solid var(--color-black);
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-black);
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
- &--disabled {
65
- background-color: var(--color-neutral-200);
66
- color: var(--color-neutral-500);
67
- fill: var(--color-neutral-500);
68
- border-color: var(--color-neutral-200);
69
- cursor: default;
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: 6px 12px;
113
+ padding: 5px 12px;
76
114
  font-size: 12px;
77
115
  line-height: 16px;
78
116
  }
79
117
 
80
118
  &--lg {
81
- padding: 14px 24px;
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 CheckboxInput = (0, react.forwardRef)(function CheckboxInput({ className, id, title, hide, disabled, withCursor = true, ...props }, ref) {
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, require_cn.when(hide, "checkbox--hide")),
49
+ className: require_cn.cn("checkbox", className),
11
50
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
12
- ref,
51
+ ...props,
52
+ ref: setRef,
13
53
  type: "checkbox",
14
54
  id: inputId,
15
55
  disabled,
16
- className: require_cn.cn("checkbox__input", require_cn.when(!withCursor, "checkbox__input--no-events")),
17
- ...props
18
- }), title ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
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: title.replaceAll("-", "–")
63
+ children: label
22
64
  }) : null]
23
65
  });
24
66
  });