@de-kit/st-ui 0.1.4 → 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 (80) 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 +66 -28
  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/icon-pause.cjs +14 -0
  64. package/dist/icons/icon-pause.cjs.map +1 -0
  65. package/dist/icons/icon-pause.d.cts +3 -0
  66. package/dist/icons/icon-pause.d.ts +3 -0
  67. package/dist/icons/icon-pause.js +14 -0
  68. package/dist/icons/icon-pause.js.map +1 -0
  69. package/dist/icons/index.cjs +4 -2
  70. package/dist/icons/index.d.cts +2 -1
  71. package/dist/icons/index.d.ts +2 -1
  72. package/dist/icons/index.js +4 -3
  73. package/dist/index.cjs +3 -0
  74. package/dist/index.d.cts +6 -4
  75. package/dist/index.d.ts +6 -4
  76. package/dist/index.js +3 -2
  77. package/dist/styles/global.scss +33 -27
  78. package/dist/styles/mixins.scss +37 -172
  79. package/dist/styles/vars.scss +73 -22
  80. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ .field {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ }
6
+
7
+ .field__label {
8
+ color: var(--color-foreground);
9
+ font-size: var(--text-lg);
10
+ font-weight: 900;
11
+ line-height: var(--text-lg--line-height);
12
+ display: block;
13
+ margin-bottom: var(--space-3);
14
+ overflow-wrap: anywhere;
15
+ -webkit-hyphens: auto;
16
+ hyphens: auto;
17
+ }
18
+
19
+ .field__hint,
20
+ .field__error {
21
+ margin: var(--space-2) 0 0 0;
22
+ font-size: var(--text-xs);
23
+ line-height: var(--text-xs--line-height);
24
+ }
25
+
26
+ .field__hint {
27
+ color: var(--color-muted-foreground);
28
+ }
29
+
30
+ .field--hint-label .field__label {
31
+ margin-bottom: var(--space-2);
32
+ }
33
+
34
+ .field__hint--label {
35
+ margin: 0 0 var(--space-3);
36
+ font-size: var(--text-sm);
37
+ line-height: var(--text-sm--line-height);
38
+ color: var(--color-foreground);
39
+ }
40
+
41
+ .field__error {
42
+ color: var(--color-destructive);
43
+ }
@@ -0,0 +1,2 @@
1
+ import { Field, FieldHintPlacement, FieldProps } from "./field.cjs";
2
+ export { Field, type FieldHintPlacement, type FieldProps };
@@ -0,0 +1,2 @@
1
+ import { Field, FieldHintPlacement, FieldProps } from "./field.js";
2
+ export { Field, type FieldHintPlacement, type FieldProps };
@@ -1,2 +1,2 @@
1
- import { Input, InputProps } from "./input.cjs";
2
- export { Input, type InputProps };
1
+ import { Input, InputProps, InputSize } from "./input.cjs";
2
+ export { Input, type InputProps, type InputSize };
@@ -1,2 +1,2 @@
1
- import { Input, InputProps } from "./input.js";
2
- export { Input, type InputProps };
1
+ import { Input, InputProps, InputSize } from "./input.js";
2
+ export { Input, type InputProps, type InputSize };
@@ -1,16 +1,96 @@
1
1
  const require_cn = require("../../lib/cn.cjs");
2
+ const require_field_context = require("../field/field-context.cjs");
3
+ const require_icon_cross = require("../../icons/icon-cross.cjs");
2
4
  let react = require("react");
3
5
  let react_jsx_runtime = require("react/jsx-runtime");
4
6
  require("./input.scss");
5
7
  // src/components/input/input.tsx
6
- const Input = (0, react.forwardRef)(function Input({ type = "text", recommended, error, className, disabled, ...props }, ref) {
7
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8
- ref,
9
- type,
10
- disabled,
11
- "aria-invalid": error || void 0,
12
- className: require_cn.cn("input", className, require_cn.when(recommended, "input--recommended"), require_cn.when(error, "input--error")),
13
- ...props
8
+ const assignRef = (ref, node) => {
9
+ if (typeof ref === "function") {
10
+ ref(node);
11
+ return;
12
+ }
13
+ if (ref) ref.current = node;
14
+ };
15
+ const toInputValue = (value) => value == null ? "" : String(value);
16
+ /**
17
+ * Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.
18
+ * Сеттер прототипа трекер не трогает.
19
+ */
20
+ const emitInputValue = (input, next) => {
21
+ (Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set)?.call(input, next);
22
+ input.dispatchEvent(new Event("input", { bubbles: true }));
23
+ };
24
+ const Input = (0, react.forwardRef)(function Input({ type = "text", size = "md", recommended, error, clearable = false, clearLabel = "Очистить", onClear, className, disabled, readOnly, value, defaultValue, onChange, ...props }, ref) {
25
+ const field = require_field_context.useFieldControl();
26
+ const reportValue = field?.reportValue;
27
+ const inputRef = (0, react.useRef)(null);
28
+ const isControlled = value !== void 0;
29
+ const [uncontrolledValue, setUncontrolledValue] = (0, react.useState)(() => toInputValue(defaultValue));
30
+ const current = isControlled ? toInputValue(value) : uncontrolledValue;
31
+ const isInvalid = Boolean(error) || Boolean(field?.invalid);
32
+ const describedBy = [field?.describedBy, props["aria-describedby"]].filter((part) => Boolean(part)).join(" ");
33
+ const setRef = (0, react.useCallback)((node) => {
34
+ inputRef.current = node;
35
+ assignRef(ref, node);
36
+ }, [ref]);
37
+ (0, react.useEffect)(() => {
38
+ const form = inputRef.current?.form;
39
+ if (!form || isControlled) return;
40
+ const handleReset = () => {
41
+ setUncontrolledValue(toInputValue(defaultValue));
42
+ };
43
+ form.addEventListener("reset", handleReset);
44
+ return () => form.removeEventListener("reset", handleReset);
45
+ }, [isControlled, defaultValue]);
46
+ (0, react.useEffect)(() => {
47
+ reportValue?.(current);
48
+ }, [current, reportValue]);
49
+ const handleChange = (event) => {
50
+ if (!isControlled) setUncontrolledValue(event.target.value);
51
+ onChange?.(event);
52
+ };
53
+ const handleClearMouseDown = (event) => {
54
+ event.preventDefault();
55
+ };
56
+ const handleClear = () => {
57
+ const input = inputRef.current;
58
+ if (!input) return;
59
+ emitInputValue(input, "");
60
+ onClear?.();
61
+ input.focus();
62
+ };
63
+ const canClear = clearable && !disabled && !readOnly && current !== "";
64
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65
+ className: "input__wrap",
66
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
67
+ ...props,
68
+ ref: setRef,
69
+ type,
70
+ disabled,
71
+ readOnly,
72
+ ...isControlled ? { value } : { defaultValue },
73
+ onChange: handleChange,
74
+ ...isInvalid ? { "aria-invalid": true } : {},
75
+ ...field ? { id: field.id } : {},
76
+ ...field?.required ? {
77
+ "required": true,
78
+ "aria-required": true
79
+ } : {},
80
+ ...describedBy ? { "aria-describedby": describedBy } : {},
81
+ ...field?.errorId ? { "aria-errormessage": field.errorId } : {},
82
+ className: require_cn.cn("input", className, require_cn.when(size !== "md", `input--${size}`), require_cn.when(recommended, "input--recommended"), require_cn.when(isInvalid, "input--error"), require_cn.when(clearable && !disabled && !readOnly, "input--clearable"))
83
+ }), canClear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
84
+ type: "button",
85
+ className: "input__clear",
86
+ "aria-label": clearLabel,
87
+ onMouseDown: handleClearMouseDown,
88
+ onClick: handleClear,
89
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icon_cross.IconCross, {
90
+ width: 16,
91
+ height: 16
92
+ })
93
+ }) : null]
14
94
  });
