@koslibs/components 1.6.0 → 1.8.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 +31 -0
- package/README.md +85 -0
- package/dist/components/Button/button.js +20 -4
- package/dist/components/Button/index_module.css +1 -27
- package/dist/components/Collapse/collapse.js +3 -1
- package/dist/components/Collapse/index_module.css +2 -4
- package/dist/components/Input/index_module.css +9 -20
- package/dist/components/Input/input.d.ts +1 -1
- package/dist/components/Input/vars.css +7 -7
- package/dist/components/Modal/components/footer/index_module.css +1 -1
- package/dist/components/Modal/components/header/index.d.ts +1 -1
- package/dist/components/Modal/components/header/index_module.css +3 -3
- package/dist/components/Modal/index_module.css +2 -2
- package/dist/components/Popover/components/popover-surface/index.js +1 -1
- package/dist/components/Popover/{use-popover-position.d.ts → hooks/use-popover-position.d.ts} +1 -1
- package/dist/components/Spinner/preset_module.css +3 -1
- package/dist/components/Status/index.d.ts +2 -0
- package/dist/components/Status/index.js +1 -0
- package/dist/components/Status/index.module.js +25 -0
- package/dist/components/Status/index_module.css +109 -0
- package/dist/components/Status/status.d.ts +85 -0
- package/dist/components/Status/status.js +35 -0
- package/dist/components/Status/vars.css +36 -0
- package/dist/components/Tabs/context.d.ts +10 -0
- package/dist/components/Tabs/context.js +3 -0
- package/dist/components/Tabs/hooks/use-tab-indicator.d.ts +11 -0
- package/dist/components/Tabs/hooks/use-tab-indicator.js +43 -0
- package/dist/components/Tabs/index.d.ts +3 -0
- package/dist/components/Tabs/index.js +2 -0
- package/dist/components/Tabs/index.module.js +12 -0
- package/dist/components/Tabs/index_module.css +92 -0
- package/dist/components/Tabs/tab.d.ts +9 -0
- package/dist/components/Tabs/tab.js +38 -0
- package/dist/components/Tabs/tabs.d.ts +13 -0
- package/dist/components/Tabs/tabs.js +116 -0
- package/dist/components/Tabs/types.d.ts +61 -0
- package/dist/components/Tabs/types.js +0 -0
- package/dist/components/Tabs/vars.css +20 -0
- package/dist/components/Textarea/index_module.css +8 -19
- package/dist/components/Textarea/textarea.d.ts +1 -1
- package/dist/components/Typography/index.d.ts +1 -1
- package/dist/components/Typography/index_module.css +2 -2
- package/dist/components/Typography/preset.module.js +2 -0
- package/dist/components/Typography/preset_module.css +4 -0
- package/dist/components/Typography/shared.d.ts +1 -1
- package/dist/components/Typography/text.d.ts +1 -1
- package/dist/components/Typography/vars.css +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/styles/theme.css +1 -0
- package/dist/styles/variables.css +63 -0
- package/package.json +2 -1
- /package/dist/components/Popover/{use-popover-position.js → hooks/use-popover-position.js} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
@import "../../styles/theme.css";
|
|
2
|
+
@import "./vars.css";
|
|
3
|
+
|
|
4
|
+
.component-p4fC6J {
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
vertical-align: middle;
|
|
7
|
+
width: fit-content;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
max-width: 100%;
|
|
10
|
+
height: var(--status-height);
|
|
11
|
+
padding: 0 var(--status-padding);
|
|
12
|
+
justify-content: center;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: var(--status-geometry-gap);
|
|
15
|
+
border-radius: var(--status-radius);
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.size-20-RGtFYL {
|
|
20
|
+
--status-height: var(--status-geometry-height-20);
|
|
21
|
+
--status-padding: var(--status-geometry-padding-20);
|
|
22
|
+
--status-rectangular-radius: var(--status-geometry-radius-rectangular-20);
|
|
23
|
+
--status-rounded-radius: var(--status-geometry-radius-rounded-20);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.size-24-80Sg54 {
|
|
27
|
+
--status-height: var(--status-geometry-height-24);
|
|
28
|
+
--status-padding: var(--status-geometry-padding-24);
|
|
29
|
+
--status-rectangular-radius: var(--status-geometry-radius-rectangular-24);
|
|
30
|
+
--status-rounded-radius: var(--status-geometry-radius-rounded-24);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.size-32-SU7Krr {
|
|
34
|
+
--status-height: var(--status-geometry-height-32);
|
|
35
|
+
--status-padding: var(--status-geometry-padding-32);
|
|
36
|
+
--status-rectangular-radius: var(--status-geometry-radius-rectangular-32);
|
|
37
|
+
--status-rounded-radius: var(--status-geometry-radius-rounded-32);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.size-40-TbyUxw {
|
|
41
|
+
--status-height: var(--status-geometry-height-40);
|
|
42
|
+
--status-padding: var(--status-geometry-padding-40);
|
|
43
|
+
--status-rectangular-radius: var(--status-geometry-radius-rectangular-40);
|
|
44
|
+
--status-rounded-radius: var(--status-geometry-radius-rounded-40);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.rectangular-ykPeeK {
|
|
48
|
+
--status-radius: var(--status-rectangular-radius);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.rounded-yFfN8M {
|
|
52
|
+
--status-radius: var(--status-rounded-radius);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.green-Jd9YVz {
|
|
56
|
+
background-color: var(--status-color-green-background);
|
|
57
|
+
color: var(--status-color-green-foreground);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.orange-tRGq_X {
|
|
61
|
+
background-color: var(--status-color-orange-background);
|
|
62
|
+
color: var(--status-color-orange-foreground);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.red-QhwnhH {
|
|
66
|
+
background-color: var(--status-color-red-background);
|
|
67
|
+
color: var(--status-color-red-foreground);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.blue-AisEDx {
|
|
71
|
+
background-color: var(--status-color-blue-background);
|
|
72
|
+
color: var(--status-color-blue-foreground);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.grey-T_mrQo {
|
|
76
|
+
background-color: var(--status-color-grey-background);
|
|
77
|
+
color: var(--status-color-grey-foreground);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.teal-E4zwls {
|
|
81
|
+
background-color: var(--status-color-teal-background);
|
|
82
|
+
color: var(--status-color-teal-foreground);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.purple-UK0BCp {
|
|
86
|
+
background-color: var(--status-color-purple-background);
|
|
87
|
+
color: var(--status-color-purple-foreground);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.sand-Bcwhyf {
|
|
91
|
+
background-color: var(--status-color-sand-background);
|
|
92
|
+
color: var(--status-color-sand-foreground);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.component-p4fC6J .label-Oml2a7 {
|
|
96
|
+
text-overflow: ellipsis;
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
min-width: 0;
|
|
99
|
+
line-height: normal;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.addon-MKQxtF {
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
align-items: center;
|
|
107
|
+
display: flex;
|
|
108
|
+
}
|
|
109
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
export type StatusProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'color'> & {
|
|
3
|
+
/**
|
|
4
|
+
* Высота плашки в пикселях.
|
|
5
|
+
* @default 24
|
|
6
|
+
*/
|
|
7
|
+
size?: 20 | 24 | 32 | 40;
|
|
8
|
+
/**
|
|
9
|
+
* Форма плашки.
|
|
10
|
+
* @default 'rectangular'
|
|
11
|
+
*/
|
|
12
|
+
shape?: 'rounded' | 'rectangular';
|
|
13
|
+
/**
|
|
14
|
+
* Цвет фона, подписи и аддонов.
|
|
15
|
+
* @default 'grey'
|
|
16
|
+
*/
|
|
17
|
+
color?: 'green' | 'orange' | 'red' | 'blue' | 'grey' | 'teal' | 'purple' | 'sand';
|
|
18
|
+
/**
|
|
19
|
+
* Короткая подпись статуса. При ограничении ширины сокращается многоточием.
|
|
20
|
+
*/
|
|
21
|
+
message: string;
|
|
22
|
+
/**
|
|
23
|
+
* Отображать подпись заглавными буквами.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
uppercase?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Декоративный элемент слева от подписи, скрытый от скринридера.
|
|
29
|
+
*/
|
|
30
|
+
leftAddon?: ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* Декоративный элемент справа от подписи, скрытый от скринридера.
|
|
33
|
+
*/
|
|
34
|
+
rightAddon?: ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* Дополнительный CSS-класс подписи.
|
|
37
|
+
*/
|
|
38
|
+
labelClassName?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Идентификатор корневого элемента для тестов.
|
|
41
|
+
*/
|
|
42
|
+
dataTestId?: string;
|
|
43
|
+
};
|
|
44
|
+
export declare const Status: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLSpanElement>, "color" | "children"> & {
|
|
45
|
+
/**
|
|
46
|
+
* Высота плашки в пикселях.
|
|
47
|
+
* @default 24
|
|
48
|
+
*/
|
|
49
|
+
size?: 20 | 24 | 32 | 40;
|
|
50
|
+
/**
|
|
51
|
+
* Форма плашки.
|
|
52
|
+
* @default 'rectangular'
|
|
53
|
+
*/
|
|
54
|
+
shape?: "rounded" | "rectangular";
|
|
55
|
+
/**
|
|
56
|
+
* Цвет фона, подписи и аддонов.
|
|
57
|
+
* @default 'grey'
|
|
58
|
+
*/
|
|
59
|
+
color?: "green" | "orange" | "red" | "blue" | "grey" | "teal" | "purple" | "sand";
|
|
60
|
+
/**
|
|
61
|
+
* Короткая подпись статуса. При ограничении ширины сокращается многоточием.
|
|
62
|
+
*/
|
|
63
|
+
message: string;
|
|
64
|
+
/**
|
|
65
|
+
* Отображать подпись заглавными буквами.
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
uppercase?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Декоративный элемент слева от подписи, скрытый от скринридера.
|
|
71
|
+
*/
|
|
72
|
+
leftAddon?: ReactNode;
|
|
73
|
+
/**
|
|
74
|
+
* Декоративный элемент справа от подписи, скрытый от скринридера.
|
|
75
|
+
*/
|
|
76
|
+
rightAddon?: ReactNode;
|
|
77
|
+
/**
|
|
78
|
+
* Дополнительный CSS-класс подписи.
|
|
79
|
+
*/
|
|
80
|
+
labelClassName?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Идентификатор корневого элемента для тестов.
|
|
83
|
+
*/
|
|
84
|
+
dataTestId?: string;
|
|
85
|
+
} & import("react").RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { TypographyText } from "../Typography/text.js";
|
|
5
|
+
import index_module from "./index.module.js";
|
|
6
|
+
function hasAddon(addon) {
|
|
7
|
+
return null != addon && 'boolean' != typeof addon;
|
|
8
|
+
}
|
|
9
|
+
const Status = /*#__PURE__*/ forwardRef(({ size = 24, shape = 'rectangular', color = 'grey', leftAddon, rightAddon, message, uppercase = false, className, labelClassName, dataTestId, ...restProps }, ref)=>/*#__PURE__*/ jsxs("span", {
|
|
10
|
+
...restProps,
|
|
11
|
+
ref: ref,
|
|
12
|
+
className: classnames(index_module.component, index_module[`size-${size}`], index_module[shape], index_module[color], className),
|
|
13
|
+
"data-test-id": dataTestId,
|
|
14
|
+
children: [
|
|
15
|
+
hasAddon(leftAddon) && /*#__PURE__*/ jsx("span", {
|
|
16
|
+
className: index_module.addon,
|
|
17
|
+
"aria-hidden": "true",
|
|
18
|
+
children: leftAddon
|
|
19
|
+
}),
|
|
20
|
+
/*#__PURE__*/ jsx(TypographyText, {
|
|
21
|
+
size: size <= 24 ? 12 : 14,
|
|
22
|
+
weight: "medium",
|
|
23
|
+
caps: uppercase,
|
|
24
|
+
className: classnames(index_module.label, labelClassName),
|
|
25
|
+
children: message
|
|
26
|
+
}),
|
|
27
|
+
hasAddon(rightAddon) && /*#__PURE__*/ jsx("span", {
|
|
28
|
+
className: index_module.addon,
|
|
29
|
+
"aria-hidden": "true",
|
|
30
|
+
children: rightAddon
|
|
31
|
+
})
|
|
32
|
+
]
|
|
33
|
+
}));
|
|
34
|
+
Status.displayName = 'Status';
|
|
35
|
+
export { Status };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--status-color-green-background: #d1f1d7;
|
|
3
|
+
--status-color-green-foreground: #08752a;
|
|
4
|
+
--status-color-orange-background: #ffe6cc;
|
|
5
|
+
--status-color-orange-foreground: #9a4600;
|
|
6
|
+
--status-color-red-background: #fde0df;
|
|
7
|
+
--status-color-red-foreground: #b42318;
|
|
8
|
+
--status-color-blue-background: #ddebff;
|
|
9
|
+
--status-color-blue-foreground: #175cd3;
|
|
10
|
+
--status-color-grey-background: #e9eaec;
|
|
11
|
+
--status-color-grey-foreground: #535862;
|
|
12
|
+
--status-color-teal-background: #d4f1ed;
|
|
13
|
+
--status-color-teal-foreground: #0b6b60;
|
|
14
|
+
--status-color-purple-background: #e9e2ff;
|
|
15
|
+
--status-color-purple-foreground: #6941c6;
|
|
16
|
+
--status-color-sand-background: #ede9de;
|
|
17
|
+
--status-color-sand-foreground: #736554;
|
|
18
|
+
--status-geometry-height-20: var(--size-20);
|
|
19
|
+
--status-geometry-height-24: var(--size-24);
|
|
20
|
+
--status-geometry-height-32: var(--size-32);
|
|
21
|
+
--status-geometry-height-40: var(--size-40);
|
|
22
|
+
--status-geometry-padding-20: var(--size-8);
|
|
23
|
+
--status-geometry-padding-24: var(--size-12);
|
|
24
|
+
--status-geometry-padding-32: var(--size-16);
|
|
25
|
+
--status-geometry-padding-40: var(--size-20);
|
|
26
|
+
--status-geometry-radius-rectangular-20: var(--border-radius-6);
|
|
27
|
+
--status-geometry-radius-rectangular-24: var(--border-radius-6);
|
|
28
|
+
--status-geometry-radius-rectangular-32: var(--border-radius-10);
|
|
29
|
+
--status-geometry-radius-rectangular-40: var(--border-radius-12);
|
|
30
|
+
--status-geometry-radius-rounded-20: var(--border-radius-10);
|
|
31
|
+
--status-geometry-radius-rounded-24: var(--border-radius-12);
|
|
32
|
+
--status-geometry-radius-rounded-32: var(--border-radius-16);
|
|
33
|
+
--status-geometry-radius-rounded-40: var(--border-radius-20);
|
|
34
|
+
--status-geometry-gap: var(--size-6);
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TabsProps } from './types.js';
|
|
2
|
+
type TabsContextValue = {
|
|
3
|
+
selectedId?: string;
|
|
4
|
+
size: NonNullable<TabsProps['size']>;
|
|
5
|
+
dataTestId?: string;
|
|
6
|
+
registerTab: (id: string, button: HTMLButtonElement | null) => void;
|
|
7
|
+
selectTab: (id: string, event: Parameters<NonNullable<TabsProps['onChange']>>[0]) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare const TabsContext: import("react").Context<TabsContextValue | null>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type MutableRefObject, type RefObject } from 'react';
|
|
2
|
+
import type { TabProps, TabsProps } from '../types.js';
|
|
3
|
+
type TabIndicatorOptions = {
|
|
4
|
+
selectedId?: string;
|
|
5
|
+
items: readonly TabProps[];
|
|
6
|
+
size: NonNullable<TabsProps['size']>;
|
|
7
|
+
listRef: RefObject<HTMLDivElement>;
|
|
8
|
+
buttons: MutableRefObject<Map<string, HTMLButtonElement>>;
|
|
9
|
+
};
|
|
10
|
+
export declare function useTabIndicator({ selectedId, items, size, listRef, buttons, }: TabIndicatorOptions): RefObject<HTMLDivElement>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from "react";
|
|
2
|
+
function useTabIndicator({ selectedId, items, size, listRef, buttons }) {
|
|
3
|
+
const indicatorRef = useRef(null);
|
|
4
|
+
const indicatorPosition = useRef();
|
|
5
|
+
useLayoutEffect(()=>{
|
|
6
|
+
const list = listRef.current;
|
|
7
|
+
const indicator = indicatorRef.current;
|
|
8
|
+
const button = void 0 !== selectedId ? buttons.current.get(selectedId) : void 0;
|
|
9
|
+
if (!list || !indicator || !button || void 0 === selectedId) {
|
|
10
|
+
indicatorPosition.current = void 0;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const updateIndicator = ()=>{
|
|
14
|
+
const left = button.offsetLeft;
|
|
15
|
+
const width = button.offsetWidth;
|
|
16
|
+
const previous = indicatorPosition.current;
|
|
17
|
+
if (previous?.element === indicator && previous.id === selectedId && previous.left === left && previous.width === width) return;
|
|
18
|
+
indicator.style.transition = previous?.element === indicator && previous.id !== selectedId ? '' : 'none';
|
|
19
|
+
indicator.style.width = `${width}px`;
|
|
20
|
+
indicator.style.transform = `translateX(${left}px)`;
|
|
21
|
+
indicatorPosition.current = {
|
|
22
|
+
id: selectedId,
|
|
23
|
+
left,
|
|
24
|
+
width,
|
|
25
|
+
element: indicator
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
updateIndicator();
|
|
29
|
+
if ("u" < typeof ResizeObserver) return;
|
|
30
|
+
const observer = new ResizeObserver(updateIndicator);
|
|
31
|
+
observer.observe(list);
|
|
32
|
+
for (const tab of buttons.current.values())observer.observe(tab);
|
|
33
|
+
return ()=>observer.disconnect();
|
|
34
|
+
}, [
|
|
35
|
+
items,
|
|
36
|
+
selectedId,
|
|
37
|
+
size,
|
|
38
|
+
listRef,
|
|
39
|
+
buttons
|
|
40
|
+
]);
|
|
41
|
+
return indicatorRef;
|
|
42
|
+
}
|
|
43
|
+
export { useTabIndicator };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "./index_module.css";
|
|
2
|
+
const index_module = {
|
|
3
|
+
component: "component-mEZo4o",
|
|
4
|
+
list: "list-pbTqPB",
|
|
5
|
+
tab: "tab-yKhKqB",
|
|
6
|
+
size40: "size40-F0WNwL",
|
|
7
|
+
size48: "size48-cLRBaS",
|
|
8
|
+
selected: "selected-Lm0vXW",
|
|
9
|
+
counter: "counter-Tokn9i",
|
|
10
|
+
indicator: "indicator-LDebsI"
|
|
11
|
+
};
|
|
12
|
+
export default index_module;
|
|
@@ -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 };
|