@de-kit/st-ui 0.1.8 → 0.1.10

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 (44) hide show
  1. package/README.md +145 -143
  2. package/dist/components/button/button.cjs +26 -13
  3. package/dist/components/button/button.cjs.map +1 -1
  4. package/dist/components/button/button.d.cts +9 -8
  5. package/dist/components/button/button.d.ts +9 -8
  6. package/dist/components/button/button.js +29 -16
  7. package/dist/components/button/button.js.map +1 -1
  8. package/dist/components/button/button.scss +213 -147
  9. package/dist/components/button/index.d.cts +2 -2
  10. package/dist/components/button/index.d.ts +2 -2
  11. package/dist/components/checkbox/checkbox.cjs.map +1 -1
  12. package/dist/components/checkbox/checkbox.js.map +1 -1
  13. package/dist/components/checkbox/checkbox.scss +137 -137
  14. package/dist/components/field/field-context.cjs.map +1 -1
  15. package/dist/components/field/field-context.js.map +1 -1
  16. package/dist/components/field/field.cjs.map +1 -1
  17. package/dist/components/field/field.js.map +1 -1
  18. package/dist/components/field/field.scss +43 -43
  19. package/dist/components/input/input.cjs.map +1 -1
  20. package/dist/components/input/input.js.map +1 -1
  21. package/dist/components/input/input.scss +127 -127
  22. package/dist/components/radio/radio.cjs.map +1 -1
  23. package/dist/components/radio/radio.js.map +1 -1
  24. package/dist/components/radio/radio.scss +144 -144
  25. package/dist/components/spinner/spinner.cjs.map +1 -1
  26. package/dist/components/spinner/spinner.js.map +1 -1
  27. package/dist/components/spinner/spinner.scss +29 -29
  28. package/dist/components/textarea/textarea.cjs.map +1 -1
  29. package/dist/components/textarea/textarea.js.map +1 -1
  30. package/dist/components/textarea/textarea.scss +85 -85
  31. package/dist/icons/svg.cjs.map +1 -1
  32. package/dist/icons/svg.js.map +1 -1
  33. package/dist/index.d.cts +2 -2
  34. package/dist/index.d.ts +2 -2
  35. package/dist/lib/cn.cjs.map +1 -1
  36. package/dist/lib/cn.js.map +1 -1
  37. package/dist/lib/slot.cjs +100 -0
  38. package/dist/lib/slot.cjs.map +1 -0
  39. package/dist/lib/slot.js +100 -0
  40. package/dist/lib/slot.js.map +1 -0
  41. package/dist/styles/global.scss +201 -201
  42. package/dist/styles/mixins.scss +51 -51
  43. package/dist/styles/vars.scss +87 -87
  44. package/package.json +1 -1
