@koslibs/components 1.5.0 → 1.7.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +85 -0
  3. package/dist/components/Button/button.js +20 -4
  4. package/dist/components/Button/index_module.css +1 -27
  5. package/dist/components/Collapse/collapse.js +3 -1
  6. package/dist/components/Collapse/index_module.css +2 -4
  7. package/dist/components/Input/index_module.css +9 -20
  8. package/dist/components/Input/input.d.ts +1 -1
  9. package/dist/components/Input/vars.css +7 -7
  10. package/dist/components/Modal/components/footer/index_module.css +1 -1
  11. package/dist/components/Modal/components/header/index.d.ts +1 -1
  12. package/dist/components/Modal/components/header/index_module.css +3 -3
  13. package/dist/components/Modal/index_module.css +2 -2
  14. package/dist/components/Popover/components/popover-surface/index.js +1 -1
  15. package/dist/components/Popover/{use-popover-position.d.ts → hooks/use-popover-position.d.ts} +1 -1
  16. package/dist/components/Spinner/preset_module.css +3 -1
  17. package/dist/components/Tabs/context.d.ts +10 -0
  18. package/dist/components/Tabs/context.js +3 -0
  19. package/dist/components/Tabs/hooks/use-tab-indicator.d.ts +11 -0
  20. package/dist/components/Tabs/hooks/use-tab-indicator.js +43 -0
  21. package/dist/components/Tabs/index.d.ts +3 -0
  22. package/dist/components/Tabs/index.js +2 -0
  23. package/dist/components/Tabs/index.module.js +12 -0
  24. package/dist/components/Tabs/index_module.css +92 -0
  25. package/dist/components/Tabs/tab.d.ts +9 -0
  26. package/dist/components/Tabs/tab.js +38 -0
  27. package/dist/components/Tabs/tabs.d.ts +13 -0
  28. package/dist/components/Tabs/tabs.js +116 -0
  29. package/dist/components/Tabs/types.d.ts +61 -0
  30. package/dist/components/Tabs/types.js +0 -0
  31. package/dist/components/Tabs/vars.css +20 -0
  32. package/dist/components/Textarea/hooks/use-autosize.d.ts +2 -0
  33. package/dist/components/Textarea/hooks/use-autosize.js +106 -0
  34. package/dist/components/Textarea/index.d.ts +2 -0
  35. package/dist/components/Textarea/index.js +1 -0
  36. package/dist/components/Textarea/index.module.js +14 -0
  37. package/dist/components/Textarea/index_module.css +269 -0
  38. package/dist/components/Textarea/textarea.d.ts +27 -0
  39. package/dist/components/Textarea/textarea.js +173 -0
  40. package/dist/components/Textarea/types.d.ts +108 -0
  41. package/dist/components/Textarea/types.js +0 -0
  42. package/dist/components/Textarea/vars.css +9 -0
  43. package/dist/components/Typography/index.d.ts +1 -1
  44. package/dist/components/Typography/index_module.css +2 -2
  45. package/dist/components/Typography/preset.module.js +2 -0
  46. package/dist/components/Typography/preset_module.css +4 -0
  47. package/dist/components/Typography/shared.d.ts +1 -1
  48. package/dist/components/Typography/text.d.ts +1 -1
  49. package/dist/components/Typography/vars.css +1 -1
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.js +2 -0
  52. package/dist/styles/theme.css +1 -0
  53. package/dist/styles/variables.css +63 -0
  54. package/package.json +2 -1
  55. /package/dist/components/Popover/{use-popover-position.js → hooks/use-popover-position.js} +0 -0