15
95
  });
16
96
  exports.Input = Input;
@@ -1 +1 @@
1
- {"version":3,"file":"input.cjs","names":["forwardRef","cn","when"],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import { forwardRef, type InputHTMLAttributes } from 'react';\n\nimport { cn, when } from '../../lib/cn';\nimport './input.scss';\n\nexport interface InputProps extends InputHTMLAttributes<HTMLInputElement> {\n recommended?: boolean;\n error?: boolean;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n { type = 'text', recommended, error, className, disabled, ...props },\n ref,\n) {\n return (\n <input\n ref={ref}\n type={type}\n disabled={disabled}\n aria-invalid={error || undefined}\n className={cn(\n 'input',\n className,\n when(recommended, 'input--recommended'),\n when(error, 'input--error'),\n )}\n {...props}\n />\n );\n});\n"],"mappings":";;;;;AAUA,MAAa,SAAA,GAAQA,MAAAA,WAAAA,CAAyC,SAAS,MACrE,EAAE,OAAO,QAAQ,aAAa,OAAO,WAAW,UAAU,GAAG,SAC7D,KACA;CACA,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;EACO;EACC;EACI;EACV,gBAAc,SAAS,KAAA;EACvB,WAAWC,WAAAA,GACT,SACA,WACAC,WAAAA,KAAK,aAAa,oBAAoB,GACtCA,WAAAA,KAAK,OAAO,cAAc,CAC5B;EACA,GAAI;CACL,CAAA;AAEL,CAAC"}
1
+ {"version":3,"file":"input.cjs","names":["forwardRef","useFieldControl","useRef","useState","useCallback","cn","when","IconCross"],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type InputHTMLAttributes,\n type MouseEvent,\n type Ref,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { IconCross } from '@/icons/icon-cross';\nimport { cn, when } from '@/lib/cn';\nimport './input.scss';\n\nexport type InputSize = 'sm' | 'md' | 'lg';\n\n/** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */\nexport interface InputProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'size'\n> {\n size?: InputSize;\n recommended?: boolean;\n error?: boolean;\n /** Крестик справа. Появляется, только когда в поле есть значение. */\n clearable?: boolean;\n clearLabel?: string;\n onClear?: () => void;\n}\n\nconst assignRef = (\n ref: Ref<HTMLInputElement> | undefined,\n node: HTMLInputElement | null,\n) => {\n if (typeof ref === 'function') {\n ref(node);\n return;\n }\n\n if (ref) ref.current = node;\n};\n\nconst toInputValue = (value: InputHTMLAttributes<HTMLInputElement>['value']) =>\n value == null ? '' : String(value);\n\n/**\n * Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.\n * Сеттер прототипа трекер не трогает.\n */\nconst emitInputValue = (input: HTMLInputElement, next: string) => {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLInputElement.prototype,\n 'value',\n )?.set;\n\n setter?.call(input, next);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n};\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n type = 'text',\n size = 'md',\n recommended,\n error,\n clearable = false,\n clearLabel = 'Очистить',\n onClear,\n className,\n disabled,\n readOnly,\n value,\n defaultValue,\n onChange,\n ...props\n },\n ref,\n) {\n const field = useFieldControl();\n const reportValue = field?.reportValue;\n const inputRef = useRef<HTMLInputElement>(null);\n const isControlled = value !== undefined;\n const [uncontrolledValue, setUncontrolledValue] = useState(() =>\n toInputValue(defaultValue),\n );\n const current = isControlled ? toInputValue(value) : uncontrolledValue;\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\n const describedBy = [field?.describedBy, props['aria-describedby']]\n .filter(part => Boolean(part))\n .join(' ');\n\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n\n useEffect(() => {\n const form = inputRef.current?.form;\n if (!form || isControlled) return;\n\n // reset всплывает от form вверх и до инпута не доходит\n const handleReset = () => {\n setUncontrolledValue(toInputValue(defaultValue));\n };\n\n form.addEventListener('reset', handleReset);\n return () => form.removeEventListener('reset', handleReset);\n }, [isControlled, defaultValue]);\n\n useEffect(() => {\n reportValue?.(current);\n }, [current, reportValue]);\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (!isControlled) setUncontrolledValue(event.target.value);\n onChange?.(event);\n };\n\n const handleClearMouseDown = (event: MouseEvent<HTMLButtonElement>) => {\n event.preventDefault();\n };\n\n const handleClear = () => {\n const input = inputRef.current;\n if (!input) return;\n\n emitInputValue(input, '');\n onClear?.();\n input.focus();\n };\n\n const canClear = clearable && !disabled && !readOnly && current !== '';\n\n return (\n <div className=\"input__wrap\">\n <input\n {...props}\n ref={setRef}\n type={type}\n disabled={disabled}\n readOnly={readOnly}\n {...(isControlled ? { value } : { defaultValue })}\n onChange={handleChange}\n {...(isInvalid ? { 'aria-invalid': true } : {})}\n {...(field ? { id: field.id } : {})}\n {...(field?.required\n ? { 'required': true, 'aria-required': true }\n : {})}\n {...(describedBy ? { 'aria-describedby': describedBy } : {})}\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\n className={cn(\n 'input',\n className,\n when(size !== 'md', `input--${size}`),\n when(recommended, 'input--recommended'),\n when(isInvalid, 'input--error'),\n when(clearable && !disabled && !readOnly, 'input--clearable'),\n )}\n />\n {canClear ? (\n <button\n type=\"button\"\n className=\"input__clear\"\n aria-label={clearLabel}\n onMouseDown={handleClearMouseDown}\n onClick={handleClear}\n >\n <IconCross width={16} height={16} />\n </button>\n ) : null}\n </div>\n );\n});\n"],"mappings":";;;;;;;AAiCA,MAAM,aACJ,KACA,SACG;CACH,IAAI,OAAO,QAAQ,YAAY;EAC7B,IAAI,IAAI;EACR;CACF;CAEA,IAAI,KAAK,IAAI,UAAU;AACzB;AAEA,MAAM,gBAAgB,UACpB,SAAS,OAAO,KAAK,OAAO,KAAK;;;;;AAMnC,MAAM,kBAAkB,OAAyB,SAAiB;CAMhE,CALe,OAAO,yBACpB,iBAAiB,WACjB,OACF,CAAC,EAAE,IAAA,EAEK,KAAK,OAAO,IAAI;CACxB,MAAM,cAAc,IAAI,MAAM,SAAS,EAAE,SAAS,KAAK,CAAC,CAAC;AAC3D;AAEA,MAAa,SAAA,GAAQA,MAAAA,WAAAA,CAAyC,SAAS,MACrE,EACE,OAAO,QACP,OAAO,MACP,aACA,OACA,YAAY,OACZ,aAAa,YACb,SACA,WACA,UACA,UACA,OACA,cACA,UACA,GAAG,SAEL,KACA;CACA,MAAM,QAAQC,sBAAAA,gBAAgB;CAC9B,MAAM,cAAc,OAAO;CAC3B,MAAM,YAAA,GAAWC,MAAAA,OAAAA,CAAyB,IAAI;CAC9C,MAAM,eAAe,UAAU,KAAA;CAC/B,MAAM,CAAC,mBAAmB,yBAAA,GAAwBC,MAAAA,SAAAA,OAChD,aAAa,YAAY,CAC3B;CACA,MAAM,UAAU,eAAe,aAAa,KAAK,IAAI;CACrD,MAAM,YAAY,QAAQ,KAAK,KAAK,QAAQ,OAAO,OAAO;CAC1D,MAAM,cAAc,CAAC,OAAO,aAAa,MAAM,mBAAmB,CAAC,CAChE,QAAO,SAAQ,QAAQ,IAAI,CAAC,CAAC,CAC7B,KAAK,GAAG;CAEX,MAAM,UAAA,GAASC,MAAAA,YAAAA,EACZ,SAAkC;EACjC,SAAS,UAAU;EACnB,UAAU,KAAK,IAAI;CACrB,GACA,CAAC,GAAG,CACN;CAEA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,OAAO,SAAS,SAAS;EAC/B,IAAI,CAAC,QAAQ,cAAc;EAG3B,MAAM,oBAAoB;GACxB,qBAAqB,aAAa,YAAY,CAAC;EACjD;EAEA,KAAK,iBAAiB,SAAS,WAAW;EAC1C,aAAa,KAAK,oBAAoB,SAAS,WAAW;CAC5D,GAAG,CAAC,cAAc,YAAY,CAAC;CAE/B,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,cAAc,OAAO;CACvB,GAAG,CAAC,SAAS,WAAW,CAAC;CAEzB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,CAAC,cAAc,qBAAqB,MAAM,OAAO,KAAK;EAC1D,WAAW,KAAK;CAClB;CAEA,MAAM,wBAAwB,UAAyC;EACrE,MAAM,eAAe;CACvB;CAEA,MAAM,oBAAoB;EACxB,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,OAAO;EAEZ,eAAe,OAAO,EAAE;EACxB,UAAU;EACV,MAAM,MAAM;CACd;CAEA,MAAM,WAAW,aAAa,CAAC,YAAY,CAAC,YAAY,YAAY;CAEpE,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GACE,GAAI;GACJ,KAAK;GACC;GACI;GACA;GACV,GAAK,eAAe,EAAE,MAAM,IAAI,EAAE,aAAa;GAC/C,UAAU;GACV,GAAK,YAAY,EAAE,gBAAgB,KAAK,IAAI,CAAC;GAC7C,GAAK,QAAQ,EAAE,IAAI,MAAM,GAAG,IAAI,CAAC;GACjC,GAAK,OAAO,WACR;IAAE,YAAY;IAAM,iBAAiB;GAAK,IAC1C,CAAC;GACL,GAAK,cAAc,EAAE,oBAAoB,YAAY,IAAI,CAAC;GAC1D,GAAK,OAAO,UAAU,EAAE,qBAAqB,MAAM,QAAQ,IAAI,CAAC;GAChE,WAAWC,WAAAA,GACT,SACA,WACAC,WAAAA,KAAK,SAAS,MAAM,UAAU,MAAM,GACpCA,WAAAA,KAAK,aAAa,oBAAoB,GACtCA,WAAAA,KAAK,WAAW,cAAc,GAC9BA,WAAAA,KAAK,aAAa,CAAC,YAAY,CAAC,UAAU,kBAAkB,CAC9D;EACD,CAAA,GACA,WACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;GACE,MAAK;GACL,WAAU;GACV,cAAY;GACZ,aAAa;GACb,SAAS;GAET,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,mBAAAA,WAAD;IAAW,OAAO;IAAI,QAAQ;GAAK,CAAA;EAC7B,CAAA,IACN,IACD;;AAET,CAAC"}
@@ -1,7 +1,14 @@
1
1
  import { InputHTMLAttributes } from "react";
2
- export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
2
+ export type InputSize = 'sm' | 'md' | 'lg';
3
+ /** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */
4
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
5
+ size?: InputSize;
3
6
  recommended?: boolean;
4
7
  error?: boolean;
8
+ /** Крестик справа. Появляется, только когда в поле есть значение. */
9
+ clearable?: boolean;
10
+ clearLabel?: string;
11
+ onClear?: () => void;
5
12
  }
6
13
  export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
7
14
  //# sourceMappingURL=input.d.cts.map
@@ -1,7 +1,14 @@
1
1
  import { InputHTMLAttributes } from "react";
2
- export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
2
+ export type InputSize = 'sm' | 'md' | 'lg';
3
+ /** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */
4
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
5
+ size?: InputSize;
3
6
  recommended?: boolean;
4
7
  error?: boolean;
8
+ /** Крестик справа. Появляется, только когда в поле есть значение. */
9
+ clearable?: boolean;
10
+ clearLabel?: string;
11
+ onClear?: () => void;
5
12
  }
6
13
  export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
7
14
  //# sourceMappingURL=input.d.ts.map
@@ -1,16 +1,96 @@
1
1
  import { cn, when } from "../../lib/cn.js";
2
- import { forwardRef } from "react";
3
- import { jsx } from "react/jsx-runtime";
2
+ import { useFieldControl } from "../field/field-context.js";
3
+ import { IconCross } from "../../icons/icon-cross.js";
4
+ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
4
6
  import "./input.scss";
5
7
  // src/components/input/input.tsx
6
- const Input = forwardRef(function Input({ type = "text", recommended, error, className, disabled, ...props }, ref) {
7
- return /* @__PURE__ */ jsx("input", {
8
- ref,
9
- type,
10
- disabled,
11
- "aria-invalid": error || void 0,
12
- className: cn("input", className, when(recommended, "input--recommended"), when(error, "input--error")),
13
- ...props
8
+ const assignRef = (ref, node) => {
9
+ if (typeof ref === "function") {
10
+ ref(node);
11
+ return;
12
+ }
13
+ if (ref) ref.current = node;
14
+ };
15
+ const toInputValue = (value) => value == null ? "" : String(value);
16
+ /**
17
+ * Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.
18
+ * Сеттер прототипа трекер не трогает.
19
+ */
20
+ const emitInputValue = (input, next) => {
21
+ (Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set)?.call(input, next);
22
+ input.dispatchEvent(new Event("input", { bubbles: true }));
23
+ };
24
+ const Input = forwardRef(function Input({ type = "text", size = "md", recommended, error, clearable = false, clearLabel = "Очистить", onClear, className, disabled, readOnly, value, defaultValue, onChange, ...props }, ref) {
25
+ const field = useFieldControl();
26
+ const reportValue = field?.reportValue;
27
+ const inputRef = useRef(null);
28
+ const isControlled = value !== void 0;
29
+ const [uncontrolledValue, setUncontrolledValue] = useState(() => toInputValue(defaultValue));
30
+ const current = isControlled ? toInputValue(value) : uncontrolledValue;
31
+ const isInvalid = Boolean(error) || Boolean(field?.invalid);
32
+ const describedBy = [field?.describedBy, props["aria-describedby"]].filter((part) => Boolean(part)).join(" ");
33
+ const setRef = useCallback((node) => {
34
+ inputRef.current = node;
35
+ assignRef(ref, node);
36
+ }, [ref]);
37
+ useEffect(() => {
38
+ const form = inputRef.current?.form;
39
+ if (!form || isControlled) return;
40
+ const handleReset = () => {
41
+ setUncontrolledValue(toInputValue(defaultValue));
42
+ };
43
+ form.addEventListener("reset", handleReset);
44
+ return () => form.removeEventListener("reset", handleReset);
45
+ }, [isControlled, defaultValue]);
46
+ useEffect(() => {
47
+ reportValue?.(current);
48
+ }, [current, reportValue]);
49
+ const handleChange = (event) => {
50
+ if (!isControlled) setUncontrolledValue(event.target.value);
51
+ onChange?.(event);
52
+ };
53
+ const handleClearMouseDown = (event) => {
54
+ event.preventDefault();
55
+ };
56
+ const handleClear = () => {
57
+ const input = inputRef.current;
58
+ if (!input) return;
59
+ emitInputValue(input, "");
60
+ onClear?.();
61
+ input.focus();
62
+ };
63
+ const canClear = clearable && !disabled && !readOnly && current !== "";
64
+ return /* @__PURE__ */ jsxs("div", {
65
+ className: "input__wrap",
66
+ children: [/* @__PURE__ */ jsx("input", {
67
+ ...props,
68
+ ref: setRef,
69
+ type,
70
+ disabled,
71
+ readOnly,
72
+ ...isControlled ? { value } : { defaultValue },
73
+ onChange: handleChange,
74
+ ...isInvalid ? { "aria-invalid": true } : {},
75
+ ...field ? { id: field.id } : {},
76
+ ...field?.required ? {
77
+ "required": true,
78
+ "aria-required": true
79
+ } : {},
80
+ ...describedBy ? { "aria-describedby": describedBy } : {},
81
+ ...field?.errorId ? { "aria-errormessage": field.errorId } : {},
82
+ className: cn("input", className, when(size !== "md", `input--${size}`), when(recommended, "input--recommended"), when(isInvalid, "input--error"), when(clearable && !disabled && !readOnly, "input--clearable"))
83
+ }), canClear ? /* @__PURE__ */ jsx("button", {
84
+ type: "button",
85
+ className: "input__clear",
86
+ "aria-label": clearLabel,
87
+ onMouseDown: handleClearMouseDown,
88
+ onClick: handleClear,
89
+ children: /* @__PURE__ */ jsx(IconCross, {
90
+ width: 16,
91
+ height: 16
92
+ })
93
+ }) : null]
14
94
  });
15
95
  });
16
96
  export { Input };
@@ -1 +1 @@
1
- {"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import { forwardRef, type InputHTMLAttributes } from 'react';\n\nimport { cn, when } from '../../lib/cn';\nimport './input.scss';\n\nexport interface InputProps extends InputHTMLAttributes<HTMLInputElement> {\n recommended?: boolean;\n error?: boolean;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n { type = 'text', recommended, error, className, disabled, ...props },\n ref,\n) {\n return (\n <input\n ref={ref}\n type={type}\n disabled={disabled}\n aria-invalid={error || undefined}\n className={cn(\n 'input',\n className,\n when(recommended, 'input--recommended'),\n when(error, 'input--error'),\n )}\n {...props}\n />\n );\n});\n"],"mappings":";;;;;AAUA,MAAa,QAAQ,WAAyC,SAAS,MACrE,EAAE,OAAO,QAAQ,aAAa,OAAO,WAAW,UAAU,GAAG,SAC7D,KACA;CACA,OACE,oBAAC,SAAD;EACO;EACC;EACI;EACV,gBAAc,SAAS,KAAA;EACvB,WAAW,GACT,SACA,WACA,KAAK,aAAa,oBAAoB,GACtC,KAAK,OAAO,cAAc,CAC5B;EACA,GAAI;CACL,CAAA;AAEL,CAAC"}
1
+ {"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type InputHTMLAttributes,\n type MouseEvent,\n type Ref,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { IconCross } from '@/icons/icon-cross';\nimport { cn, when } from '@/lib/cn';\nimport './input.scss';\n\nexport type InputSize = 'sm' | 'md' | 'lg';\n\n/** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */\nexport interface InputProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'size'\n> {\n size?: InputSize;\n recommended?: boolean;\n error?: boolean;\n /** Крестик справа. Появляется, только когда в поле есть значение. */\n clearable?: boolean;\n clearLabel?: string;\n onClear?: () => void;\n}\n\nconst assignRef = (\n ref: Ref<HTMLInputElement> | undefined,\n node: HTMLInputElement | null,\n) => {\n if (typeof ref === 'function') {\n ref(node);\n return;\n }\n\n if (ref) ref.current = node;\n};\n\nconst toInputValue = (value: InputHTMLAttributes<HTMLInputElement>['value']) =>\n value == null ? '' : String(value);\n\n/**\n * Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.\n * Сеттер прототипа трекер не трогает.\n */\nconst emitInputValue = (input: HTMLInputElement, next: string) => {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLInputElement.prototype,\n 'value',\n )?.set;\n\n setter?.call(input, next);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n};\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n type = 'text',\n size = 'md',\n recommended,\n error,\n clearable = false,\n clearLabel = 'Очистить',\n onClear,\n className,\n disabled,\n readOnly,\n value,\n defaultValue,\n onChange,\n ...props\n },\n ref,\n) {\n const field = useFieldControl();\n const reportValue = field?.reportValue;\n const inputRef = useRef<HTMLInputElement>(null);\n const isControlled = value !== undefined;\n const [uncontrolledValue, setUncontrolledValue] = useState(() =>\n toInputValue(defaultValue),\n );\n const current = isControlled ? toInputValue(value) : uncontrolledValue;\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\n const describedBy = [field?.describedBy, props['aria-describedby']]\n .filter(part => Boolean(part))\n .join(' ');\n\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n\n useEffect(() => {\n const form = inputRef.current?.form;\n if (!form || isControlled) return;\n\n // reset всплывает от form вверх и до инпута не доходит\n const handleReset = () => {\n setUncontrolledValue(toInputValue(defaultValue));\n };\n\n form.addEventListener('reset', handleReset);\n return () => form.removeEventListener('reset', handleReset);\n }, [isControlled, defaultValue]);\n\n useEffect(() => {\n reportValue?.(current);\n }, [current, reportValue]);\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (!isControlled) setUncontrolledValue(event.target.value);\n onChange?.(event);\n };\n\n const handleClearMouseDown = (event: MouseEvent<HTMLButtonElement>) => {\n event.preventDefault();\n };\n\n const handleClear = () => {\n const input = inputRef.current;\n if (!input) return;\n\n emitInputValue(input, '');\n onClear?.();\n input.focus();\n };\n\n const canClear = clearable && !disabled && !readOnly && current !== '';\n\n return (\n <div className=\"input__wrap\">\n <input\n {...props}\n ref={setRef}\n type={type}\n disabled={disabled}\n readOnly={readOnly}\n {...(isControlled ? { value } : { defaultValue })}\n onChange={handleChange}\n {...(isInvalid ? { 'aria-invalid': true } : {})}\n {...(field ? { id: field.id } : {})}\n {...(field?.required\n ? { 'required': true, 'aria-required': true }\n : {})}\n {...(describedBy ? { 'aria-describedby': describedBy } : {})}\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\n className={cn(\n 'input',\n className,\n when(size !== 'md', `input--${size}`),\n when(recommended, 'input--recommended'),\n when(isInvalid, 'input--error'),\n when(clearable && !disabled && !readOnly, 'input--clearable'),\n )}\n />\n {canClear ? (\n <button\n type=\"button\"\n className=\"input__clear\"\n aria-label={clearLabel}\n onMouseDown={handleClearMouseDown}\n onClick={handleClear}\n >\n <IconCross width={16} height={16} />\n </button>\n ) : null}\n </div>\n );\n});\n"],"mappings":"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"}
@@ -1,13 +1,17 @@
1
1
  .input {
2
+ // Размер контрола. Высоты совпадают с Button: sm 30px, md 40px, lg 52px.
3
+ --input-padding-y: 9px;
4
+ --input-padding-x: 14px;
5
+ --input-height: 40px;
2
6
  box-sizing: border-box;
3
7
  display: flex;
4
8
  align-items: center;
5
9
  width: 100%;
6
- padding: 9px 14px;
7
- font-size: 14px;
8
- line-height: 20px;
10
+ padding: var(--input-padding-y) var(--input-padding-x);
11
+ font-size: var(--text-sm);
12
+ line-height: var(--text-sm--line-height);
9
13
  background-color: transparent;
10
- color: var(--color-black);
14
+ color: var(--color-foreground);
11
15
  border-radius: var(--radius-sm);
12
16
  appearance: none;
13
17
  font-family: var(--font-sans);
@@ -18,11 +22,19 @@
18
22
  border-color 0.3s,
19
23
  box-shadow 0.2s;
20
24
 
25
+ // Фон автозаполнения Chrome перебивается только внутренней тенью.
21
26
  &:-webkit-autofill,
22
27
  &:-webkit-autofill:hover,
23
- &:-webkit-autofill:focus,
24
28
  &:-webkit-autofill:active {
25
- -webkit-box-shadow: 0 0 0 30px white inset;
29
+ -webkit-box-shadow: 0 0 0 30px var(--color-background) inset;
30
+ }
31
+
32
+ // :-webkit-autofill:focus специфичнее :focus и затирает его box-shadow,
33
+ // поэтому обводка фокуса повторяется здесь второй тенью.
34
+ &:-webkit-autofill:focus {
35
+ -webkit-box-shadow:
36
+ 0 0 0 30px var(--color-background) inset,
37
+ 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
26
38
  }
27
39
 
28
40
  &[type='number']::-webkit-outer-spin-button,
@@ -36,7 +48,7 @@
36
48
  }
37
49
 
38
50
  &[type='date'] {
39
- height: 40px;
51
+ height: var(--input-height);
40
52
  }
41
53
 
42
54
  &::-webkit-date-and-time-value {
@@ -44,21 +56,72 @@
44
56
  }
45
57
 
46
58
  &:focus {
47
- border-color: var(--color-black);
48
- box-shadow: 0 0 0 1px var(--color-black);
59
+ border-color: var(--color-ring);
60
+ box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
49
61
  }
50
62
 
51
63
  &:disabled {
52
- background-color: var(--color-neutral-200);
53
- color: var(--color-neutral-500);
54
- fill: var(--color-neutral-500);
64
+ background-color: var(--color-muted);
65
+ color: var(--color-muted-foreground);
66
+ fill: var(--color-muted-foreground);
67
+ }
68
+
69
+ &--sm {
70
+ --input-padding-y: 4px;
71
+ --input-padding-x: 12px;
72
+ --input-height: 30px;
73
+ }
74
+
75
+ &--lg {
76
+ --input-padding-y: var(--space-3);
77
+ --input-padding-x: 24px;
78
+ --input-height: 52px;
55
79
  }
56
80
 
57
81
  &--recommended {
58
- background-color: color-mix(in srgb, var(--color-yellow) 10%, transparent);
82
+ background-color: var(--color-recommended);
59
83
  }
60
84
 
61
85
  &--error {
62
- border-color: color-mix(in srgb, var(--color-red) 50%, transparent);
86
+ border-color: color-mix(in srgb, var(--color-destructive) 50%, transparent);
87
+ }
88
+
89
+ &--clearable {
90
+ padding-right: 36px;
91
+
92
+ &[type='search']::-webkit-search-cancel-button {
93
+ -webkit-appearance: none;
94
+ appearance: none;
95
+ }
96
+ }
97
+ }
98
+
99
+ .input__wrap {
100
+ position: relative;
101
+ width: 100%;
102
+ }
103
+
104
+ .input__clear {
105
+ position: absolute;
106
+ top: 0;
107
+ right: 4px;
108
+ bottom: 0;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ width: 28px;
113
+ margin: 0;
114
+ padding: 0;
115
+ border: 0;
116
+ background: none;
117
+ color: var(--color-muted-foreground);
118
+
119
+ &:hover {
120
+ color: var(--color-foreground);
121
+ }
122
+
123
+ &:focus-visible {
124
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
125
+ border-radius: var(--radius-sm);
63
126
  }
64
127
  }
@@ -1,2 +1,2 @@
1
- import { RadioInput, RadioInputProps } from "./radio.cjs";
2
- export { RadioInput, type RadioInputProps };
1
+ import { RadioGroup, RadioGroupProps, RadioInput, RadioInputProps } from "./radio.cjs";
2
+ export { RadioGroup, type RadioGroupProps, RadioInput, type RadioInputProps };
@@ -1,2 +1,2 @@
1
- import { RadioInput, RadioInputProps } from "./radio.js";
2
- export { RadioInput, type RadioInputProps };
1
+ import { RadioGroup, RadioGroupProps, RadioInput, RadioInputProps } from "./radio.js";
2
+ export { RadioGroup, type RadioGroupProps, RadioInput, type RadioInputProps };
@@ -1,27 +1,69 @@
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("./radio.scss");
5
6
  // src/components/radio/radio.tsx
6
- const RadioInput = (0, react.forwardRef)(function RadioInput({ id, className, title, error, disabled, ...props }, ref) {
7
+ const RadioGroupStateContext = (0, react.createContext)(null);
8
+ const RadioGroup = ({ label, name, required: requiredProp = false, children, className }) => {
9
+ const field = require_field_context.useFieldControl();
10
+ const required = requiredProp || Boolean(field?.required);
11
+ const labelledBy = !label && field?.labelId ? field.labelId : void 0;
12
+ const reportValue = field?.reportValue;
13
+ const group = (0, react.useMemo)(() => ({
14
+ name,
15
+ required,
16
+ reportValue
17
+ }), [
18
+ name,
19
+ required,
20
+ reportValue
21
+ ]);
22
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
23
+ className: require_cn.cn("radio-group", className),
24
+ role: "radiogroup",
25
+ ...labelledBy ? { "aria-labelledby": labelledBy } : {},
26
+ ...field?.invalid ? { "aria-invalid": true } : {},
27
+ ...required ? { "aria-required": true } : {},
28
+ ...field?.describedBy ? { "aria-describedby": field.describedBy } : {},
29
+ ...field?.errorId ? { "aria-errormessage": field.errorId } : {},
30
+ children: [label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
31
+ className: "radio-group__legend",
32
+ children: label
33
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RadioGroupStateContext.Provider, {
34
+ value: group,
35
+ children
36
+ })]
37
+ });
38
+ };
39
+ const RadioInput = (0, react.forwardRef)(function RadioInput({ id, className, label, error, disabled, name, onChange, ...props }, ref) {
40
+ const group = (0, react.useContext)(RadioGroupStateContext);
7
41
  const autoId = (0, react.useId)();
8
42
  const inputId = id ?? autoId;
43
+ const handleChange = (event) => {
44
+ if (event.target.checked) group?.reportValue?.(event.target.value);
45
+ onChange?.(event);
46
+ };
9
47
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10
48
  className: require_cn.cn("radio", className, require_cn.when(error, "radio--error")),
11
49
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
50
+ ...props,
12
51
  ref,
13
52
  type: "radio",
14
53
  id: inputId,
54
+ name: group?.name ?? name,
15
55
  disabled,
16
- className: "radio__input",
17
- ...props
18
- }), title ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
56
+ ...group?.required ? { required: true } : {},
57
+ onChange: handleChange,
58
+ className: "radio__input"
59
+ }), label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
19
60
  className: "radio__label",
