@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
@@ -1 +1 @@
1
- {"version":3,"file":"checkbox.cjs","names":["forwardRef","useId","cn","when"],"sources":["../../../src/components/checkbox/checkbox.tsx"],"sourcesContent":["import { forwardRef, useId, type InputHTMLAttributes } from 'react';\n\nimport { cn, when } from '../../lib/cn';\nimport './checkbox.scss';\n\nexport interface CheckboxInputProps extends InputHTMLAttributes<HTMLInputElement> {\n title?: string;\n hide?: boolean;\n withCursor?: boolean;\n}\n\nexport const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(\n function CheckboxInput(\n { className, id, title, hide, disabled, withCursor = true, ...props },\n ref,\n ) {\n const autoId = useId();\n const inputId = id ?? autoId;\n\n return (\n <div className={cn('checkbox', className, when(hide, 'checkbox--hide'))}>\n <input\n ref={ref}\n type=\"checkbox\"\n id={inputId}\n disabled={disabled}\n className={cn(\n 'checkbox__input',\n when(!withCursor, 'checkbox__input--no-events'),\n )}\n {...props}\n />\n {title ? (\n <label className=\"checkbox__label\" htmlFor={inputId}>\n {title.replaceAll('-', '–')}\n </label>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;AAWA,MAAa,iBAAA,GAAgBA,MAAAA,WAAAA,CAC3B,SAAS,cACP,EAAE,WAAW,IAAI,OAAO,MAAM,UAAU,aAAa,MAAM,GAAG,SAC9D,KACA;CACA,MAAM,UAAA,GAASC,MAAAA,MAAAA,CAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAWC,WAAAA,GAAG,YAAY,WAAWC,WAAAA,KAAK,MAAM,gBAAgB,CAAC;EAAtE,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GACO;GACL,MAAK;GACL,IAAI;GACM;GACV,WAAWD,WAAAA,GACT,mBACAC,WAAAA,KAAK,CAAC,YAAY,4BAA4B,CAChD;GACA,GAAI;EACL,CAAA,GACA,QACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GAAO,WAAU;GAAkB,SAAS;GACzC,UAAA,MAAM,WAAW,KAAK,GAAG;EACrB,CAAA,IACL,IACD;;AAET,CACF"}
1
+ {"version":3,"file":"checkbox.cjs","names":["forwardRef","useFieldControl","useId","useRef","useCallback","cn","when"],"sources":["../../../src/components/checkbox/checkbox.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n type ChangeEvent,\n type InputHTMLAttributes,\n type ReactNode,\n type Ref,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { cn, when } from '@/lib/cn';\nimport './checkbox.scss';\n\nexport interface CheckboxInputProps extends InputHTMLAttributes<HTMLInputElement> {\n label?: ReactNode;\n /** Красная рамка. Внутри `Field` состоянием ошибки распоряжается он. */\n error?: boolean;\n /** «Выбрать все»: IDL `indeterminate`, React его атрибутом не прокидывает. */\n indeterminate?: boolean;\n /**\n * false — `pointer-events: none` на input (`checkbox__input--no-events`).\n * Клик по лейблу по-прежнему переключает.\n */\n clickable?: boolean;\n}\n\nconst assignRef = (\n ref: Ref<HTMLInputElement> | null,\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 mergeDescribedBy = (\n fromField: string | undefined,\n fromProps: string | undefined,\n) => {\n const value = [fromField, fromProps].filter(part => Boolean(part)).join(' ');\n return value || undefined;\n};\n\nexport const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(\n function CheckboxInput(\n {\n className,\n id,\n label,\n error,\n disabled,\n clickable = true,\n indeterminate = false,\n checked,\n onChange,\n ...props\n },\n ref,\n ) {\n const field = useFieldControl();\n const reportValue = field?.reportValue;\n const autoId = useId();\n const inputId = field?.id ?? id ?? autoId;\n const inputRef = useRef<HTMLInputElement>(null);\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\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 if (inputRef.current) inputRef.current.indeterminate = indeterminate;\n }, [indeterminate]);\n\n // Field ждёт строку: отдаём то же, что ушло бы в FormData — 'on' или пусто.\n useEffect(() => {\n const node = inputRef.current;\n if (node) reportValue?.(node.checked ? 'on' : '');\n }, [checked, reportValue]);\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (checked === undefined) {\n reportValue?.(event.target.checked ? 'on' : '');\n }\n onChange?.(event);\n };\n\n const describedBy = field\n ? mergeDescribedBy(field.describedBy, props['aria-describedby'])\n : undefined;\n\n const fieldAttrs: InputHTMLAttributes<HTMLInputElement> = {};\n\n if (field?.required) {\n fieldAttrs.required = true;\n fieldAttrs['aria-required'] = true;\n }\n\n if (isInvalid) fieldAttrs['aria-invalid'] = true;\n if (describedBy) fieldAttrs['aria-describedby'] = describedBy;\n if (field?.errorId) fieldAttrs['aria-errormessage'] = field.errorId;\n\n return (\n <div className={cn('checkbox', className)}>\n <input\n {...props}\n ref={setRef}\n type=\"checkbox\"\n id={inputId}\n disabled={disabled}\n {...(checked !== undefined ? { checked } : {})}\n onChange={handleChange}\n className={cn(\n 'checkbox__input',\n when(!clickable, 'checkbox__input--no-events'),\n )}\n {...fieldAttrs}\n />\n {label ? (\n <label className=\"checkbox__label\" htmlFor={inputId}>\n {label}\n </label>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;;AA6BA,MAAM,aACJ,KACA,SACG;CACH,IAAI,OAAO,QAAQ,YAAY;EAC7B,IAAI,IAAI;EACR;CACF;CAEA,IAAI,KAAK,IAAI,UAAU;AACzB;AAEA,MAAM,oBACJ,WACA,cACG;CAEH,OADc,CAAC,WAAW,SAAS,CAAC,CAAC,QAAO,SAAQ,QAAQ,IAAI,CAAC,CAAC,CAAC,KAAK,GAC7D,KAAK,KAAA;AAClB;AAEA,MAAa,iBAAA,GAAgBA,MAAAA,WAAAA,CAC3B,SAAS,cACP,EACE,WACA,IACA,OACA,OACA,UACA,YAAY,MACZ,gBAAgB,OAChB,SACA,UACA,GAAG,SAEL,KACA;CACA,MAAM,QAAQC,sBAAAA,gBAAgB;CAC9B,MAAM,cAAc,OAAO;CAC3B,MAAM,UAAA,GAASC,MAAAA,MAAAA,CAAM;CACrB,MAAM,UAAU,OAAO,MAAM,MAAM;CACnC,MAAM,YAAA,GAAWC,MAAAA,OAAAA,CAAyB,IAAI;CAC9C,MAAM,YAAY,QAAQ,KAAK,KAAK,QAAQ,OAAO,OAAO;CAE1D,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,IAAI,SAAS,SAAS,SAAS,QAAQ,gBAAgB;CACzD,GAAG,CAAC,aAAa,CAAC;CAGlB,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,OAAO,SAAS;EACtB,IAAI,MAAM,cAAc,KAAK,UAAU,OAAO,EAAE;CAClD,GAAG,CAAC,SAAS,WAAW,CAAC;CAEzB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,YAAY,KAAA,GACd,cAAc,MAAM,OAAO,UAAU,OAAO,EAAE;EAEhD,WAAW,KAAK;CAClB;CAEA,MAAM,cAAc,QAChB,iBAAiB,MAAM,aAAa,MAAM,mBAAmB,IAC7D,KAAA;CAEJ,MAAM,aAAoD,CAAC;CAE3D,IAAI,OAAO,UAAU;EACnB,WAAW,WAAW;EACtB,WAAW,mBAAmB;CAChC;CAEA,IAAI,WAAW,WAAW,kBAAkB;CAC5C,IAAI,aAAa,WAAW,sBAAsB;CAClD,IAAI,OAAO,SAAS,WAAW,uBAAuB,MAAM;CAE5D,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAWC,WAAAA,GAAG,YAAY,SAAS;EAAxC,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GACE,GAAI;GACJ,KAAK;GACL,MAAK;GACL,IAAI;GACM;GACV,GAAK,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC5C,UAAU;GACV,WAAWA,WAAAA,GACT,mBACAC,WAAAA,KAAK,CAAC,WAAW,4BAA4B,CAC/C;GACA,GAAI;EACL,CAAA,GACA,QACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GAAO,WAAU;GAAkB,SAAS;GACzC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
@@ -1,8 +1,15 @@
1
- import { InputHTMLAttributes } from "react";
1
+ import { InputHTMLAttributes, ReactNode } from "react";
2
2
  export interface CheckboxInputProps extends InputHTMLAttributes<HTMLInputElement> {
3
- title?: string;
4
- hide?: boolean;
5
- withCursor?: boolean;
3
+ label?: ReactNode;
4
+ /** Красная рамка. Внутри `Field` состоянием ошибки распоряжается он. */
5
+ error?: boolean;
6
+ /** «Выбрать все»: IDL `indeterminate`, React его атрибутом не прокидывает. */
7
+ indeterminate?: boolean;
8
+ /**
9
+ * false — `pointer-events: none` на input (`checkbox__input--no-events`).
10
+ * Клик по лейблу по-прежнему переключает.
11
+ */
12
+ clickable?: boolean;
6
13
  }
7
14
  export declare const CheckboxInput: import("react").ForwardRefExoticComponent<CheckboxInputProps & import("react").RefAttributes<HTMLInputElement>>;
8
15
  //# sourceMappingURL=checkbox.d.cts.map
@@ -1,8 +1,15 @@
1
- import { InputHTMLAttributes } from "react";
1
+ import { InputHTMLAttributes, ReactNode } from "react";
2
2
  export interface CheckboxInputProps extends InputHTMLAttributes<HTMLInputElement> {
3
- title?: string;
4
- hide?: boolean;
5
- withCursor?: boolean;
3
+ label?: ReactNode;
4
+ /** Красная рамка. Внутри `Field` состоянием ошибки распоряжается он. */
5
+ error?: boolean;
6
+ /** «Выбрать все»: IDL `indeterminate`, React его атрибутом не прокидывает. */
7
+ indeterminate?: boolean;
8
+ /**
9
+ * false — `pointer-events: none` на input (`checkbox__input--no-events`).
10
+ * Клик по лейблу по-прежнему переключает.
11
+ */
12
+ clickable?: boolean;
6
13
  }
7
14
  export declare const CheckboxInput: import("react").ForwardRefExoticComponent<CheckboxInputProps & import("react").RefAttributes<HTMLInputElement>>;
8
15
  //# sourceMappingURL=checkbox.d.ts.map
@@ -1,24 +1,66 @@
1
1
  import { cn, when } from "../../lib/cn.js";
2
- import { forwardRef, useId } from "react";
2
+ import { useFieldControl } from "../field/field-context.js";
3
+ import { forwardRef, useCallback, useEffect, useId, useRef } from "react";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  import "./checkbox.scss";
5
6
  // src/components/checkbox/checkbox.tsx
6
- const CheckboxInput = 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 = forwardRef(function CheckboxInput({ className, id, label, error, disabled, clickable = true, indeterminate = false, checked, onChange, ...props }, ref) {
18
+ const field = useFieldControl();
19
+ const reportValue = field?.reportValue;
7
20
  const autoId = useId();
8
- const inputId = id ?? autoId;
21
+ const inputId = field?.id ?? id ?? autoId;
22
+ const inputRef = useRef(null);
23
+ const isInvalid = Boolean(error) || Boolean(field?.invalid);
24
+ const setRef = useCallback((node) => {
25
+ inputRef.current = node;
26
+ assignRef(ref, node);
27
+ }, [ref]);
28
+ useEffect(() => {
29
+ if (inputRef.current) inputRef.current.indeterminate = indeterminate;
30
+ }, [indeterminate]);
31
+ 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__ */ jsxs("div", {
10
- className: cn("checkbox", className, when(hide, "checkbox--hide")),
49
+ className: cn("checkbox", className),
11
50
  children: [/* @__PURE__ */ jsx("input", {
12
- ref,
51
+ ...props,
52
+ ref: setRef,
13
53
  type: "checkbox",
14
54
  id: inputId,
15
55
  disabled,
16
- className: cn("checkbox__input", when(!withCursor, "checkbox__input--no-events")),
17
- ...props
18
- }), title ? /* @__PURE__ */ jsx("label", {
56
+ ...checked !== void 0 ? { checked } : {},
57
+ onChange: handleChange,
58
+ className: cn("checkbox__input", when(!clickable, "checkbox__input--no-events")),
59
+ ...fieldAttrs
60
+ }), label ? /* @__PURE__ */ 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
  });
@@ -1 +1 @@
1
- {"version":3,"file":"checkbox.js","names":[],"sources":["../../../src/components/checkbox/checkbox.tsx"],"sourcesContent":["import { forwardRef, useId, type InputHTMLAttributes } from 'react';\n\nimport { cn, when } from '../../lib/cn';\nimport './checkbox.scss';\n\nexport interface CheckboxInputProps extends InputHTMLAttributes<HTMLInputElement> {\n title?: string;\n hide?: boolean;\n withCursor?: boolean;\n}\n\nexport const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(\n function CheckboxInput(\n { className, id, title, hide, disabled, withCursor = true, ...props },\n ref,\n ) {\n const autoId = useId();\n const inputId = id ?? autoId;\n\n return (\n <div className={cn('checkbox', className, when(hide, 'checkbox--hide'))}>\n <input\n ref={ref}\n type=\"checkbox\"\n id={inputId}\n disabled={disabled}\n className={cn(\n 'checkbox__input',\n when(!withCursor, 'checkbox__input--no-events'),\n )}\n {...props}\n />\n {title ? (\n <label className=\"checkbox__label\" htmlFor={inputId}>\n {title.replaceAll('-', '–')}\n </label>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;AAWA,MAAa,gBAAgB,WAC3B,SAAS,cACP,EAAE,WAAW,IAAI,OAAO,MAAM,UAAU,aAAa,MAAM,GAAG,SAC9D,KACA;CACA,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,YAAY,WAAW,KAAK,MAAM,gBAAgB,CAAC;EAAtE,UAAA,CACE,oBAAC,SAAD;GACO;GACL,MAAK;GACL,IAAI;GACM;GACV,WAAW,GACT,mBACA,KAAK,CAAC,YAAY,4BAA4B,CAChD;GACA,GAAI;EACL,CAAA,GACA,QACC,oBAAC,SAAD;GAAO,WAAU;GAAkB,SAAS;GACzC,UAAA,MAAM,WAAW,KAAK,GAAG;EACrB,CAAA,IACL,IACD;;AAET,CACF"}
1
+ {"version":3,"file":"checkbox.js","names":[],"sources":["../../../src/components/checkbox/checkbox.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n type ChangeEvent,\n type InputHTMLAttributes,\n type ReactNode,\n type Ref,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { cn, when } from '@/lib/cn';\nimport './checkbox.scss';\n\nexport interface CheckboxInputProps extends InputHTMLAttributes<HTMLInputElement> {\n label?: ReactNode;\n /** Красная рамка. Внутри `Field` состоянием ошибки распоряжается он. */\n error?: boolean;\n /** «Выбрать все»: IDL `indeterminate`, React его атрибутом не прокидывает. */\n indeterminate?: boolean;\n /**\n * false — `pointer-events: none` на input (`checkbox__input--no-events`).\n * Клик по лейблу по-прежнему переключает.\n */\n clickable?: boolean;\n}\n\nconst assignRef = (\n ref: Ref<HTMLInputElement> | null,\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 mergeDescribedBy = (\n fromField: string | undefined,\n fromProps: string | undefined,\n) => {\n const value = [fromField, fromProps].filter(part => Boolean(part)).join(' ');\n return value || undefined;\n};\n\nexport const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(\n function CheckboxInput(\n {\n className,\n id,\n label,\n error,\n disabled,\n clickable = true,\n indeterminate = false,\n checked,\n onChange,\n ...props\n },\n ref,\n ) {\n const field = useFieldControl();\n const reportValue = field?.reportValue;\n const autoId = useId();\n const inputId = field?.id ?? id ?? autoId;\n const inputRef = useRef<HTMLInputElement>(null);\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\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 if (inputRef.current) inputRef.current.indeterminate = indeterminate;\n }, [indeterminate]);\n\n // Field ждёт строку: отдаём то же, что ушло бы в FormData — 'on' или пусто.\n useEffect(() => {\n const node = inputRef.current;\n if (node) reportValue?.(node.checked ? 'on' : '');\n }, [checked, reportValue]);\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (checked === undefined) {\n reportValue?.(event.target.checked ? 'on' : '');\n }\n onChange?.(event);\n };\n\n const describedBy = field\n ? mergeDescribedBy(field.describedBy, props['aria-describedby'])\n : undefined;\n\n const fieldAttrs: InputHTMLAttributes<HTMLInputElement> = {};\n\n if (field?.required) {\n fieldAttrs.required = true;\n fieldAttrs['aria-required'] = true;\n }\n\n if (isInvalid) fieldAttrs['aria-invalid'] = true;\n if (describedBy) fieldAttrs['aria-describedby'] = describedBy;\n if (field?.errorId) fieldAttrs['aria-errormessage'] = field.errorId;\n\n return (\n <div className={cn('checkbox', className)}>\n <input\n {...props}\n ref={setRef}\n type=\"checkbox\"\n id={inputId}\n disabled={disabled}\n {...(checked !== undefined ? { checked } : {})}\n onChange={handleChange}\n className={cn(\n 'checkbox__input',\n when(!clickable, 'checkbox__input--no-events'),\n )}\n {...fieldAttrs}\n />\n {label ? (\n <label className=\"checkbox__label\" htmlFor={inputId}>\n {label}\n </label>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;;AA6BA,MAAM,aACJ,KACA,SACG;CACH,IAAI,OAAO,QAAQ,YAAY;EAC7B,IAAI,IAAI;EACR;CACF;CAEA,IAAI,KAAK,IAAI,UAAU;AACzB;AAEA,MAAM,oBACJ,WACA,cACG;CAEH,OADc,CAAC,WAAW,SAAS,CAAC,CAAC,QAAO,SAAQ,QAAQ,IAAI,CAAC,CAAC,CAAC,KAAK,GAC7D,KAAK,KAAA;AAClB;AAEA,MAAa,gBAAgB,WAC3B,SAAS,cACP,EACE,WACA,IACA,OACA,OACA,UACA,YAAY,MACZ,gBAAgB,OAChB,SACA,UACA,GAAG,SAEL,KACA;CACA,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,cAAc,OAAO;CAC3B,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,OAAO,MAAM,MAAM;CACnC,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,YAAY,QAAQ,KAAK,KAAK,QAAQ,OAAO,OAAO;CAE1D,MAAM,SAAS,aACZ,SAAkC;EACjC,SAAS,UAAU;EACnB,UAAU,KAAK,IAAI;CACrB,GACA,CAAC,GAAG,CACN;CAEA,gBAAgB;EACd,IAAI,SAAS,SAAS,SAAS,QAAQ,gBAAgB;CACzD,GAAG,CAAC,aAAa,CAAC;CAGlB,gBAAgB;EACd,MAAM,OAAO,SAAS;EACtB,IAAI,MAAM,cAAc,KAAK,UAAU,OAAO,EAAE;CAClD,GAAG,CAAC,SAAS,WAAW,CAAC;CAEzB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,YAAY,KAAA,GACd,cAAc,MAAM,OAAO,UAAU,OAAO,EAAE;EAEhD,WAAW,KAAK;CAClB;CAEA,MAAM,cAAc,QAChB,iBAAiB,MAAM,aAAa,MAAM,mBAAmB,IAC7D,KAAA;CAEJ,MAAM,aAAoD,CAAC;CAE3D,IAAI,OAAO,UAAU;EACnB,WAAW,WAAW;EACtB,WAAW,mBAAmB;CAChC;CAEA,IAAI,WAAW,WAAW,kBAAkB;CAC5C,IAAI,aAAa,WAAW,sBAAsB;CAClD,IAAI,OAAO,SAAS,WAAW,uBAAuB,MAAM;CAE5D,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,YAAY,SAAS;EAAxC,UAAA,CACE,oBAAC,SAAD;GACE,GAAI;GACJ,KAAK;GACL,MAAK;GACL,IAAI;GACM;GACV,GAAK,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC5C,UAAU;GACV,WAAW,GACT,mBACA,KAAK,CAAC,WAAW,4BAA4B,CAC/C;GACA,GAAI;EACL,CAAA,GACA,QACC,oBAAC,SAAD;GAAO,WAAU;GAAkB,SAAS;GACzC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
@@ -1,34 +1,31 @@
1
- @function get-check-icon($color: 'fff') {
2
- @return url("data:image/svg+xml,%3Csvg fill='%23#{$color}' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='20px' height='20px'%3E%3Cpath d='M15.71,6.29c-0.39-0.39-1.02-0.39-1.41,0c-0.31,0.31-3.76,3.76-5.29,5.29c-0.81-0.81-2.01-2.01-2.29-2.29c-0.39-0.39-1.02-0.39-1.41,0c-0.39,0.39-0.39,1.02,0,1.41c0.39,0.39,2.61,2.61,3,3c0.39,0.39,1.02,0.39,1.41,0c0.39-0.39,5.61-5.61,6-6C16.1,7.32,16.1,6.68,15.71,6.29z'/%3E%3C/svg%3E");
1
+ // data-uri не читает custom properties, поэтому галочка — маска: цвет берёт background-color.
2
+ @function get-check-icon() {
3
+ @return url("data:image/svg+xml,%3Csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='20px' height='20px'%3E%3Cpath d='M15.71,6.29c-0.39-0.39-1.02-0.39-1.41,0c-0.31,0.31-3.76,3.76-5.29,5.29c-0.81-0.81-2.01-2.01-2.29-2.29c-0.39-0.39-1.02-0.39-1.41,0c-0.39,0.39-0.39,1.02,0,1.41c0.39,0.39,2.61,2.61,3,3c0.39,0.39,1.02,0.39,1.41,0c0.39-0.39,5.61-5.61,6-6C16.1,7.32,16.1,6.68,15.71,6.29z'/%3E%3C/svg%3E");
3
4
  }
4
5
 
5
6
  .checkbox {
6
7
  display: flex;
7
8
  align-items: flex-start;
8
-
9
- &--hide {
10
- display: none;
11
- }
12
9
  }
13
10
 
14
11
  .checkbox__label {
15
- font-size: 14px;
16
- line-height: 20px;
12
+ font-size: var(--text-sm);
13
+ line-height: var(--text-sm--line-height);
17
14
  display: inline-block;
18
15
  vertical-align: top;
19
- padding-left: 5px;
16
+ padding-left: var(--space-1);
20
17
  user-select: none;
21
18
  cursor: pointer;
22
19
  }
23
20
 
24
21
  .checkbox__input {
25
- --active: #000;
26
- --active-inner: #fff;
27
- --border: #e5e5e5;
28
- --border-hover: #000;
29
- --background: #fff;
30
- --disabled: #e5e5e5;
31
- --disabled-inner: #e5e5e5;
22
+ --active: var(--color-primary);
23
+ --active-inner: var(--color-primary-foreground);
24
+ --border: var(--color-muted);
25
+ --border-hover: var(--color-foreground);
26
+ --background: var(--color-background);
27
+ --disabled: var(--color-muted);
28
+ --disabled-inner: var(--color-muted);
32
29
  box-sizing: border-box;
33
30
  appearance: none;
34
31
  width: 20px;
@@ -41,7 +38,7 @@
41
38
  flex-shrink: 0;
42
39
  border: 1px solid var(--bc, var(--border));
43
40
  background: var(--b, var(--background));
44
- border-radius: 5px;
41
+ border-radius: var(--radius-sm);
45
42
  transition:
46
43
  background 0.3s,
47
44
  border-color 0.3s,
@@ -58,12 +55,16 @@
58
55
  position: absolute;
59
56
  transition: opacity var(--d-o, 0.2s);
60
57
  opacity: var(--o, 0);
61
- background-position: center;
62
- background-image: get-check-icon();
58
+ background-color: var(--active-inner);
59
+ -webkit-mask: get-check-icon() center / 20px 20px no-repeat;
60
+ mask: get-check-icon() center / 20px 20px no-repeat;
63
61
  }
64
62
 
65
- &:required:not(:hover):not(:checked):not(:disabled) {
66
- border: 1px solid var(--color-red);
63
+ // Красным красит состояние ошибки, а не сам по себе required: моментом ошибки
64
+ // внутри Field распоряжается он. Рамка держится и под курсором — ховер только
65
+ // добавляет к ней толщину.
66
+ &[aria-invalid='true']:not(:disabled) {
67
+ border-color: var(--color-destructive);
67
68
  }
68
69
 
69
70
  &:checked {
@@ -72,6 +73,21 @@
72
73
  --o: 1;
73
74
  }
74
75
 
76
+ &:indeterminate {
77
+ --b: var(--active);
78
+ --bc: var(--active);
79
+ --o: 1;
80
+
81
+ &::after {
82
+ -webkit-mask: none;
83
+ mask: none;
84
+ top: 9px;
85
+ right: 4px;
86
+ bottom: 9px;
87
+ left: 4px;
88
+ }
89
+ }
90
+
75
91
  &:hover:not(:checked):not(:disabled) {
76
92
  border-width: 2px;
77
93
  --bc: var(--active);
@@ -82,6 +98,15 @@
82
98
  border-width: 2px;
83
99
  }
84
100
 
101
+ &:focus:not(:focus-visible) {
102
+ outline: none;
103
+ }
104
+
105
+ &:focus-visible {
106
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
107
+ outline-offset: var(--focus-ring-offset);
108
+ }
109
+
85
110
  &:disabled {
86
111
  --b: var(--disabled);
87
112
  border: 1px solid var(--disabled);
@@ -90,14 +115,11 @@
90
115
 
91
116
  &:checked {
92
117
  --b: var(--disabled-inner);
93
-
94
- &::after {
95
- background-image: get-check-icon('727272');
96
- }
118
+ --active-inner: var(--color-muted-foreground);
97
119
  }
98
120
 
99
121
  & + .checkbox__label {
100
- color: var(--color-gray);
122
+ color: var(--color-muted-foreground);
101
123
  cursor: not-allowed;
102
124
  }
103
125
  }
@@ -106,3 +128,10 @@
106
128
  pointer-events: none;
107
129
  }
108
130
  }
131
+
132
+ @media (prefers-reduced-motion: reduce) {
133
+ .checkbox__input,
134
+ .checkbox__input::after {
135
+ transition: none;
136
+ }
137
+ }
@@ -0,0 +1,9 @@
1
+ let react = require("react");
2
+ // src/components/field/field-context.ts
3
+ const FieldControlReactContext = (0, react.createContext)(null);
4
+ const FieldControlProvider = FieldControlReactContext.Provider;
5
+ const useFieldControl = () => (0, react.useContext)(FieldControlReactContext);
6
+ exports.FieldControlProvider = FieldControlProvider;
7
+ exports.useFieldControl = useFieldControl;
8
+
9
+ //# sourceMappingURL=field-context.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-context.cjs","names":["createContext","useContext"],"sources":["../../../src/components/field/field-context.ts"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nexport interface FieldControlContext {\n id: string;\n /** Для контролов без собственного лейбла: группа ссылается на него aria-labelledby. */\n labelId?: string;\n invalid: boolean;\n required: boolean;\n describedBy?: string;\n errorId?: string;\n /** Контрол сообщает показанное значение: текст поля или 'on' у отмеченного чекбокса. */\n reportValue?: (value: string) => void;\n}\n\nconst FieldControlReactContext = createContext<FieldControlContext | null>(\n null,\n);\n\nexport const FieldControlProvider = FieldControlReactContext.Provider;\n\nexport const useFieldControl = (): FieldControlContext | null =>\n useContext(FieldControlReactContext);\n"],"mappings":";;AAcA,MAAM,4BAAA,GAA2BA,MAAAA,cAAAA,CAC/B,IACF;AAEA,MAAa,uBAAuB,yBAAyB;AAE7D,MAAa,yBAAA,GACXC,MAAAA,WAAAA,CAAW,wBAAwB"}
@@ -0,0 +1,8 @@
1
+ import { createContext, useContext } from "react";
2
+ // src/components/field/field-context.ts
3
+ const FieldControlReactContext = createContext(null);
4
+ const FieldControlProvider = FieldControlReactContext.Provider;
5
+ const useFieldControl = () => useContext(FieldControlReactContext);
6
+ export { FieldControlProvider, useFieldControl };
7
+
8
+ //# sourceMappingURL=field-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-context.js","names":[],"sources":["../../../src/components/field/field-context.ts"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nexport interface FieldControlContext {\n id: string;\n /** Для контролов без собственного лейбла: группа ссылается на него aria-labelledby. */\n labelId?: string;\n invalid: boolean;\n required: boolean;\n describedBy?: string;\n errorId?: string;\n /** Контрол сообщает показанное значение: текст поля или 'on' у отмеченного чекбокса. */\n reportValue?: (value: string) => void;\n}\n\nconst FieldControlReactContext = createContext<FieldControlContext | null>(\n null,\n);\n\nexport const FieldControlProvider = FieldControlReactContext.Provider;\n\nexport const useFieldControl = (): FieldControlContext | null =>\n useContext(FieldControlReactContext);\n"],"mappings":";;AAcA,MAAM,2BAA2B,cAC/B,IACF;AAEA,MAAa,uBAAuB,yBAAyB;AAE7D,MAAa,wBACX,WAAW,wBAAwB"}
@@ -0,0 +1,105 @@
1
+ const require_cn = require("../../lib/cn.cjs");
2
+ const require_field_context = require("./field-context.cjs");
3
+ let react = require("react");
4
+ let react_jsx_runtime = require("react/jsx-runtime");
5
+ require("./field.scss");
6
+ // src/components/field/field.tsx
7
+ const isFilled = (value) => value.trim() !== "";
8
+ const requiredText = (label, message) => {
9
+ if (message) return message;
10
+ if (typeof label === "string") return `Поле «${label}» обязательно`;
11
+ return "Обязательное поле";
12
+ };
13
+ const Field = ({ label, hint, hintPlacement = "bottom", error, required = false, requiredMessage, className, children }) => {
14
+ const inputId = (0, react.useId)();
15
+ const labelId = `${inputId}-label`;
16
+ const rootRef = (0, react.useRef)(null);
17
+ const hadValueRef = (0, react.useRef)(false);
18
+ const [showRequired, setShowRequired] = (0, react.useState)(false);
19
+ const requiredError = required && showRequired ? requiredText(label, requiredMessage) : null;
20
+ const visibleError = error ?? requiredError;
21
+ const invalid = Boolean(visibleError);
22
+ const hintId = hint ? `${inputId}-hint` : void 0;
23
+ const errorId = invalid ? `${inputId}-error` : void 0;
24
+ const describedBy = [errorId, hintId].filter(Boolean).join(" ");
25
+ const reportValue = (0, react.useCallback)((value) => {
26
+ if (!required) return;
27
+ if (isFilled(value)) {
28
+ hadValueRef.current = true;
29
+ setShowRequired(false);
30
+ return;
31
+ }
32
+ if (hadValueRef.current) setShowRequired(true);
33
+ }, [required]);
34
+ /**
35
+ * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,
36
+ * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки
37
+ * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,
38
+ * поэтому одного слушателя на корне поля хватает на любой контрол внутри.
39
+ * Реагируем только на пустое значение: формат, длина и прочие нарушения
40
+ * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.
41
+ */
42
+ (0, react.useEffect)(() => {
43
+ const node = rootRef.current;
44
+ if (!node || !required) return;
45
+ const handleInvalid = (event) => {
46
+ const target = event.target;
47
+ if ((target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) && target.validity.valueMissing) setShowRequired(true);
48
+ };
49
+ node.addEventListener("invalid", handleInvalid, true);
50
+ return () => node.removeEventListener("invalid", handleInvalid, true);
51
+ }, [required]);
52
+ const contextValue = (0, react.useMemo)(() => ({
53
+ id: inputId,
54
+ labelId,
55
+ invalid,
56
+ required,
57
+ describedBy: describedBy || void 0,
58
+ errorId,
59
+ reportValue
60
+ }), [
61
+ describedBy,
62
+ errorId,
63
+ inputId,
64
+ invalid,
65
+ labelId,
66
+ reportValue,
67
+ required
68
+ ]);
69
+ const hintByPlacement = {
70
+ label: null,
71
+ bottom: null
72
+ };
73
+ if (hint) hintByPlacement[hintPlacement] = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
74
+ id: hintId,
75
+ className: require_cn.cn("field__hint", require_cn.when(hintPlacement === "label", "field__hint--label")),
76
+ children: hint
77
+ });
78
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
79
+ ref: rootRef,
80
+ className: require_cn.cn("field", className, require_cn.when(Boolean(hint) && hintPlacement === "label", "field--hint-label")),
81
+ children: [
82
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
83
+ id: labelId,
84
+ className: "field__label",
85
+ htmlFor: inputId,
86
+ children: label
87
+ }),
88
+ hintByPlacement.label,
89
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_field_context.FieldControlProvider, {
90
+ value: contextValue,
91
+ children
92
+ }),
93
+ visibleError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
94
+ id: errorId,
95
+ className: "field__error",
96
+ role: "alert",
97
+ children: visibleError
98
+ }) : null,
99
+ hintByPlacement.bottom
100
+ ]
101
+ });
102
+ };
103
+ exports.Field = Field;
104
+
105
+ //# sourceMappingURL=field.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.cjs","names":["useId","useRef","useState","useCallback","useMemo","cn","when","FieldControlProvider"],"sources":["../../../src/components/field/field.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport {\n FieldControlProvider,\n type FieldControlContext,\n} from './field-context';\nimport './field.scss';\n\nexport type FieldHintPlacement = 'bottom' | 'label';\n\nexport interface FieldProps {\n label: ReactNode;\n hint?: ReactNode;\n /** `bottom` — под полем, `label` — сразу под лейблом. */\n hintPlacement?: FieldHintPlacement;\n error?: ReactNode;\n /** Пустое значение после того, как в поле уже что-то было. */\n required?: boolean;\n /**\n * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.\n * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.\n */\n requiredMessage?: ReactNode;\n className?: string;\n /**\n * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке\n * перебивается, а два контрола внутри получат одинаковый.\n */\n children: ReactNode;\n}\n\nconst isFilled = (value: string) => value.trim() !== '';\n\nconst requiredText = (label: ReactNode, message: ReactNode | undefined) => {\n if (message) return message;\n if (typeof label === 'string') return `Поле «${label}» обязательно`;\n return 'Обязательное поле';\n};\n\nexport const Field = ({\n label,\n hint,\n hintPlacement = 'bottom',\n error,\n required = false,\n requiredMessage,\n className,\n children,\n}: FieldProps) => {\n const inputId = useId();\n const labelId = `${inputId}-label`;\n const rootRef = useRef<HTMLDivElement>(null);\n // ponytail: value/defaultValue больше не читаем с ребёнка. Контрол зовёт\n // reportValue из эффекта — до первого эффекта hadValue ещё false.\n const hadValueRef = useRef(false);\n const [showRequired, setShowRequired] = useState(false);\n const requiredError =\n required && showRequired ? requiredText(label, requiredMessage) : null;\n const visibleError = error ?? requiredError;\n const invalid = Boolean(visibleError);\n const hintId = hint ? `${inputId}-hint` : undefined;\n const errorId = invalid ? `${inputId}-error` : undefined;\n const describedBy = [errorId, hintId].filter(Boolean).join(' ');\n\n const reportValue = useCallback(\n (value: string) => {\n if (!required) return;\n\n if (isFilled(value)) {\n hadValueRef.current = true;\n setShowRequired(false);\n return;\n }\n\n if (hadValueRef.current) setShowRequired(true);\n },\n [required],\n );\n\n /**\n * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,\n * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки\n * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,\n * поэтому одного слушателя на корне поля хватает на любой контрол внутри.\n * Реагируем только на пустое значение: формат, длина и прочие нарушения\n * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.\n */\n useEffect(() => {\n const node = rootRef.current;\n if (!node || !required) return;\n\n const handleInvalid = (event: Event) => {\n const target = event.target;\n const missing =\n (target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement) &&\n target.validity.valueMissing;\n\n if (missing) setShowRequired(true);\n };\n\n node.addEventListener('invalid', handleInvalid, true);\n return () => node.removeEventListener('invalid', handleInvalid, true);\n }, [required]);\n\n const contextValue = useMemo<FieldControlContext>(\n () => ({\n id: inputId,\n labelId,\n invalid,\n required,\n describedBy: describedBy || undefined,\n errorId,\n reportValue,\n }),\n [describedBy, errorId, inputId, invalid, labelId, reportValue, required],\n );\n\n const hintByPlacement: Record<FieldHintPlacement, ReactNode> = {\n label: null,\n bottom: null,\n };\n\n if (hint) {\n hintByPlacement[hintPlacement] = (\n <p\n id={hintId}\n className={cn(\n 'field__hint',\n when(hintPlacement === 'label', 'field__hint--label'),\n )}\n >\n {hint}\n </p>\n );\n }\n\n return (\n <div\n ref={rootRef}\n className={cn(\n 'field',\n className,\n when(Boolean(hint) && hintPlacement === 'label', 'field--hint-label'),\n )}\n >\n <label id={labelId} className=\"field__label\" htmlFor={inputId}>\n {label}\n </label>\n {hintByPlacement.label}\n <FieldControlProvider value={contextValue}>\n {children}\n </FieldControlProvider>\n {visibleError ? (\n <p id={errorId} className=\"field__error\" role=\"alert\">\n {visibleError}\n </p>\n ) : null}\n {hintByPlacement.bottom}\n </div>\n );\n};\n"],"mappings":";;;;;;AAwCA,MAAM,YAAY,UAAkB,MAAM,KAAK,MAAM;AAErD,MAAM,gBAAgB,OAAkB,YAAmC;CACzE,IAAI,SAAS,OAAO;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO,SAAS,MAAM;CACrD,OAAO;AACT;AAEA,MAAa,SAAS,EACpB,OACA,MACA,gBAAgB,UAChB,OACA,WAAW,OACX,iBACA,WACA,eACgB;CAChB,MAAM,WAAA,GAAUA,MAAAA,MAAAA,CAAM;CACtB,MAAM,UAAU,GAAG,QAAQ;CAC3B,MAAM,WAAA,GAAUC,MAAAA,OAAAA,CAAuB,IAAI;CAG3C,MAAM,eAAA,GAAcA,MAAAA,OAAAA,CAAO,KAAK;CAChC,MAAM,CAAC,cAAc,oBAAA,GAAmBC,MAAAA,SAAAA,CAAS,KAAK;CACtD,MAAM,gBACJ,YAAY,eAAe,aAAa,OAAO,eAAe,IAAI;CACpE,MAAM,eAAe,SAAS;CAC9B,MAAM,UAAU,QAAQ,YAAY;CACpC,MAAM,SAAS,OAAO,GAAG,QAAQ,SAAS,KAAA;CAC1C,MAAM,UAAU,UAAU,GAAG,QAAQ,UAAU,KAAA;CAC/C,MAAM,cAAc,CAAC,SAAS,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAE9D,MAAM,eAAA,GAAcC,MAAAA,YAAAA,EACjB,UAAkB;EACjB,IAAI,CAAC,UAAU;EAEf,IAAI,SAAS,KAAK,GAAG;GACnB,YAAY,UAAU;GACtB,gBAAgB,KAAK;GACrB;EACF;EAEA,IAAI,YAAY,SAAS,gBAAgB,IAAI;CAC/C,GACA,CAAC,QAAQ,CACX;;;;;;;;;CAUA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,QAAQ,CAAC,UAAU;EAExB,MAAM,iBAAiB,UAAiB;GACtC,MAAM,SAAS,MAAM;GAOrB,KALG,kBAAkB,oBACjB,kBAAkB,uBAClB,kBAAkB,sBACpB,OAAO,SAAS,cAEL,gBAAgB,IAAI;EACnC;EAEA,KAAK,iBAAiB,WAAW,eAAe,IAAI;EACpD,aAAa,KAAK,oBAAoB,WAAW,eAAe,IAAI;CACtE,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,gBAAA,GAAeC,MAAAA,QAAAA,QACZ;EACL,IAAI;EACJ;EACA;EACA;EACA,aAAa,eAAe,KAAA;EAC5B;EACA;CACF,IACA;EAAC;EAAa;EAAS;EAAS;EAAS;EAAS;EAAa;CAAQ,CACzE;CAEA,MAAM,kBAAyD;EAC7D,OAAO;EACP,QAAQ;CACV;CAEA,IAAI,MACF,gBAAgB,iBACd,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;EACE,IAAI;EACJ,WAAWC,WAAAA,GACT,eACAC,WAAAA,KAAK,kBAAkB,SAAS,oBAAoB,CACtD;EAEC,UAAA;CACA,CAAA;CAIP,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EACE,KAAK;EACL,WAAWD,WAAAA,GACT,SACA,WACAC,WAAAA,KAAK,QAAQ,IAAI,KAAK,kBAAkB,SAAS,mBAAmB,CACtE;EANF,UAAA;GAQE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;IAAO,IAAI;IAAS,WAAU;IAAe,SAAS;IACnD,UAAA;GACI,CAAA;GACN,gBAAgB;GACjB,iBAAA,GAAA,kBAAA,IAAA,CAACC,sBAAAA,sBAAD;IAAsB,OAAO;IAC1B;GACmB,CAAA;GACrB,eACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;IAAG,IAAI;IAAS,WAAU;IAAe,MAAK;IAC3C,UAAA;GACA,CAAA,IACD;GACH,gBAAgB;EACd;;AAET"}
@@ -0,0 +1,24 @@
1
+ import { ReactNode } from "react";
2
+ export type FieldHintPlacement = 'bottom' | 'label';
3
+ export interface FieldProps {
4
+ label: ReactNode;
5
+ hint?: ReactNode;
6
+ /** `bottom` — под полем, `label` — сразу под лейблом. */
7
+ hintPlacement?: FieldHintPlacement;
8
+ error?: ReactNode;
9
+ /** Пустое значение после того, как в поле уже что-то было. */
10
+ required?: boolean;
11
+ /**
12
+ * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.
13
+ * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.
14
+ */
15
+ requiredMessage?: ReactNode;
16
+ className?: string;
17
+ /**
18
+ * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке
19
+ * перебивается, а два контрола внутри получат одинаковый.
20
+ */
21
+ children: ReactNode;
22
+ }
23
+ export declare const Field: ({ label, hint, hintPlacement, error, required, requiredMessage, className, children }: FieldProps) => import("react").JSX.Element;
24
+ //# sourceMappingURL=field.d.cts.map
@@ -0,0 +1,24 @@
1
+ import { ReactNode } from "react";
2
+ export type FieldHintPlacement = 'bottom' | 'label';
3
+ export interface FieldProps {
4
+ label: ReactNode;
5
+ hint?: ReactNode;
6
+ /** `bottom` — под полем, `label` — сразу под лейблом. */
7
+ hintPlacement?: FieldHintPlacement;
8
+ error?: ReactNode;
9
+ /** Пустое значение после того, как в поле уже что-то было. */
10
+ required?: boolean;
11
+ /**
12
+ * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.
13
+ * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.
14
+ */
15
+ requiredMessage?: ReactNode;
16
+ className?: string;
17
+ /**
18
+ * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке
19
+ * перебивается, а два контрола внутри получат одинаковый.
20
+ */
21
+ children: ReactNode;
22
+ }
23
+ export declare const Field: ({ label, hint, hintPlacement, error, required, requiredMessage, className, children }: FieldProps) => import("react").JSX.Element;
24
+ //# sourceMappingURL=field.d.ts.map
@@ -0,0 +1,105 @@
1
+ import { cn, when } from "../../lib/cn.js";
2
+ import { FieldControlProvider } from "./field-context.js";
3
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import "./field.scss";
6
+ // src/components/field/field.tsx
7
+ const isFilled = (value) => value.trim() !== "";
8
+ const requiredText = (label, message) => {
9
+ if (message) return message;
10
+ if (typeof label === "string") return `Поле «${label}» обязательно`;
11
+ return "Обязательное поле";
12
+ };
13
+ const Field = ({ label, hint, hintPlacement = "bottom", error, required = false, requiredMessage, className, children }) => {
14
+ const inputId = useId();
15
+ const labelId = `${inputId}-label`;
16
+ const rootRef = useRef(null);
17
+ const hadValueRef = useRef(false);
18
+ const [showRequired, setShowRequired] = useState(false);
19
+ const requiredError = required && showRequired ? requiredText(label, requiredMessage) : null;
20
+ const visibleError = error ?? requiredError;
21
+ const invalid = Boolean(visibleError);
22
+ const hintId = hint ? `${inputId}-hint` : void 0;
23
+ const errorId = invalid ? `${inputId}-error` : void 0;
24
+ const describedBy = [errorId, hintId].filter(Boolean).join(" ");
25
+ const reportValue = useCallback((value) => {
26
+ if (!required) return;
27
+ if (isFilled(value)) {
28
+ hadValueRef.current = true;
29
+ setShowRequired(false);
30
+ return;
31
+ }
32
+ if (hadValueRef.current) setShowRequired(true);
33
+ }, [required]);
34
+ /**
35
+ * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,
36
+ * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки
37
+ * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,
38
+ * поэтому одного слушателя на корне поля хватает на любой контрол внутри.
39
+ * Реагируем только на пустое значение: формат, длина и прочие нарушения
40
+ * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.
41
+ */
42
+ useEffect(() => {
43
+ const node = rootRef.current;
44
+ if (!node || !required) return;
45
+ const handleInvalid = (event) => {
46
+ const target = event.target;
47
+ if ((target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) && target.validity.valueMissing) setShowRequired(true);
48
+ };
49
+ node.addEventListener("invalid", handleInvalid, true);
50
+ return () => node.removeEventListener("invalid", handleInvalid, true);
51
+ }, [required]);
52
+ const contextValue = useMemo(() => ({
53
+ id: inputId,
54
+ labelId,
55
+ invalid,
56
+ required,
57
+ describedBy: describedBy || void 0,
58
+ errorId,
59
+ reportValue
60
+ }), [
61
+ describedBy,
62
+ errorId,
63
+ inputId,
64
+ invalid,
65
+ labelId,
66
+ reportValue,
67
+ required
68
+ ]);
69
+ const hintByPlacement = {
70
+ label: null,
71
+ bottom: null
72
+ };
73
+ if (hint) hintByPlacement[hintPlacement] = /* @__PURE__ */ jsx("p", {
74
+ id: hintId,
75
+ className: cn("field__hint", when(hintPlacement === "label", "field__hint--label")),
76
+ children: hint
77
+ });
78
+ return /* @__PURE__ */ jsxs("div", {
79
+ ref: rootRef,
80
+ className: cn("field", className, when(Boolean(hint) && hintPlacement === "label", "field--hint-label")),
81
+ children: [
82
+ /* @__PURE__ */ jsx("label", {
83
+ id: labelId,
84
+ className: "field__label",
85
+ htmlFor: inputId,
86
+ children: label
87
+ }),
88
+ hintByPlacement.label,
89
+ /* @__PURE__ */ jsx(FieldControlProvider, {
90
+ value: contextValue,
91
+ children
92
+ }),
93
+ visibleError ? /* @__PURE__ */ jsx("p", {
94
+ id: errorId,
95
+ className: "field__error",
96
+ role: "alert",
97
+ children: visibleError
98
+ }) : null,
99
+ hintByPlacement.bottom
100
+ ]
101
+ });
102
+ };
103
+ export { Field };
104
+
105
+ //# sourceMappingURL=field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.js","names":[],"sources":["../../../src/components/field/field.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport {\n FieldControlProvider,\n type FieldControlContext,\n} from './field-context';\nimport './field.scss';\n\nexport type FieldHintPlacement = 'bottom' | 'label';\n\nexport interface FieldProps {\n label: ReactNode;\n hint?: ReactNode;\n /** `bottom` — под полем, `label` — сразу под лейблом. */\n hintPlacement?: FieldHintPlacement;\n error?: ReactNode;\n /** Пустое значение после того, как в поле уже что-то было. */\n required?: boolean;\n /**\n * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.\n * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.\n */\n requiredMessage?: ReactNode;\n className?: string;\n /**\n * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке\n * перебивается, а два контрола внутри получат одинаковый.\n */\n children: ReactNode;\n}\n\nconst isFilled = (value: string) => value.trim() !== '';\n\nconst requiredText = (label: ReactNode, message: ReactNode | undefined) => {\n if (message) return message;\n if (typeof label === 'string') return `Поле «${label}» обязательно`;\n return 'Обязательное поле';\n};\n\nexport const Field = ({\n label,\n hint,\n hintPlacement = 'bottom',\n error,\n required = false,\n requiredMessage,\n className,\n children,\n}: FieldProps) => {\n const inputId = useId();\n const labelId = `${inputId}-label`;\n const rootRef = useRef<HTMLDivElement>(null);\n // ponytail: value/defaultValue больше не читаем с ребёнка. Контрол зовёт\n // reportValue из эффекта — до первого эффекта hadValue ещё false.\n const hadValueRef = useRef(false);\n const [showRequired, setShowRequired] = useState(false);\n const requiredError =\n required && showRequired ? requiredText(label, requiredMessage) : null;\n const visibleError = error ?? requiredError;\n const invalid = Boolean(visibleError);\n const hintId = hint ? `${inputId}-hint` : undefined;\n const errorId = invalid ? `${inputId}-error` : undefined;\n const describedBy = [errorId, hintId].filter(Boolean).join(' ');\n\n const reportValue = useCallback(\n (value: string) => {\n if (!required) return;\n\n if (isFilled(value)) {\n hadValueRef.current = true;\n setShowRequired(false);\n return;\n }\n\n if (hadValueRef.current) setShowRequired(true);\n },\n [required],\n );\n\n /**\n * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,\n * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки\n * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,\n * поэтому одного слушателя на корне поля хватает на любой контрол внутри.\n * Реагируем только на пустое значение: формат, длина и прочие нарушения\n * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.\n */\n useEffect(() => {\n const node = rootRef.current;\n if (!node || !required) return;\n\n const handleInvalid = (event: Event) => {\n const target = event.target;\n const missing =\n (target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement) &&\n target.validity.valueMissing;\n\n if (missing) setShowRequired(true);\n };\n\n node.addEventListener('invalid', handleInvalid, true);\n return () => node.removeEventListener('invalid', handleInvalid, true);\n }, [required]);\n\n const contextValue = useMemo<FieldControlContext>(\n () => ({\n id: inputId,\n labelId,\n invalid,\n required,\n describedBy: describedBy || undefined,\n errorId,\n reportValue,\n }),\n [describedBy, errorId, inputId, invalid, labelId, reportValue, required],\n );\n\n const hintByPlacement: Record<FieldHintPlacement, ReactNode> = {\n label: null,\n bottom: null,\n };\n\n if (hint) {\n hintByPlacement[hintPlacement] = (\n <p\n id={hintId}\n className={cn(\n 'field__hint',\n when(hintPlacement === 'label', 'field__hint--label'),\n )}\n >\n {hint}\n </p>\n );\n }\n\n return (\n <div\n ref={rootRef}\n className={cn(\n 'field',\n className,\n when(Boolean(hint) && hintPlacement === 'label', 'field--hint-label'),\n )}\n >\n <label id={labelId} className=\"field__label\" htmlFor={inputId}>\n {label}\n </label>\n {hintByPlacement.label}\n <FieldControlProvider value={contextValue}>\n {children}\n </FieldControlProvider>\n {visibleError ? (\n <p id={errorId} className=\"field__error\" role=\"alert\">\n {visibleError}\n </p>\n ) : null}\n {hintByPlacement.bottom}\n </div>\n );\n};\n"],"mappings":";;;;;;AAwCA,MAAM,YAAY,UAAkB,MAAM,KAAK,MAAM;AAErD,MAAM,gBAAgB,OAAkB,YAAmC;CACzE,IAAI,SAAS,OAAO;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO,SAAS,MAAM;CACrD,OAAO;AACT;AAEA,MAAa,SAAS,EACpB,OACA,MACA,gBAAgB,UAChB,OACA,WAAW,OACX,iBACA,WACA,eACgB;CAChB,MAAM,UAAU,MAAM;CACtB,MAAM,UAAU,GAAG,QAAQ;CAC3B,MAAM,UAAU,OAAuB,IAAI;CAG3C,MAAM,cAAc,OAAO,KAAK;CAChC,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,gBACJ,YAAY,eAAe,aAAa,OAAO,eAAe,IAAI;CACpE,MAAM,eAAe,SAAS;CAC9B,MAAM,UAAU,QAAQ,YAAY;CACpC,MAAM,SAAS,OAAO,GAAG,QAAQ,SAAS,KAAA;CAC1C,MAAM,UAAU,UAAU,GAAG,QAAQ,UAAU,KAAA;CAC/C,MAAM,cAAc,CAAC,SAAS,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAE9D,MAAM,cAAc,aACjB,UAAkB;EACjB,IAAI,CAAC,UAAU;EAEf,IAAI,SAAS,KAAK,GAAG;GACnB,YAAY,UAAU;GACtB,gBAAgB,KAAK;GACrB;EACF;EAEA,IAAI,YAAY,SAAS,gBAAgB,IAAI;CAC/C,GACA,CAAC,QAAQ,CACX;;;;;;;;;CAUA,gBAAgB;EACd,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,QAAQ,CAAC,UAAU;EAExB,MAAM,iBAAiB,UAAiB;GACtC,MAAM,SAAS,MAAM;GAOrB,KALG,kBAAkB,oBACjB,kBAAkB,uBAClB,kBAAkB,sBACpB,OAAO,SAAS,cAEL,gBAAgB,IAAI;EACnC;EAEA,KAAK,iBAAiB,WAAW,eAAe,IAAI;EACpD,aAAa,KAAK,oBAAoB,WAAW,eAAe,IAAI;CACtE,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,eAAe,eACZ;EACL,IAAI;EACJ;EACA;EACA;EACA,aAAa,eAAe,KAAA;EAC5B;EACA;CACF,IACA;EAAC;EAAa;EAAS;EAAS;EAAS;EAAS;EAAa;CAAQ,CACzE;CAEA,MAAM,kBAAyD;EAC7D,OAAO;EACP,QAAQ;CACV;CAEA,IAAI,MACF,gBAAgB,iBACd,oBAAC,KAAD;EACE,IAAI;EACJ,WAAW,GACT,eACA,KAAK,kBAAkB,SAAS,oBAAoB,CACtD;EAEC,UAAA;CACA,CAAA;CAIP,OACE,qBAAC,OAAD;EACE,KAAK;EACL,WAAW,GACT,SACA,WACA,KAAK,QAAQ,IAAI,KAAK,kBAAkB,SAAS,mBAAmB,CACtE;EANF,UAAA;GAQE,oBAAC,SAAD;IAAO,IAAI;IAAS,WAAU;IAAe,SAAS;IACnD,UAAA;GACI,CAAA;GACN,gBAAgB;GACjB,oBAAC,sBAAD;IAAsB,OAAO;IAC1B;GACmB,CAAA;GACrB,eACC,oBAAC,KAAD;IAAG,IAAI;IAAS,WAAU;IAAe,MAAK;IAC3C,UAAA;GACA,CAAA,IACD;GACH,gBAAgB;EACd;;AAET"}