@@ -1,127 +1,127 @@
1
- .input {
2
- // Размер контрола. Высоты совпадают с Button: sm 30px, md 40px, lg 52px.
3
- --input-padding-y: 9px;
4
- --input-padding-x: 14px;
5
- --input-height: 40px;
6
- box-sizing: border-box;
7
- display: flex;
8
- align-items: center;
9
- width: 100%;
10
- padding: var(--input-padding-y) var(--input-padding-x);
11
- font-size: var(--text-sm);
12
- line-height: var(--text-sm--line-height);
13
- background-color: transparent;
14
- color: var(--color-foreground);
15
- border-radius: var(--radius-sm);
16
- appearance: none;
17
- font-family: var(--font-sans);
18
- border: 1px solid var(--color-input);
19
- outline: none;
20
- transition:
21
- background 0.3s,
22
- border-color 0.3s,
23
- box-shadow 0.2s;
24
-
25
- // Фон автозаполнения Chrome перебивается только внутренней тенью.
26
- &:-webkit-autofill,
27
- &:-webkit-autofill:hover,
28
- &:-webkit-autofill:active {
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);
38
- }
39
-
40
- &[type='number']::-webkit-outer-spin-button,
41
- &[type='number']::-webkit-inner-spin-button {
42
- -webkit-appearance: none;
43
- margin: 0;
44
- }
45
-
46
- &[type='number'] {
47
- -moz-appearance: textfield;
48
- }
49
-
50
- &[type='date'] {
51
- height: var(--input-height);
52
- }
53
-
54
- &::-webkit-date-and-time-value {
55
- text-align: left;
56
- }
57
-
58
- &:focus {
59
- border-color: var(--color-ring);
60
- box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
61
- }
62
-
63
- &:disabled {
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;
79
- }
80
-
81
- &--recommended {
82
- background-color: var(--color-recommended);
83
- }
84
-
85
- &--error {
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);
126
- }
127
- }
1
+ .input {
2
+ // Размер контрола. Высоты совпадают с Button: sm 30px, md 40px, lg 52px.
3
+ --input-padding-y: 9px;
4
+ --input-padding-x: 14px;
5
+ --input-height: 40px;
6
+ box-sizing: border-box;
7
+ display: flex;
8
+ align-items: center;
9
+ width: 100%;
10
+ padding: var(--input-padding-y) var(--input-padding-x);
11
+ font-size: var(--text-sm);
12
+ line-height: var(--text-sm--line-height);
13
+ background-color: transparent;
14
+ color: var(--color-foreground);
15
+ border-radius: var(--radius-sm);
16
+ appearance: none;
17
+ font-family: var(--font-sans);
18
+ border: 1px solid var(--color-input);
19
+ outline: none;
20
+ transition:
21
+ background 0.3s,
22
+ border-color 0.3s,
23
+ box-shadow 0.2s;
24
+
25
+ // Фон автозаполнения Chrome перебивается только внутренней тенью.
26
+ &:-webkit-autofill,
27
+ &:-webkit-autofill:hover,
28
+ &:-webkit-autofill:active {
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);
38
+ }
39
+
40
+ &[type='number']::-webkit-outer-spin-button,
41
+ &[type='number']::-webkit-inner-spin-button {
42
+ -webkit-appearance: none;
43
+ margin: 0;
44
+ }
45
+
46
+ &[type='number'] {
47
+ -moz-appearance: textfield;
48
+ }
49
+
50
+ &[type='date'] {
51
+ height: var(--input-height);
52
+ }
53
+
54
+ &::-webkit-date-and-time-value {
55
+ text-align: left;
56
+ }
57
+
58
+ &:focus {
59
+ border-color: var(--color-ring);
60
+ box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
61
+ }
62
+
63
+ &:disabled {
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;
79
+ }
80
+
81
+ &--recommended {
82
+ background-color: var(--color-recommended);
83
+ }
84
+
85
+ &--error {
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);
126
+ }
127
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"radio.cjs","names":["createContext","useFieldControl","useMemo","cn","forwardRef","useContext","useId","when"],"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import {\r\n createContext,\r\n forwardRef,\r\n useContext,\r\n useId,\r\n useMemo,\r\n type ChangeEvent,\r\n type InputHTMLAttributes,\r\n type ReactNode,\r\n} from 'react';\r\n\r\nimport { useFieldControl } from '@/components/field/field-context';\r\nimport { cn, when } from '@/lib/cn';\r\nimport './radio.scss';\r\n\r\ninterface RadioGroupState {\r\n name: string;\r\n required: boolean;\r\n reportValue?: (value: string) => void;\r\n}\r\n\r\nconst RadioGroupStateContext = createContext<RadioGroupState | null>(null);\r\n\r\nexport interface RadioGroupProps {\r\n /** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */\r\n label?: ReactNode;\r\n name: string;\r\n /** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */\r\n required?: boolean;\r\n children: ReactNode;\r\n className?: string;\r\n}\r\n\r\nexport const RadioGroup = ({\r\n label,\r\n name,\r\n required: requiredProp = false,\r\n children,\r\n className,\r\n}: RadioGroupProps) => {\r\n const field = useFieldControl();\r\n const required = requiredProp || Boolean(field?.required);\r\n const labelledBy = !label && field?.labelId ? field.labelId : undefined;\r\n // Обязательность держат пункты: нативный required на них не даёт отправить\r\n // форму, а его invalid Field ловит и показывает свою ошибку. Выбор отдаём\r\n // в reportValue — иначе показанная ошибка так и висела бы после выбора.\r\n const reportValue = field?.reportValue;\r\n const group = useMemo<RadioGroupState>(\r\n () => ({ name, required, reportValue }),\r\n [name, required, reportValue],\r\n );\r\n\r\n return (\r\n <fieldset\r\n className={cn('radio-group', className)}\r\n role=\"radiogroup\"\r\n {...(labelledBy ? { 'aria-labelledby': labelledBy } : {})}\r\n {...(field?.invalid ? { 'aria-invalid': true } : {})}\r\n {...(required ? { 'aria-required': true } : {})}\r\n {...(field?.describedBy ? { 'aria-describedby': field.describedBy } : {})}\r\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\r\n >\r\n {label ? <legend className=\"radio-group__legend\">{label}</legend> : null}\r\n <RadioGroupStateContext.Provider value={group}>\r\n {children}\r\n </RadioGroupStateContext.Provider>\r\n </fieldset>\r\n );\r\n};\r\n\r\nexport interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {\r\n label?: ReactNode;\r\n error?: boolean;\r\n}\r\n\r\nexport const RadioInput = forwardRef<HTMLInputElement, RadioInputProps>(\r\n function RadioInput(\r\n { id, className, label, error, disabled, name, onChange, ...props },\r\n ref,\r\n ) {\r\n const group = useContext(RadioGroupStateContext);\r\n const autoId = useId();\r\n const inputId = id ?? autoId;\r\n\r\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\r\n if (event.target.checked) group?.reportValue?.(event.target.value);\r\n onChange?.(event);\r\n };\r\n\r\n return (\r\n <div className={cn('radio', className, when(error, 'radio--error'))}>\r\n <input\r\n {...props}\r\n ref={ref}\r\n type=\"radio\"\r\n id={inputId}\r\n // Внутри группы имя группы побеждает, иначе её можно разрезать name на пункте.\r\n name={group?.name ?? name}\r\n disabled={disabled}\r\n // required на любом пункте делает обязательной всю группу с этим name.\r\n {...(group?.required ? { required: true } : {})}\r\n onChange={handleChange}\r\n className=\"radio__input\"\r\n />\r\n {label ? (\r\n <label className=\"radio__label\" htmlFor={inputId}>\r\n {label}\r\n </label>\r\n ) : null}\r\n </div>\r\n );\r\n },\r\n);\r\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
+ {"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 +1 @@
1
- {"version":3,"file":"radio.js","names":[],"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import {\r\n createContext,\r\n forwardRef,\r\n useContext,\r\n useId,\r\n useMemo,\r\n type ChangeEvent,\r\n type InputHTMLAttributes,\r\n type ReactNode,\r\n} from 'react';\r\n\r\nimport { useFieldControl } from '@/components/field/field-context';\r\nimport { cn, when } from '@/lib/cn';\r\nimport './radio.scss';\r\n\r\ninterface RadioGroupState {\r\n name: string;\r\n required: boolean;\r\n reportValue?: (value: string) => void;\r\n}\r\n\r\nconst RadioGroupStateContext = createContext<RadioGroupState | null>(null);\r\n\r\nexport interface RadioGroupProps {\r\n /** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */\r\n label?: ReactNode;\r\n name: string;\r\n /** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */\r\n required?: boolean;\r\n children: ReactNode;\r\n className?: string;\r\n}\r\n\r\nexport const RadioGroup = ({\r\n label,\r\n name,\r\n required: requiredProp = false,\r\n children,\r\n className,\r\n}: RadioGroupProps) => {\r\n const field = useFieldControl();\r\n const required = requiredProp || Boolean(field?.required);\r\n const labelledBy = !label && field?.labelId ? field.labelId : undefined;\r\n // Обязательность держат пункты: нативный required на них не даёт отправить\r\n // форму, а его invalid Field ловит и показывает свою ошибку. Выбор отдаём\r\n // в reportValue — иначе показанная ошибка так и висела бы после выбора.\r\n const reportValue = field?.reportValue;\r\n const group = useMemo<RadioGroupState>(\r\n () => ({ name, required, reportValue }),\r\n [name, required, reportValue],\r\n );\r\n\r\n return (\r\n <fieldset\r\n className={cn('radio-group', className)}\r\n role=\"radiogroup\"\r\n {...(labelledBy ? { 'aria-labelledby': labelledBy } : {})}\r\n {...(field?.invalid ? { 'aria-invalid': true } : {})}\r\n {...(required ? { 'aria-required': true } : {})}\r\n {...(field?.describedBy ? { 'aria-describedby': field.describedBy } : {})}\r\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\r\n >\r\n {label ? <legend className=\"radio-group__legend\">{label}</legend> : null}\r\n <RadioGroupStateContext.Provider value={group}>\r\n {children}\r\n </RadioGroupStateContext.Provider>\r\n </fieldset>\r\n );\r\n};\r\n\r\nexport interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {\r\n label?: ReactNode;\r\n error?: boolean;\r\n}\r\n\r\nexport const RadioInput = forwardRef<HTMLInputElement, RadioInputProps>(\r\n function RadioInput(\r\n { id, className, label, error, disabled, name, onChange, ...props },\r\n ref,\r\n ) {\r\n const group = useContext(RadioGroupStateContext);\r\n const autoId = useId();\r\n const inputId = id ?? autoId;\r\n\r\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\r\n if (event.target.checked) group?.reportValue?.(event.target.value);\r\n onChange?.(event);\r\n };\r\n\r\n return (\r\n <div className={cn('radio', className, when(error, 'radio--error'))}>\r\n <input\r\n {...props}\r\n ref={ref}\r\n type=\"radio\"\r\n id={inputId}\r\n // Внутри группы имя группы побеждает, иначе её можно разрезать name на пункте.\r\n name={group?.name ?? name}\r\n disabled={disabled}\r\n // required на любом пункте делает обязательной всю группу с этим name.\r\n {...(group?.required ? { required: true } : {})}\r\n onChange={handleChange}\r\n className=\"radio__input\"\r\n />\r\n {label ? (\r\n <label className=\"radio__label\" htmlFor={inputId}>\r\n {label}\r\n </label>\r\n ) : null}\r\n </div>\r\n );\r\n },\r\n);\r\n"],"mappings":";;;;;;AAqBA,MAAM,yBAAyB,cAAsC,IAAI;AAYzE,MAAa,cAAc,EACzB,OACA,MACA,UAAU,eAAe,OACzB,UACA,gBACqB;CACrB,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,WAAW,gBAAgB,QAAQ,OAAO,QAAQ;CACxD,MAAM,aAAa,CAAC,SAAS,OAAO,UAAU,MAAM,UAAU,KAAA;CAI9D,MAAM,cAAc,OAAO;CAC3B,MAAM,QAAQ,eACL;EAAE;EAAM;EAAU;CAAY,IACrC;EAAC;EAAM;EAAU;CAAW,CAC9B;CAEA,OACE,qBAAC,YAAD;EACE,WAAW,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,oBAAC,UAAD;GAAQ,WAAU;GAAuB,UAAA;EAAc,CAAA,IAAI,MACpE,oBAAC,uBAAuB,UAAxB;GAAiC,OAAO;GACrC;EAC8B,CAAA,CACzB;;AAEd;AAOA,MAAa,aAAa,WACxB,SAAS,WACP,EAAE,IAAI,WAAW,OAAO,OAAO,UAAU,MAAM,UAAU,GAAG,SAC5D,KACA;CACA,MAAM,QAAQ,WAAW,sBAAsB;CAC/C,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,MAAM,OAAO,SAAS,OAAO,cAAc,MAAM,OAAO,KAAK;EACjE,WAAW,KAAK;CAClB;CAEA,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,SAAS,WAAW,KAAK,OAAO,cAAc,CAAC;EAAlE,UAAA,CACE,oBAAC,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,oBAAC,SAAD;GAAO,WAAU;GAAe,SAAS;GACtC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
1
+ {"version":3,"file":"radio.js","names":[],"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,yBAAyB,cAAsC,IAAI;AAYzE,MAAa,cAAc,EACzB,OACA,MACA,UAAU,eAAe,OACzB,UACA,gBACqB;CACrB,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,WAAW,gBAAgB,QAAQ,OAAO,QAAQ;CACxD,MAAM,aAAa,CAAC,SAAS,OAAO,UAAU,MAAM,UAAU,KAAA;CAI9D,MAAM,cAAc,OAAO;CAC3B,MAAM,QAAQ,eACL;EAAE;EAAM;EAAU;CAAY,IACrC;EAAC;EAAM;EAAU;CAAW,CAC9B;CAEA,OACE,qBAAC,YAAD;EACE,WAAW,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,oBAAC,UAAD;GAAQ,WAAU;GAAuB,UAAA;EAAc,CAAA,IAAI,MACpE,oBAAC,uBAAuB,UAAxB;GAAiC,OAAO;GACrC;EAC8B,CAAA,CACzB;;AAEd;AAOA,MAAa,aAAa,WACxB,SAAS,WACP,EAAE,IAAI,WAAW,OAAO,OAAO,UAAU,MAAM,UAAU,GAAG,SAC5D,KACA;CACA,MAAM,QAAQ,WAAW,sBAAsB;CAC/C,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,MAAM,OAAO,SAAS,OAAO,cAAc,MAAM,OAAO,KAAK;EACjE,WAAW,KAAK;CAClB;CAEA,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,SAAS,WAAW,KAAK,OAAO,cAAc,CAAC;EAAlE,UAAA,CACE,oBAAC,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,oBAAC,SAAD;GAAO,WAAU;GAAe,SAAS;GACtC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
@@ -1,144 +1,144 @@
1
- .radio__label {
2
- font-size: var(--text-sm);
3
- line-height: var(--text-sm--line-height);
4
- display: inline-block;
5
- vertical-align: top;
6
- padding-left: var(--space-1);
7
- user-select: none;
8
- cursor: pointer;
9
- }
10
-
11
- .radio__input {
12
- --active: var(--color-primary);
13
- --active-inner: var(--color-primary-foreground);
14
- --border: var(--color-muted);
15
- --border-hover: var(--color-foreground);
16
- --background: var(--color-background);
17
- --disabled: var(--color-muted);
18
- --disabled-inner: var(--color-muted);
19
- --disabled-after-inner: var(--color-muted-foreground);
20
- box-sizing: border-box;
21
- appearance: none;
22
- width: 20px;
23
- height: 20px;
24
- outline: none;
25
- display: inline-block;
26
- vertical-align: top;
27
- position: relative;
28
- margin: 0;
29
- flex-shrink: 0;
30
- cursor: pointer;
31
- border: 1px solid var(--bc, var(--border));
32
- background: var(--b, var(--background));
33
- border-radius: 50%;
34
- transition:
35
- background 0.3s,
36
- border-color 0.3s,
37
- box-shadow 0.2s;
38
-
39
- &::after {
40
- content: '';
41
- display: block;
42
- width: 6px;
43
- height: 6px;
44
- left: 50%;
45
- top: 50%;
46
- border-radius: 50%;
47
- background: var(--active-inner);
48
- position: absolute;
49
- opacity: var(--o, 0);
50
- transform: translate(-50%, -50%);
51
- transition:
52
- transform var(--d-t, 0.3s) var(--d-t-e, ease),
53
- opacity var(--d-o, 0.2s);
54
- }
55
-
56
- &:checked {
57
- --b: var(--active);
58
- --bc: var(--active);
59
- --o: 1;
60
- --d-o: 0.3s;
61
- --d-t: 0.6s;
62
- --d-t-e: cubic-bezier(0.2, 0.85, 0.32, 1.2);
63
- }
64
-
65
- &:hover:not(:checked):not(:disabled) {
66
- border-width: 2px;
67
- --bc: var(--active);
68
- }
69
-
70
- &:hover:not(:disabled) {
71
- border-color: var(--border-hover);
72
- }
73
-
74
- &:focus {
75
- border: 1px solid var(--border-hover);
76
- }
77
-
78
- &:focus:not(:focus-visible) {
79
- outline: none;
80
- }
81
-
82
- &:focus-visible {
83
- outline: var(--focus-ring-width) solid var(--focus-ring-color);
84
- outline-offset: var(--focus-ring-offset);
85
- }
86
-
87
- &:disabled {
88
- --b: var(--disabled);
89
- cursor: not-allowed;
90
- opacity: 1;
91
-
92
- &:checked {
93
- --b: var(--disabled-inner);
94
- --bc: var(--border);
95
-
96
- &::after {
97
- background: var(--disabled-after-inner);
98
- }
99
- }
100
-
101
- & + .radio__label {
102
- color: var(--color-muted-foreground);
103
- cursor: not-allowed;
104
- }
105
- }
106
- }
107
-
108
- // Модификатор ошибки после .radio__input: иначе shorthand border затрёт border-color.
109
- .radio {
110
- display: flex;
111
- align-items: flex-start;
112
-
113
- &--error {
114
- .radio__input {
115
- border-color: color-mix(
116
- in srgb,
117
- var(--color-destructive) 50%,
118
- transparent
119
- );
120
- }
121
- }
122
- }
123
-
124
- .radio-group {
125
- margin: 0;
126
- padding: 0;
127
- border: 0;
128
- min-inline-size: 0;
129
- }
130
-
131
- .radio-group__legend {
132
- margin: 0 0 var(--space-3);
133
- color: var(--color-foreground);
134
- font-size: var(--text-lg);
135
- font-weight: 900;
136
- line-height: var(--text-lg--line-height);
137
- }
138
-
139
- @media (prefers-reduced-motion: reduce) {
140
- .radio__input,
141
- .radio__input::after {
142
- transition: none;
143
- }
144
- }
1
+ .radio__label {
2
+ font-size: var(--text-sm);
3
+ line-height: var(--text-sm--line-height);
4
+ display: inline-block;
5
+ vertical-align: top;
6
+ padding-left: var(--space-1);
7
+ user-select: none;
8
+ cursor: pointer;
9
+ }
10
+
11
+ .radio__input {
12
+ --active: var(--color-primary);
13
+ --active-inner: var(--color-primary-foreground);
14
+ --border: var(--color-muted);
15
+ --border-hover: var(--color-foreground);
16
+ --background: var(--color-background);
17
+ --disabled: var(--color-muted);
18
+ --disabled-inner: var(--color-muted);
19
+ --disabled-after-inner: var(--color-muted-foreground);
20
+ box-sizing: border-box;
21
+ appearance: none;
22
+ width: 20px;
23
+ height: 20px;
24
+ outline: none;
25
+ display: inline-block;
26
+ vertical-align: top;
27
+ position: relative;
28
+ margin: 0;
29
+ flex-shrink: 0;
30
+ cursor: pointer;
31
+ border: 1px solid var(--bc, var(--border));
32
+ background: var(--b, var(--background));
33
+ border-radius: 50%;
34
+ transition:
35
+ background 0.3s,
36
+ border-color 0.3s,
37
+ box-shadow 0.2s;
38
+
39
+ &::after {
40
+ content: '';
41
+ display: block;
42
+ width: 6px;
43
+ height: 6px;
44
+ left: 50%;
45
+ top: 50%;
46
+ border-radius: 50%;
47
+ background: var(--active-inner);
48
+ position: absolute;
49
+ opacity: var(--o, 0);
50
+ transform: translate(-50%, -50%);
51
+ transition:
52
+ transform var(--d-t, 0.3s) var(--d-t-e, ease),
53
+ opacity var(--d-o, 0.2s);
54
+ }
55
+
56
+ &:checked {
57
+ --b: var(--active);
58
+ --bc: var(--active);
59
+ --o: 1;
60
+ --d-o: 0.3s;
61
+ --d-t: 0.6s;
62
+ --d-t-e: cubic-bezier(0.2, 0.85, 0.32, 1.2);
63
+ }
64
+
65
+ &:hover:not(:checked):not(:disabled) {
66
+ border-width: 2px;
67
+ --bc: var(--active);
68
+ }
69
+
70
+ &:hover:not(:disabled) {
71
+ border-color: var(--border-hover);
72
+ }
73
+
74
+ &:focus {
75
+ border: 1px solid var(--border-hover);
76
+ }
77
+
78
+ &:focus:not(:focus-visible) {
79
+ outline: none;
80
+ }
81
+
82
+ &:focus-visible {
83
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
84
+ outline-offset: var(--focus-ring-offset);
85
+ }
86
+
87
+ &:disabled {
88
+ --b: var(--disabled);
89
+ cursor: not-allowed;
90
+ opacity: 1;
91
+
92
+ &:checked {
93
+ --b: var(--disabled-inner);
94
+ --bc: var(--border);
95
+
96
+ &::after {
97
+ background: var(--disabled-after-inner);
98
+ }
99
+ }
100
+
101
+ & + .radio__label {
102
+ color: var(--color-muted-foreground);
103
+ cursor: not-allowed;
104
+ }
105
+ }
106
+ }
107
+
108
+ // Модификатор ошибки после .radio__input: иначе shorthand border затрёт border-color.
109
+ .radio {
110
+ display: flex;
111
+ align-items: flex-start;
112
+
113
+ &--error {
114
+ .radio__input {
115
+ border-color: color-mix(
116
+ in srgb,
117
+ var(--color-destructive) 50%,
118
+ transparent
119
+ );
120
+ }
121
+ }
122
+ }
123
+
124
+ .radio-group {
125
+ margin: 0;
126
+ padding: 0;
127
+ border: 0;
128
+ min-inline-size: 0;
129
+ }
130
+
131
+ .radio-group__legend {
132
+ margin: 0 0 var(--space-3);
133
+ color: var(--color-foreground);
134
+ font-size: var(--text-lg);
135
+ font-weight: 900;
136
+ line-height: var(--text-lg--line-height);
137
+ }
138
+
139
+ @media (prefers-reduced-motion: reduce) {
140
+ .radio__input,
141
+ .radio__input::after {
142
+ transition: none;
143
+ }
144
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.cjs","names":["useId","useSyncExternalStore","cn","when"],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import {\r\n useId,\r\n useSyncExternalStore,\r\n type FC,\r\n type HTMLAttributes,\r\n type SVGProps,\r\n} from 'react';\r\n\r\nimport { cn, when } from '@/lib/cn';\r\nimport './spinner.scss';\r\n\r\nconst spinnerPosition = {\r\n top: 'top',\r\n center: 'center',\r\n} as const;\r\n\r\nexport type SpinnerPosition =\r\n (typeof spinnerPosition)[keyof typeof spinnerPosition];\r\n\r\nexport interface SpinnerProps extends Omit<\r\n HTMLAttributes<HTMLDivElement>,\r\n 'size' | 'position'\r\n> {\r\n size?: number;\r\n position?: SpinnerPosition;\r\n /** Текст для скринридера. Виден только ему. */\r\n label?: string;\r\n}\r\n\r\nconst reducedMotionQuery = '(prefers-reduced-motion: reduce)';\r\n\r\nconst subscribeReducedMotion = (onChange: () => void) => {\r\n if (typeof window.matchMedia !== 'function') return () => {};\r\n\r\n const media = window.matchMedia(reducedMotionQuery);\r\n media.addEventListener('change', onChange);\r\n return () => media.removeEventListener('change', onChange);\r\n};\r\n\r\nconst prefersReducedMotion = () =>\r\n typeof window.matchMedia === 'function' &&\r\n window.matchMedia(reducedMotionQuery).matches;\r\n\r\n// ponytail: сервер всегда false — гидрация не расходится, но первый кадр после неё может мигнуть SMIL, пока клиент не прочитает matchMedia. Апгрейд: Sec-CH-Prefers-Reduced-Motion.\r\nconst prefersReducedMotionServer = () => false;\r\n\r\nconst dotDelays = [\r\n '0s',\r\n '0.125s',\r\n '0.25s',\r\n '0.375s',\r\n '0.5s',\r\n '0.625s',\r\n '0.75s',\r\n '0.875s',\r\n] as const;\r\n\r\nconst Dot = ({\r\n begin,\r\n rotate,\r\n reduced,\r\n}: {\r\n begin: string;\r\n rotate: number;\r\n reduced: boolean;\r\n}) => (\r\n <circle\r\n transform={`rotate(${rotate} 16 16)`}\r\n cx=\"16\"\r\n cy=\"3\"\r\n r={reduced ? 3 : 0}\r\n >\r\n {reduced ? null : (\r\n <animate\r\n attributeName=\"r\"\r\n values=\"0;3;0;0\"\r\n dur=\"1s\"\r\n repeatCount=\"indefinite\"\r\n begin={begin}\r\n keySplines=\"0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8\"\r\n calcMode=\"spline\"\r\n />\r\n )}\r\n </circle>\r\n);\r\n\r\nconst svgProps: SVGProps<SVGSVGElement> = {\r\n 'xmlns': 'http://www.w3.org/2000/svg',\r\n 'viewBox': '0 0 32 32',\r\n 'fill': 'var(--color-muted)',\r\n 'aria-hidden': true,\r\n};\r\n\r\nexport const Spinner: FC<SpinnerProps> = ({\r\n className,\r\n position = spinnerPosition.center,\r\n size = 64,\r\n label = 'Загрузка',\r\n ...props\r\n}) => {\r\n const labelId = useId();\r\n const reduced = useSyncExternalStore(\r\n subscribeReducedMotion,\r\n prefersReducedMotion,\r\n prefersReducedMotionServer,\r\n );\r\n\r\n return (\r\n <div\r\n {...props}\r\n // eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- live region, не <output>\r\n role=\"status\"\r\n aria-labelledby={labelId}\r\n className={cn(\r\n 'spinner',\r\n className,\r\n when(position === spinnerPosition.top, 'spinner--top'),\r\n )}\r\n >\r\n <span id={labelId} className=\"spinner__label\">\r\n {label}\r\n </span>\r\n <svg {...svgProps} width={size} height={size}>\r\n {dotDelays.map((begin, index) => (\r\n <Dot\r\n key={begin}\r\n begin={begin}\r\n rotate={index * 45}\r\n reduced={reduced}\r\n />\r\n ))}\r\n </svg>\r\n </div>\r\n );\r\n};\r\n"],"mappings":";;;;;AAWA,MAAM,kBAAkB;CACtB,KAAK;CACL,QAAQ;AACV;AAeA,MAAM,qBAAqB;AAE3B,MAAM,0BAA0B,aAAyB;CACvD,IAAI,OAAO,OAAO,eAAe,YAAY,aAAa,CAAC;CAE3D,MAAM,QAAQ,OAAO,WAAW,kBAAkB;CAClD,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa,MAAM,oBAAoB,UAAU,QAAQ;AAC3D;AAEA,MAAM,6BACJ,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kBAAkB,CAAC,CAAC;AAGxC,MAAM,mCAAmC;AAEzC,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,OAAO,EACX,OACA,QACA,cAMA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;CACE,WAAW,UAAU,OAAO;CAC5B,IAAG;CACH,IAAG;CACH,GAAG,UAAU,IAAI;CAEhB,UAAA,UAAU,OACT,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;EACE,eAAc;EACd,QAAO;EACP,KAAI;EACJ,aAAY;EACL;EACP,YAAW;EACX,UAAS;CACV,CAAA;AAEG,CAAA;AAGV,MAAM,WAAoC;CACxC,SAAS;CACT,WAAW;CACX,QAAQ;CACR,eAAe;AACjB;AAEA,MAAa,WAA6B,EACxC,WACA,WAAW,gBAAgB,QAC3B,OAAO,IACP,QAAQ,YACR,GAAG,YACC;CACJ,MAAM,WAAA,GAAUA,MAAAA,MAAAA,CAAM;CACtB,MAAM,WAAA,GAAUC,MAAAA,qBAAAA,CACd,wBACA,sBACA,0BACF;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EACE,GAAI;EAEJ,MAAK;EACL,mBAAiB;EACjB,WAAWC,WAAAA,GACT,WACA,WACAC,WAAAA,KAAK,aAAa,gBAAgB,KAAK,cAAc,CACvD;EATF,UAAA,CAWE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;GAAM,IAAI;GAAS,WAAU;GAC1B,UAAA;EACG,CAAA,GACN,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;GAAK,GAAI;GAAU,OAAO;GAAM,QAAQ;GACrC,UAAA,UAAU,KAAK,OAAO,UACrB,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;IAES;IACP,QAAQ,QAAQ;IACP;GACV,GAJM,KAIN,CACF;EACE,CAAA,CACF;;AAET"}
1
+ {"version":3,"file":"spinner.cjs","names":["useId","useSyncExternalStore","cn","when"],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import {\n useId,\n useSyncExternalStore,\n type FC,\n type HTMLAttributes,\n type SVGProps,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport './spinner.scss';\n\nconst spinnerPosition = {\n top: 'top',\n center: 'center',\n} as const;\n\nexport type SpinnerPosition =\n (typeof spinnerPosition)[keyof typeof spinnerPosition];\n\nexport interface SpinnerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'size' | 'position'\n> {\n size?: number;\n position?: SpinnerPosition;\n /** Текст для скринридера. Виден только ему. */\n label?: string;\n}\n\nconst reducedMotionQuery = '(prefers-reduced-motion: reduce)';\n\nconst subscribeReducedMotion = (onChange: () => void) => {\n if (typeof window.matchMedia !== 'function') return () => {};\n\n const media = window.matchMedia(reducedMotionQuery);\n media.addEventListener('change', onChange);\n return () => media.removeEventListener('change', onChange);\n};\n\nconst prefersReducedMotion = () =>\n typeof window.matchMedia === 'function' &&\n window.matchMedia(reducedMotionQuery).matches;\n\n// ponytail: сервер всегда false — гидрация не расходится, но первый кадр после неё может мигнуть SMIL, пока клиент не прочитает matchMedia. Апгрейд: Sec-CH-Prefers-Reduced-Motion.\nconst prefersReducedMotionServer = () => false;\n\nconst dotDelays = [\n '0s',\n '0.125s',\n '0.25s',\n '0.375s',\n '0.5s',\n '0.625s',\n '0.75s',\n '0.875s',\n] as const;\n\nconst Dot = ({\n begin,\n rotate,\n reduced,\n}: {\n begin: string;\n rotate: number;\n reduced: boolean;\n}) => (\n <circle\n transform={`rotate(${rotate} 16 16)`}\n cx=\"16\"\n cy=\"3\"\n r={reduced ? 3 : 0}\n >\n {reduced ? null : (\n <animate\n attributeName=\"r\"\n values=\"0;3;0;0\"\n dur=\"1s\"\n repeatCount=\"indefinite\"\n begin={begin}\n keySplines=\"0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8\"\n calcMode=\"spline\"\n />\n )}\n </circle>\n);\n\nconst svgProps: SVGProps<SVGSVGElement> = {\n 'xmlns': 'http://www.w3.org/2000/svg',\n 'viewBox': '0 0 32 32',\n 'fill': 'var(--color-muted)',\n 'aria-hidden': true,\n};\n\nexport const Spinner: FC<SpinnerProps> = ({\n className,\n position = spinnerPosition.center,\n size = 64,\n label = 'Загрузка',\n ...props\n}) => {\n const labelId = useId();\n const reduced = useSyncExternalStore(\n subscribeReducedMotion,\n prefersReducedMotion,\n prefersReducedMotionServer,\n );\n\n return (\n <div\n {...props}\n // eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- live region, не <output>\n role=\"status\"\n aria-labelledby={labelId}\n className={cn(\n 'spinner',\n className,\n when(position === spinnerPosition.top, 'spinner--top'),\n )}\n >\n <span id={labelId} className=\"spinner__label\">\n {label}\n </span>\n <svg {...svgProps} width={size} height={size}>\n {dotDelays.map((begin, index) => (\n <Dot\n key={begin}\n begin={begin}\n rotate={index * 45}\n reduced={reduced}\n />\n ))}\n </svg>\n </div>\n );\n};\n"],"mappings":";;;;;AAWA,MAAM,kBAAkB;CACtB,KAAK;CACL,QAAQ;AACV;AAeA,MAAM,qBAAqB;AAE3B,MAAM,0BAA0B,aAAyB;CACvD,IAAI,OAAO,OAAO,eAAe,YAAY,aAAa,CAAC;CAE3D,MAAM,QAAQ,OAAO,WAAW,kBAAkB;CAClD,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa,MAAM,oBAAoB,UAAU,QAAQ;AAC3D;AAEA,MAAM,6BACJ,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kBAAkB,CAAC,CAAC;AAGxC,MAAM,mCAAmC;AAEzC,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,OAAO,EACX,OACA,QACA,cAMA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;CACE,WAAW,UAAU,OAAO;CAC5B,IAAG;CACH,IAAG;CACH,GAAG,UAAU,IAAI;CAEhB,UAAA,UAAU,OACT,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;EACE,eAAc;EACd,QAAO;EACP,KAAI;EACJ,aAAY;EACL;EACP,YAAW;EACX,UAAS;CACV,CAAA;AAEG,CAAA;AAGV,MAAM,WAAoC;CACxC,SAAS;CACT,WAAW;CACX,QAAQ;CACR,eAAe;AACjB;AAEA,MAAa,WAA6B,EACxC,WACA,WAAW,gBAAgB,QAC3B,OAAO,IACP,QAAQ,YACR,GAAG,YACC;CACJ,MAAM,WAAA,GAAUA,MAAAA,MAAAA,CAAM;CACtB,MAAM,WAAA,GAAUC,MAAAA,qBAAAA,CACd,wBACA,sBACA,0BACF;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EACE,GAAI;EAEJ,MAAK;EACL,mBAAiB;EACjB,WAAWC,WAAAA,GACT,WACA,WACAC,WAAAA,KAAK,aAAa,gBAAgB,KAAK,cAAc,CACvD;EATF,UAAA,CAWE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;GAAM,IAAI;GAAS,WAAU;GAC1B,UAAA;EACG,CAAA,GACN,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;GAAK,GAAI;GAAU,OAAO;GAAM,QAAQ;GACrC,UAAA,UAAU,KAAK,OAAO,UACrB,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;IAES;IACP,QAAQ,QAAQ;IACP;GACV,GAJM,KAIN,CACF;EACE,CAAA,CACF;;AAET"}