20
61
  htmlFor: inputId,
21
- children: title
62
+ children: label
22
63
  }) : null]
23
64
  });
24
65
  });
66
+ exports.RadioGroup = RadioGroup;
25
67
  exports.RadioInput = RadioInput;
26
68
 
27
69
  //# sourceMappingURL=radio.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"radio.cjs","names":["forwardRef","useId","cn","when"],"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import { forwardRef, useId, type InputHTMLAttributes } from 'react';\n\nimport { cn, when } from '../../lib/cn';\nimport './radio.scss';\n\nexport interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {\n title?: string;\n error?: boolean;\n}\n\nexport const RadioInput = forwardRef<HTMLInputElement, RadioInputProps>(\n function RadioInput(\n { id, className, title, error, disabled, ...props },\n ref,\n ) {\n const autoId = useId();\n const inputId = id ?? autoId;\n\n return (\n <div className={cn('radio', className, when(error, 'radio--error'))}>\n <input\n ref={ref}\n type=\"radio\"\n id={inputId}\n disabled={disabled}\n className=\"radio__input\"\n {...props}\n />\n {title ? (\n <label className=\"radio__label\" htmlFor={inputId}>\n {title}\n </label>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;AAUA,MAAa,cAAA,GAAaA,MAAAA,WAAAA,CACxB,SAAS,WACP,EAAE,IAAI,WAAW,OAAO,OAAO,UAAU,GAAG,SAC5C,KACA;CACA,MAAM,UAAA,GAASC,MAAAA,MAAAA,CAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAWC,WAAAA,GAAG,SAAS,WAAWC,WAAAA,KAAK,OAAO,cAAc,CAAC;EAAlE,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GACO;GACL,MAAK;GACL,IAAI;GACM;GACV,WAAU;GACV,GAAI;EACL,CAAA,GACA,QACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GAAO,WAAU;GAAe,SAAS;GACtC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