@@ -0,0 +1,92 @@
1
+ @import "../../styles/theme.css";
2
+ @import "./vars.css";
3
+
4
+ .component-mEZo4o {
5
+ width: 100%;
6
+ min-width: 0;
7
+ font-family: var(--font-primary);
8
+ }
9
+
10
+ .list-pbTqPB {
11
+ gap: var(--tabs-gap);
12
+ box-shadow: inset 0 -1px var(--tabs-divider-color);
13
+ display: flex;
14
+ position: relative;
15
+ }
16
+
17
+ .tab-yKhKqB {
18
+ justify-content: center;
19
+ align-items: center;
20
+ gap: var(--tabs-gap);
21
+ box-sizing: border-box;
22
+ margin: var(--size-0);
23
+ padding: var(--size-0) var(--tabs-padding);
24
+ border-radius: var(--tabs-border-radius);
25
+ color: var(--tabs-muted-color);
26
+ white-space: nowrap;
27
+ cursor: pointer;
28
+ -webkit-tap-highlight-color: transparent;
29
+ background: none;
30
+ border: 0;
31
+ outline: none;
32
+ flex: none;
33
+ display: inline-flex;
34
+ position: relative;
35
+ }
36
+
37
+ .size40-F0WNwL {
38
+ height: var(--tabs-height-40);
39
+ }
40
+
41
+ .size48-cLRBaS {
42
+ height: var(--tabs-height-48);
43
+ }
44
+
45
+ .tab-yKhKqB:after {
46
+ height: var(--tabs-indicator-height);
47
+ content: "";
48
+ background: none;
49
+ position: absolute;
50
+ bottom: 0;
51
+ left: 0;
52
+ right: 0;
53
+ }
54
+
55
+ .tab-yKhKqB:hover:not(:disabled) {
56
+ color: var(--tabs-color);
57
+ }
58
+
59
+ .tab-yKhKqB:hover:not(:disabled):not(.selected-Lm0vXW):after {
60
+ background: var(--tabs-hover-indicator-color);
61
+ }
62
+
63
+ .tab-yKhKqB.selected-Lm0vXW, .tab-yKhKqB .counter-Tokn9i {
64
+ color: var(--tabs-color);
65
+ }
66
+
67
+ .indicator-LDebsI {
68
+ height: var(--tabs-indicator-height);
69
+ background: var(--tabs-indicator-color);
70
+ pointer-events: none;
71
+ transition: transform var(--tabs-indicator-transition-duration) ease,
72
+ width var(--tabs-indicator-transition-duration) ease;
73
+ position: absolute;
74
+ bottom: 0;
75
+ left: 0;
76
+ }
77
+
78
+ .tab-yKhKqB .counter-Tokn9i {
79
+ padding: var(--size-0) var(--tabs-counter-padding);
80
+ border-radius: var(--tabs-counter-border-radius);
81
+ background: var(--tabs-counter-background-color);
82
+ }
83
+
84
+ .tab-yKhKqB:disabled, .tab-yKhKqB:disabled .counter-Tokn9i {
85
+ color: var(--tabs-disabled-color);
86
+ cursor: default;
87
+ }
88
+
89
+ .tab-yKhKqB:disabled .counter-Tokn9i {
90
+ background: var(--tabs-disabled-counter-background-color);
91
+ }
92
+
@@ -0,0 +1,9 @@
1
+ import type { TabProps } from './types.js';
2
+ /**
3
+ * Кнопка вкладки с заголовком, счётчиком и состоянием выбора из Tabs.
4
+ * Содержимое выбранной вкладки отображается отдельно в приложении.
5
+ */
6
+ export declare const Tab: {
7
+ ({ id, title, count, disabled }: TabProps): import("react").JSX.Element;
8
+ displayName: string;
9
+ };
@@ -0,0 +1,38 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { useContext } from "react";
4
+ import { useDataTestId } from "../../hooks/use-data-test-id.js";
5
+ import { TypographyText } from "../Typography/text.js";
6
+ import { TabsContext } from "./context.js";
7
+ import index_module from "./index.module.js";
8
+ const Tab = ({ id, title, count, disabled = false })=>{
9
+ const context = useContext(TabsContext);
10
+ const getDataTestId = useDataTestId(context?.dataTestId);
11
+ if (!context) throw new Error('Tab must be rendered inside Tabs.');
12
+ const { selectedId, size, registerTab, selectTab } = context;
13
+ const selected = id === selectedId;
14
+ return /*#__PURE__*/ jsxs("button", {
15
+ ref: (button)=>registerTab(id, button),
16
+ type: "button",
17
+ disabled: disabled,
18
+ className: classnames(index_module.tab, index_module[`size${size}`], {
19
+ [index_module.selected]: selected
20
+ }),
21
+ onClick: (event)=>selectTab(id, event),
22
+ "data-test-id": getDataTestId(`tab-${id}`),
23
+ children: [
24
+ /*#__PURE__*/ jsx(TypographyText, {
25
+ size: 14,
26
+ weight: "medium",
27
+ children: title
28
+ }),
29
+ void 0 !== count && Number.isInteger(count) && count >= 0 && /*#__PURE__*/ jsx(TypographyText, {
30
+ size: 11,
31
+ className: index_module.counter,
32
+ children: count
33
+ })
34
+ ]
35
+ });
36
+ };
37
+ Tab.displayName = 'Tab';
38
+ export { Tab };
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const Tabs: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLDivElement>, "children" | "onChange"> & {
3
+ children?: ReactNode;
4
+ selectedId?: string;
5
+ defaultSelectedId?: string;
6
+ onChange?: (event: import("react").MouseEvent<HTMLButtonElement>, payload: {
7
+ selectedId: string;
8
+ }) => void;
9
+ size?: 40 | 48;
10
+ emptyState?: ReactNode;
11
+ listClassName?: string;
12
+ dataTestId?: string;
13
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,116 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { Children, Fragment, forwardRef, isValidElement, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { useDataTestId } from "../../hooks/use-data-test-id.js";
5
+ import { TabsContext } from "./context.js";
6
+ import { useTabIndicator } from "./hooks/use-tab-indicator.js";
7
+ import { Tab } from "./tab.js";
8
+ import index_module from "./index.module.js";
9
+ function collectTabs(children) {
10
+ const items = [];
11
+ const ids = new Set();
12
+ const collect = (nodes)=>{
13
+ Children.forEach(nodes, (child)=>{
14
+ if (null == child || 'boolean' == typeof child) return;
15
+ if (/*#__PURE__*/ isValidElement(child) && child.type === Fragment) return void collect(child.props.children);
16
+ if (!/*#__PURE__*/ isValidElement(child) || child.type !== Tab) throw new Error('Tabs accepts only Tab elements and Fragments.');
17
+ if (ids.has(child.props.id)) throw new Error(`Tabs requires unique Tab ids: "${child.props.id}" is duplicated.`);
18
+ ids.add(child.props.id);
19
+ items.push(child.props);
20
+ });
21
+ };
22
+ collect(children);
23
+ return items;
24
+ }
25
+ function resolveValue(items, value, previous) {
26
+ const index = items.findIndex((item)=>item.id === value);
27
+ if (index >= 0 && !items[index].disabled) return value;
28
+ if (index < 0) {
29
+ const previousIndex = previous.items.findIndex((item)=>item.id === value);
30
+ if (previousIndex >= 0) {
31
+ const neighbors = [
32
+ ...previous.items.slice(previousIndex + 1),
33
+ ...previous.items.slice(0, previousIndex).reverse()
34
+ ];
35
+ const neighbor = neighbors.find((candidate)=>items.some((item)=>item.id === candidate.id && !item.disabled));
36
+ if (neighbor) return neighbor.id;
37
+ }
38
+ if (items.some((item)=>item.id === previous.value && !item.disabled)) return previous.value;
39
+ }
40
+ const start = Math.max(0, index);
41
+ return items.slice(start).find((item)=>!item.disabled)?.id ?? items.slice(0, start).reverse().find((item)=>!item.disabled)?.id;
42
+ }
43
+ const Tabs = /*#__PURE__*/ forwardRef(({ children, selectedId, defaultSelectedId, onChange, size = 40, emptyState, listClassName, dataTestId, className, ...restProps }, ref)=>{
44
+ const getDataTestId = useDataTestId(dataTestId);
45
+ const items = useMemo(()=>collectTabs(children), [
46
+ children
47
+ ]);
48
+ const [internalValue, setInternalValue] = useState(defaultSelectedId);
49
+ const previousSelection = useRef({
50
+ items
51
+ });
52
+ const selectedValue = resolveValue(items, selectedId ?? internalValue, previousSelection.current);
53
+ const listRef = useRef(null);
54
+ const buttons = useRef(new Map());
55
+ useLayoutEffect(()=>{
56
+ previousSelection.current = {
57
+ items,
58
+ value: selectedValue
59
+ };
60
+ if (void 0 === selectedId && internalValue !== selectedValue) setInternalValue(selectedValue);
61
+ }, [
62
+ items,
63
+ selectedValue,
64
+ selectedId,
65
+ internalValue
66
+ ]);
67
+ const indicatorRef = useTabIndicator({
68
+ selectedId: selectedValue,
69
+ items,
70
+ size,
71
+ listRef,
72
+ buttons
73
+ });
74
+ const select = (id, event)=>{
75
+ if (id === selectedValue) return;
76
+ if (void 0 === selectedId) setInternalValue(id);
77
+ onChange?.(event, {
78
+ selectedId: id
79
+ });
80
+ };
81
+ return /*#__PURE__*/ jsx("div", {
82
+ ...restProps,
83
+ ref: ref,
84
+ className: classnames(index_module.component, className),
85
+ "data-test-id": dataTestId,
86
+ children: void 0 === selectedValue ? emptyState : /*#__PURE__*/ jsx(TabsContext.Provider, {
87
+ value: {
88
+ selectedId: selectedValue,
89
+ size,
90
+ dataTestId,
91
+ registerTab: (id, button)=>{
92
+ if (button) buttons.current.set(id, button);
93
+ else buttons.current.delete(id);
94
+ },
95
+ selectTab: select
96
+ },
97
+ children: /*#__PURE__*/ jsxs("div", {
98
+ ref: listRef,
99
+ className: classnames(index_module.list, listClassName),
100
+ "data-test-id": getDataTestId('list'),
101
+ children: [
102
+ items.map((item)=>/*#__PURE__*/ jsx(Tab, {
103
+ ...item
104
+ }, item.id)),
105
+ /*#__PURE__*/ jsx("div", {
106
+ ref: indicatorRef,
107
+ className: index_module.indicator,
108
+ "data-test-id": getDataTestId('indicator')
109
+ })
110
+ ]
111
+ })
112
+ })
113
+ });
114
+ });
115
+ Tabs.displayName = 'Tabs';
116
+ export { Tabs };
@@ -0,0 +1,61 @@
1
+ import type { HTMLAttributes, MouseEvent, ReactNode } from 'react';
2
+ export type TabProps = {
3
+ /**
4
+ * Уникальный стабильный идентификатор вкладки.
5
+ */
6
+ id: string;
7
+ /**
8
+ * Текст заголовка вкладки.
9
+ */
10
+ title: ReactNode;
11
+ /**
12
+ * Неотрицательное целое число. Неизвестное значение скрывается, 0 отображается.
13
+ */
14
+ count?: number;
15
+ /**
16
+ * Запрещает выбор вкладки.
17
+ * @default false
18
+ */
19
+ disabled?: boolean;
20
+ };
21
+ export type TabsProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> & {
22
+ /**
23
+ * Дочерние Tab в порядке отображения. id должны быть уникальными.
24
+ * Поддерживаются массивы, условные элементы и Fragment.
25
+ */
26
+ children?: ReactNode;
27
+ /**
28
+ * Идентификатор активной вкладки в controlled-режиме.
29
+ */
30
+ selectedId?: string;
31
+ /**
32
+ * Начальная вкладка в uncontrolled-режиме. По умолчанию — первая доступная.
33
+ */
34
+ defaultSelectedId?: string;
35
+ /**
36
+ * Запрос выбора другой вкладки пользователем: событие и { selectedId }.
37
+ * В controlled-режиме обновите selectedId. Повторный выбор и изменение children
38
+ * не вызывают обработчик.
39
+ */
40
+ onChange?: (event: MouseEvent<HTMLButtonElement>, payload: {
41
+ selectedId: string;
42
+ }) => void;
43
+ /**
44
+ * Высота заголовков вкладок в пикселях.
45
+ * @default 40
46
+ */
47
+ size?: 40 | 48;
48
+ /**
49
+ * Общее пустое состояние, когда нет доступных вкладок.
50
+ */
51
+ emptyState?: ReactNode;
52
+ /**
53
+ * Класс ряда заголовков.
54
+ */
55
+ listClassName?: string;
56
+ /**
57
+ * Идентификатор корня. Части получают суффиксы -list, -indicator,
58
+ * -tab-{id}.
59
+ */
60
+ dataTestId?: string;
61
+ };
File without changes
@@ -0,0 +1,20 @@
1
+ :root {
2
+ --tabs-color: #1c1a18;
3
+ --tabs-muted-color: #736554;
4
+ --tabs-disabled-color: #9da099;
5
+ --tabs-indicator-color: #d95300;
6
+ --tabs-hover-indicator-color: #c8beaa;
7
+ --tabs-counter-background-color: #e0dbce;
8
+ --tabs-disabled-counter-background-color: #e0dbce73;
9
+ --tabs-divider-color: #e0dbce;
10
+ --tabs-height-40: 40px;
11
+ --tabs-height-48: 48px;
12
+ --tabs-padding: var(--size-12);
13
+ --tabs-gap: var(--size-8);
14
+ --tabs-border-radius: var(--border-radius-4);
15
+ --tabs-indicator-height: 2px;
16
+ --tabs-indicator-transition-duration: .2s;
17
+ --tabs-counter-padding: var(--size-6);
18
+ --tabs-counter-border-radius: var(--border-radius-pill);
19
+ }
20
+
@@ -0,0 +1,2 @@
1
+ import { type RefObject } from 'react';
2
+ export declare function useAutosize(ref: RefObject<HTMLTextAreaElement>, autosize: boolean, minRows: number, maxRows?: number, formId?: string): void;
@@ -0,0 +1,106 @@
1
+ import { useEffect, useLayoutEffect, useRef } from "react";
2
+ const useBrowserLayoutEffect = "u" < typeof document ? useEffect : useLayoutEffect;
3
+ function useAutosize(ref, autosize, minRows, maxRows, formId) {
4
+ const updateRef = useRef();
5
+ useBrowserLayoutEffect(()=>{
6
+ if (!autosize) return;
7
+ const textarea = ref.current;
8
+ if (!textarea) return;
9
+ const doc = textarea.ownerDocument;
10
+ const view = doc.defaultView;
11
+ if (!view) return;
12
+ const measure = doc.createElement('textarea');
13
+ measure.tabIndex = -1;
14
+ Object.assign(measure.style, {
15
+ position: 'absolute',
16
+ visibility: 'hidden',
17
+ pointerEvents: 'none',
18
+ height: '0',
19
+ minHeight: '0',
20
+ maxHeight: 'none',
21
+ padding: '0',
22
+ border: '0',
23
+ boxSizing: 'content-box',
24
+ overflow: 'hidden',
25
+ top: '0',
26
+ left: '0'
27
+ });
28
+ doc.body.append(measure);
29
+ const update = ()=>{
30
+ const computed = view.getComputedStyle(textarea);
31
+ const width = textarea.clientWidth;
32
+ if (!width) return;
33
+ Object.assign(measure.style, {
34
+ width: `${width}px`,
35
+ fontFamily: computed.fontFamily,
36
+ fontSize: computed.fontSize,
37
+ fontWeight: computed.fontWeight,
38
+ fontStyle: computed.fontStyle,
39
+ lineHeight: computed.lineHeight,
40
+ letterSpacing: computed.letterSpacing,
41
+ textIndent: computed.textIndent,
42
+ textTransform: computed.textTransform,
43
+ wordBreak: computed.wordBreak,
44
+ overflowWrap: computed.overflowWrap
45
+ });
46
+ measure.wrap = textarea.wrap;
47
+ const lineHeight = parseFloat(computed.lineHeight);
48
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) return;
49
+ measure.value = textarea.value || textarea.placeholder || ' ';
50
+ const height = Math.min(Math.max(measure.scrollHeight, minRows * lineHeight), void 0 === maxRows ? 1 / 0 : maxRows * lineHeight);
51
+ textarea.style.height = `${height}px`;
52
+ };
53
+ update();
54
+ updateRef.current = update;
55
+ const handleInput = ()=>{
56
+ queueMicrotask(()=>{
57
+ if (measure.isConnected) update();
58
+ });
59
+ };
60
+ textarea.addEventListener('input', handleInput);
61
+ const handleReset = (event)=>{
62
+ setTimeout(()=>{
63
+ if (measure.isConnected && !event.defaultPrevented) update();
64
+ }, 0);
65
+ };
66
+ const form = textarea.form;
67
+ form?.addEventListener('reset', handleReset);
68
+ let previousWidth = textarea.clientWidth;
69
+ const observer = "u" < typeof ResizeObserver ? void 0 : new ResizeObserver(()=>{
70
+ const width = textarea.clientWidth;
71
+ if (width !== previousWidth) {
72
+ previousWidth = width;
73
+ update();
74
+ }
75
+ });
76
+ observer?.observe(textarea);
77
+ view.addEventListener('resize', update);
78
+ doc.fonts?.addEventListener('loadingdone', update);
79
+ return ()=>{
80
+ observer?.disconnect();
81
+ view.removeEventListener('resize', update);
82
+ textarea.removeEventListener('input', handleInput);
83
+ form?.removeEventListener('reset', handleReset);
84
+ doc.fonts?.removeEventListener('loadingdone', update);
85
+ measure.remove();
86
+ updateRef.current = void 0;
87
+ };
88
+ }, [
89
+ ref,
90
+ autosize,
91
+ minRows,
92
+ maxRows,
93
+ formId
94
+ ]);
95
+ useBrowserLayoutEffect(()=>{
96
+ updateRef.current?.();
97
+ });
98
+ useBrowserLayoutEffect(()=>{
99
+ if (!autosize) ref.current?.style.removeProperty('height');
100
+ }, [
101
+ ref,
102
+ autosize,
103
+ minRows
104
+ ]);
105
+ }
106
+ export { useAutosize };
@@ -0,0 +1,2 @@
1
+ export { Textarea } from './textarea.js';
2
+ export type { TextareaProps } from './types.js';
@@ -0,0 +1 @@
1
+ export { Textarea } from "./textarea.js";
@@ -0,0 +1,14 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-K9LhZN",
4
+ field: "field-GADSqb",
5
+ textarea: "textarea-vCe_J9",
6
+ value: "value-pi9twE",
7
+ header: "header-AqxZRm",
8
+ innerHeader: "innerHeader-N47qW7",
9
+ counter: "counter-XxZoUu",
10
+ label: "label-fhdpHS",
11
+ clear: "clear-BZMCFP",
12
+ message: "message-_mT8Ov"
13
+ };
14
+ export default index_module;