@koslibs/components 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @koslibs/components
2
2
 
3
+ ## 1.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#20](https://github.com/koslibs/components/pull/20) [`ea31f0a`](https://github.com/koslibs/components/commit/ea31f0a1ece96d62c21f943de86459946fb1a5fe) Thanks [@holypower777](https://github.com/holypower777)! - Добавлен Chips: одиночный и множественный выбор, управляемые и внутренние значения, блокировка группы и пунктов, состояния из Figma с плавными переходами, перенос полных подписей, нативные формы, Storybook и публичные типы. All обрабатывается как обычный пункт.
8
+
3
9
  ## 1.16.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ import type { ChipsProps } from './types.js';
2
+ export declare const Chips: import("react").ForwardRefExoticComponent<ChipsProps & import("react").RefAttributes<HTMLFieldSetElement>>;
@@ -0,0 +1,48 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { forwardRef, useId, useImperativeHandle } from "react";
4
+ import { useDataTestId } from "../../hooks/use-data-test-id.js";
5
+ import { TypographyText } from "../Typography/text.js";
6
+ import { useChipsState } from "./hooks/use-chips-state.js";
7
+ import index_module from "./index.module.js";
8
+ const Chips = /*#__PURE__*/ forwardRef((props, ref)=>{
9
+ const { items, multiselect = false, value: _value, defaultValue: _defaultValue, onChange: _onChange, disabled = false, form, className, listClassName, itemClassName, dataTestId, ...restProps } = props;
10
+ const groupName = useId();
11
+ const getId = useDataTestId(dataTestId);
12
+ const state = useChipsState(props);
13
+ useImperativeHandle(ref, ()=>state.groupRef.current, []);
14
+ return /*#__PURE__*/ jsx("fieldset", {
15
+ ...restProps,
16
+ ref: state.groupRef,
17
+ form: form,
18
+ disabled: disabled,
19
+ className: classnames(index_module.component, className),
20
+ "data-test-id": dataTestId,
21
+ children: /*#__PURE__*/ jsx("div", {
22
+ className: classnames(index_module.list, listClassName),
23
+ children: items.map((item)=>/*#__PURE__*/ jsxs("label", {
24
+ className: index_module.item,
25
+ children: [
26
+ /*#__PURE__*/ jsx("input", {
27
+ type: multiselect ? 'checkbox' : 'radio',
28
+ name: groupName,
29
+ form: groupName,
30
+ value: item.value,
31
+ checked: state.selected.has(item.value),
32
+ disabled: disabled || item.disabled,
33
+ onChange: state.handleChange,
34
+ className: index_module.input,
35
+ "data-test-id": getId(item.value)
36
+ }),
37
+ /*#__PURE__*/ jsx(TypographyText, {
38
+ size: 14,
39
+ className: classnames(index_module.chip, itemClassName),
40
+ children: item.label
41
+ })
42
+ ]
43
+ }, item.value))
44
+ })
45
+ });
46
+ });
47
+ Chips.displayName = 'Chips';
48
+ export { Chips };
@@ -0,0 +1,7 @@
1
+ import { type ChangeEvent } from 'react';
2
+ import type { ChipsProps } from '../types.js';
3
+ export declare function useChipsState(props: ChipsProps): {
4
+ groupRef: import("react").RefObject<HTMLFieldSetElement>;
5
+ selected: Set<string>;
6
+ handleChange: (event: ChangeEvent<HTMLInputElement>) => void;
7
+ };
@@ -0,0 +1,56 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { normalizeValue } from "../utils/normalize-value.js";
3
+ function useChipsState(props) {
4
+ const { items, multiselect = false, defaultValue } = props;
5
+ const controlled = void 0 !== props.value;
6
+ const groupRef = useRef(null);
7
+ const [localValue, setLocalValue] = useState(defaultValue);
8
+ const value = normalizeValue(items, multiselect, controlled ? props.value : localValue);
9
+ const selected = new Set(Array.isArray(value) ? value : [
10
+ value
11
+ ].filter((item)=>null !== item));
12
+ useEffect(()=>{
13
+ const form = groupRef.current?.form;
14
+ let active = true;
15
+ const handleReset = (event)=>{
16
+ queueMicrotask(()=>{
17
+ if (active && !event.defaultPrevented && !controlled) setLocalValue(defaultValue);
18
+ });
19
+ };
20
+ form?.addEventListener('reset', handleReset);
21
+ return ()=>{
22
+ active = false;
23
+ form?.removeEventListener('reset', handleReset);
24
+ };
25
+ }, [
26
+ props.form,
27
+ controlled,
28
+ defaultValue
29
+ ]);
30
+ const handleChange = (event)=>{
31
+ const item = items.find((candidate)=>candidate.value === event.currentTarget.value);
32
+ if (props.disabled || !item || item.disabled) return;
33
+ if (props.multiselect) {
34
+ const current = Array.isArray(value) ? value : [];
35
+ const next = event.currentTarget.checked ? [
36
+ ...current,
37
+ item.value
38
+ ] : current.filter((candidate)=>candidate !== item.value);
39
+ if (!controlled) setLocalValue(next);
40
+ props.onChange?.(event, {
41
+ value: next
42
+ });
43
+ } else if (value !== item.value) {
44
+ if (!controlled) setLocalValue(item.value);
45
+ props.onChange?.(event, {
46
+ value: item.value
47
+ });
48
+ }
49
+ };
50
+ return {
51
+ groupRef,
52
+ selected,
53
+ handleChange
54
+ };
55
+ }
56
+ export { useChipsState };
@@ -0,0 +1,2 @@
1
+ export { Chips } from './chips.js';
2
+ export type { ChipsProps, ChipsItem, ChipsChangePayload } from './types.js';
@@ -0,0 +1 @@
1
+ export { Chips } from "./chips.js";
@@ -0,0 +1,9 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-YzqK3S",
4
+ list: "list-Dlab5I",
5
+ item: "item-Q7WaXH",
6
+ input: "input-UTMIEW",
7
+ chip: "chip-jqwYZi"
8
+ };
9
+ export default index_module;
@@ -0,0 +1,99 @@
1
+ @import "../../styles/theme.css";
2
+ @import "./vars.css";
3
+
4
+ .component-YzqK3S {
5
+ box-sizing: border-box;
6
+ min-width: 0;
7
+ color: var(--chips-color);
8
+ font-family: var(--font-primary);
9
+ border: 0;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ .list-Dlab5I {
15
+ gap: var(--chips-gap);
16
+ flex-wrap: wrap;
17
+ align-items: flex-start;
18
+ display: flex;
19
+ }
20
+
21
+ .item-Q7WaXH {
22
+ min-width: 0;
23
+ max-width: 100%;
24
+ display: inline-flex;
25
+ position: relative;
26
+ }
27
+
28
+ .input-UTMIEW {
29
+ z-index: 1;
30
+ opacity: 0;
31
+ cursor: pointer;
32
+ width: 100%;
33
+ height: 100%;
34
+ margin: 0;
35
+ position: absolute;
36
+ inset: 0;
37
+ }
38
+
39
+ .chip-jqwYZi {
40
+ box-sizing: border-box;
41
+ min-width: 0;
42
+ min-height: var(--chips-height);
43
+ padding: var(--chips-padding-y) var(--chips-padding-x);
44
+ border-radius: var(--chips-radius);
45
+ background: var(--chips-background);
46
+ color: var(--chips-color);
47
+ text-align: center;
48
+ overflow-wrap: anywhere;
49
+ line-height: 20px;
50
+ transition: background-color .1s ease-in-out, color .1s ease-in-out;
51
+ display: block;
52
+ }
53
+
54
+ .input-UTMIEW:checked + .chip-jqwYZi {
55
+ background: var(--chips-selected-background);
56
+ }
57
+
58
+ @media (hover: hover) {
59
+ .input-UTMIEW:not(:disabled):hover + .chip-jqwYZi {
60
+ background: var(--chips-hover-background);
61
+ }
62
+
63
+ .input-UTMIEW:not(:disabled):checked:hover + .chip-jqwYZi {
64
+ background: var(--chips-selected-hover-background);
65
+ }
66
+ }
67
+
68
+ .input-UTMIEW:not(:disabled):active + .chip-jqwYZi {
69
+ background: var(--chips-active-background);
70
+ }
71
+
72
+ .input-UTMIEW:not(:disabled):checked:active + .chip-jqwYZi {
73
+ background: var(--chips-selected-active-background);
74
+ }
75
+
76
+ .input-UTMIEW:focus-visible + .chip-jqwYZi {
77
+ outline: 2px solid var(--chips-focus-color);
78
+ outline-offset: 2px;
79
+ }
80
+
81
+ .input-UTMIEW:disabled {
82
+ cursor: not-allowed;
83
+ }
84
+
85
+ .input-UTMIEW:disabled + .chip-jqwYZi {
86
+ background: var(--chips-disabled-background);
87
+ color: var(--chips-disabled-color);
88
+ }
89
+
90
+ .input-UTMIEW:disabled:checked + .chip-jqwYZi {
91
+ background: var(--chips-disabled-selected-background);
92
+ }
93
+
94
+ @media (prefers-reduced-motion: reduce) {
95
+ .chip-jqwYZi {
96
+ transition: none;
97
+ }
98
+ }
99
+
@@ -0,0 +1,86 @@
1
+ import type { ChangeEvent, FieldsetHTMLAttributes } from 'react';
2
+ export type ChipsItem = {
3
+ /**
4
+ * Уникальное значение пункта.
5
+ */
6
+ value: string;
7
+ /**
8
+ * Полная текстовая подпись пункта.
9
+ */
10
+ label: string;
11
+ /**
12
+ * Блокирует переключение пункта, сохраняя его выбранное состояние.
13
+ * @default false
14
+ */
15
+ disabled?: boolean;
16
+ };
17
+ export type ChipsChangePayload<Value = string | null> = {
18
+ /**
19
+ * Выбранное значение или массив значений в порядке выбора.
20
+ */
21
+ value: Value;
22
+ };
23
+ type CommonProps = Omit<FieldsetHTMLAttributes<HTMLFieldSetElement>, 'onChange' | 'defaultValue' | 'children' | 'name' | 'disabled'> & {
24
+ /**
25
+ * Пункты с уникальными value. All обрабатывается как обычный пункт.
26
+ */
27
+ items: readonly ChipsItem[];
28
+ /**
29
+ * Блокирует все пункты группы, сохраняя выбор.
30
+ * @default false
31
+ */
32
+ disabled?: boolean;
33
+ /**
34
+ * Дополнительный CSS-класс списка пунктов.
35
+ */
36
+ listClassName?: string;
37
+ /**
38
+ * Дополнительный CSS-класс каждого chip.
39
+ */
40
+ itemClassName?: string;
41
+ /**
42
+ * Идентификатор группы для тестов. Пункты получают суффикс с их value.
43
+ */
44
+ dataTestId?: string;
45
+ };
46
+ type SingleProps = {
47
+ /**
48
+ * Выбирает один пункт. Повторное нажатие сохраняет выбор.
49
+ * @default false
50
+ */
51
+ multiselect?: false;
52
+ /**
53
+ * Управляемое значение; null означает пустой выбор.
54
+ */
55
+ value?: string | null;
56
+ /**
57
+ * Начальное значение неуправляемой группы.
58
+ * @default null
59
+ */
60
+ defaultValue?: string | null;
61
+ /**
62
+ * Изменение пользователем выбранного значения.
63
+ */
64
+ onChange?: (event: ChangeEvent<HTMLInputElement>, payload: ChipsChangePayload) => void;
65
+ };
66
+ type MultipleProps = {
67
+ /**
68
+ * Независимое переключение нескольких пунктов.
69
+ */
70
+ multiselect: true;
71
+ /**
72
+ * Управляемый массив выбранных значений.
73
+ */
74
+ value?: readonly string[];
75
+ /**
76
+ * Начальный массив значений неуправляемой группы.
77
+ * @default []
78
+ */
79
+ defaultValue?: readonly string[];
80
+ /**
81
+ * Изменение пользователем набора выбранных значений.
82
+ */
83
+ onChange?: (event: ChangeEvent<HTMLInputElement>, payload: ChipsChangePayload<string[]>) => void;
84
+ };
85
+ export type ChipsProps = CommonProps & (SingleProps | MultipleProps);
86
+ export {};
File without changes
@@ -0,0 +1,2 @@
1
+ import type { ChipsItem } from '../types.js';
2
+ export declare function normalizeValue(items: readonly ChipsItem[], multiselect: boolean, value: unknown): string | string[] | null;
@@ -0,0 +1,9 @@
1
+ function normalizeValue(items, multiselect, value) {
2
+ const available = new Set(items.map((item)=>item.value));
3
+ if (!multiselect) return 'string' == typeof value && available.has(value) ? value : null;
4
+ if (!Array.isArray(value)) return [];
5
+ return [
6
+ ...new Set(value.filter((item)=>'string' == typeof item && available.has(item)))
7
+ ];
8
+ }
9
+ export { normalizeValue };
@@ -0,0 +1,19 @@
1
+ :root {
2
+ --chips-gap: var(--size-8);
3
+ --chips-height: var(--size-32);
4
+ --chips-padding-x: var(--size-12);
5
+ --chips-padding-y: var(--size-6);
6
+ --chips-radius: var(--border-radius-pill);
7
+ --chips-color: #1c1a18;
8
+ --chips-background: transparent;
9
+ --chips-hover-background: #e9e4d9;
10
+ --chips-active-background: #dcd4c5;
11
+ --chips-selected-background: #fdde9b;
12
+ --chips-selected-hover-background: #f4d38d;
13
+ --chips-selected-active-background: #e9c479;
14
+ --chips-disabled-background: #cfd1cb;
15
+ --chips-disabled-selected-background: #f7e6c2;
16
+ --chips-disabled-color: #858981;
17
+ --chips-focus-color: #ff6b0a;
18
+ }
19
+
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { Button } from './components/Button/index.js';
2
2
  export type { ButtonProps } from './components/Button/index.js';
3
+ export { Chips } from './components/Chips/index.js';
4
+ export type { ChipsProps, ChipsItem, ChipsChangePayload } from './components/Chips/index.js';
3
5
  export { Input } from './components/Input/index.js';
4
6
  export type { InputProps } from './components/Input/index.js';
5
7
  export { Datepicker, DatepickerPanel } from './components/Datepicker/index.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Button } from "./components/Button/index.js";
2
+ export { Chips } from "./components/Chips/index.js";
2
3
  export { Input } from "./components/Input/index.js";
3
4
  export { Datepicker, DatepickerPanel } from "./components/Datepicker/index.js";
4
5
  export { Select } from "./components/Select/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koslibs/components",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Shared React components for koslibs projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",