1
+ {"version":3,"file":"radio.cjs","names":["createContext","useFieldControl","useMemo","cn","forwardRef","useContext","useId","when"],"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import {\n createContext,\n forwardRef,\n useContext,\n useId,\n useMemo,\n type ChangeEvent,\n type InputHTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { cn, when } from '@/lib/cn';\nimport './radio.scss';\n\ninterface RadioGroupState {\n name: string;\n required: boolean;\n reportValue?: (value: string) => void;\n}\n\nconst RadioGroupStateContext = createContext<RadioGroupState | null>(null);\n\nexport interface RadioGroupProps {\n /** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */\n label?: ReactNode;\n name: string;\n /** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */\n required?: boolean;\n children: ReactNode;\n className?: string;\n}\n\nexport const RadioGroup = ({\n label,\n name,\n required: requiredProp = false,\n children,\n className,\n}: RadioGroupProps) => {\n const field = useFieldControl();\n const required = requiredProp || Boolean(field?.required);\n const labelledBy = !label && field?.labelId ? field.labelId : undefined;\n // Обязательность держат пункты: нативный required на них не даёт отправить\n // форму, а его invalid Field ловит и показывает свою ошибку. Выбор отдаём\n // в reportValue — иначе показанная ошибка так и висела бы после выбора.\n const reportValue = field?.reportValue;\n const group = useMemo<RadioGroupState>(\n () => ({ name, required, reportValue }),\n [name, required, reportValue],\n );\n\n return (\n <fieldset\n className={cn('radio-group', className)}\n role=\"radiogroup\"\n {...(labelledBy ? { 'aria-labelledby': labelledBy } : {})}\n {...(field?.invalid ? { 'aria-invalid': true } : {})}\n {...(required ? { 'aria-required': true } : {})}\n {...(field?.describedBy ? { 'aria-describedby': field.describedBy } : {})}\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\n >\n {label ? <legend className=\"radio-group__legend\">{label}</legend> : null}\n <RadioGroupStateContext.Provider value={group}>\n {children}\n </RadioGroupStateContext.Provider>\n </fieldset>\n );\n};\n\nexport interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {\n label?: ReactNode;\n error?: boolean;\n}\n\nexport const RadioInput = forwardRef<HTMLInputElement, RadioInputProps>(\n function RadioInput(\n { id, className, label, error, disabled, name, onChange, ...props },\n ref,\n ) {\n const group = useContext(RadioGroupStateContext);\n const autoId = useId();\n const inputId = id ?? autoId;\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (event.target.checked) group?.reportValue?.(event.target.value);\n onChange?.(event);\n };\n\n return (\n <div className={cn('radio', className, when(error, 'radio--error'))}>\n <input\n {...props}\n ref={ref}\n type=\"radio\"\n id={inputId}\n // Внутри группы имя группы побеждает, иначе её можно разрезать name на пункте.\n name={group?.name ?? name}\n disabled={disabled}\n // required на любом пункте делает обязательной всю группу с этим name.\n {...(group?.required ? { required: true } : {})}\n onChange={handleChange}\n className=\"radio__input\"\n />\n {label ? (\n <label className=\"radio__label\" htmlFor={inputId}>\n {label}\n </label>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;;AAqBA,MAAM,0BAAA,GAAyBA,MAAAA,cAAAA,CAAsC,IAAI;AAYzE,MAAa,cAAc,EACzB,OACA,MACA,UAAU,eAAe,OACzB,UACA,gBACqB;CACrB,MAAM,QAAQC,sBAAAA,gBAAgB;CAC9B,MAAM,WAAW,gBAAgB,QAAQ,OAAO,QAAQ;CACxD,MAAM,aAAa,CAAC,SAAS,OAAO,UAAU,MAAM,UAAU,KAAA;CAI9D,MAAM,cAAc,OAAO;CAC3B,MAAM,SAAA,GAAQC,MAAAA,QAAAA,QACL;EAAE;EAAM;EAAU;CAAY,IACrC;EAAC;EAAM;EAAU;CAAW,CAC9B;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,YAAD;EACE,WAAWC,WAAAA,GAAG,eAAe,SAAS;EACtC,MAAK;EACL,GAAK,aAAa,EAAE,mBAAmB,WAAW,IAAI,CAAC;EACvD,GAAK,OAAO,UAAU,EAAE,gBAAgB,KAAK,IAAI,CAAC;EAClD,GAAK,WAAW,EAAE,iBAAiB,KAAK,IAAI,CAAC;EAC7C,GAAK,OAAO,cAAc,EAAE,oBAAoB,MAAM,YAAY,IAAI,CAAC;EACvE,GAAK,OAAO,UAAU,EAAE,qBAAqB,MAAM,QAAQ,IAAI,CAAC;EAPlE,UAAA,CASG,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;GAAQ,WAAU;GAAuB,UAAA;EAAc,CAAA,IAAI,MACpE,iBAAA,GAAA,kBAAA,IAAA,CAAC,uBAAuB,UAAxB;GAAiC,OAAO;GACrC;EAC8B,CAAA,CACzB;;AAEd;AAOA,MAAa,cAAA,GAAaC,MAAAA,WAAAA,CACxB,SAAS,WACP,EAAE,IAAI,WAAW,OAAO,OAAO,UAAU,MAAM,UAAU,GAAG,SAC5D,KACA;CACA,MAAM,SAAA,GAAQC,MAAAA,WAAAA,CAAW,sBAAsB;CAC/C,MAAM,UAAA,GAASC,MAAAA,MAAAA,CAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,MAAM,OAAO,SAAS,OAAO,cAAc,MAAM,OAAO,KAAK;EACjE,WAAW,KAAK;CAClB;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAWH,WAAAA,GAAG,SAAS,WAAWI,WAAAA,KAAK,OAAO,cAAc,CAAC;EAAlE,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GACE,GAAI;GACC;GACL,MAAK;GACL,IAAI;GAEJ,MAAM,OAAO,QAAQ;GACX;GAEV,GAAK,OAAO,WAAW,EAAE,UAAU,KAAK,IAAI,CAAC;GAC7C,UAAU;GACV,WAAU;EACX,CAAA,GACA,QACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GAAO,WAAU;GAAe,SAAS;GACtC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
@@ -1,6 +1,16 @@
1
- import { InputHTMLAttributes } from "react";
1
+ import { InputHTMLAttributes, ReactNode } from "react";
2
+ export interface RadioGroupProps {
3
+ /** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */
4
+ label?: ReactNode;
5
+ name: string;
6
+ /** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */
7
+ required?: boolean;
8
+ children: ReactNode;
9
+ className?: string;
10
+ }
11
+ export declare const RadioGroup: ({ label, name, required: requiredProp, children, className }: RadioGroupProps) => import("react").JSX.Element;
2
12
  export interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {
3
- title?: string;
13
+ label?: ReactNode;
4
14
  error?: boolean;
5
15
  }
6
16
  export declare const RadioInput: import("react").ForwardRefExoticComponent<RadioInputProps & import("react").RefAttributes<HTMLInputElement>>;
@@ -1,6 +1,16 @@
1
- import { InputHTMLAttributes } from "react";
1
+ import { InputHTMLAttributes, ReactNode } from "react";
2
+ export interface RadioGroupProps {
3
+ /** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */
4
+ label?: ReactNode;
5
+ name: string;
6
+ /** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */
7
+ required?: boolean;
8
+ children: ReactNode;
9
+ className?: string;
10
+ }
11
+ export declare const RadioGroup: ({ label, name, required: requiredProp, children, className }: RadioGroupProps) => import("react").JSX.Element;
2
12
  export interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {
3
- title?: string;
13
+ label?: ReactNode;
4
14
  error?: boolean;
5
15
  }
6
16
  export declare const RadioInput: import("react").ForwardRefExoticComponent<RadioInputProps & import("react").RefAttributes<HTMLInputElement>>;