@xsolla/xui-tabs 0.74.0 → 0.75.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/native/index.d.mts +8 -1
- package/native/index.d.ts +8 -1
- package/native/index.js +169 -9
- package/native/index.js.flow +159 -0
- package/native/index.js.map +1 -1
- package/native/index.mjs +170 -10
- package/native/index.mjs.map +1 -1
- package/package.json +12 -5
- package/web/index.d.mts +8 -1
- package/web/index.d.ts +8 -1
- package/web/index.js +169 -9
- package/web/index.js.flow +159 -0
- package/web/index.js.map +1 -1
- package/web/index.mjs +170 -10
- package/web/index.mjs.map +1 -1
package/native/index.d.mts
CHANGED
|
@@ -25,8 +25,12 @@ interface TabsProps {
|
|
|
25
25
|
onChange?: (id: string) => void;
|
|
26
26
|
/** Size variant of the tabs */
|
|
27
27
|
size?: "xl" | "lg" | "md" | "sm";
|
|
28
|
-
/**
|
|
28
|
+
/** Visual variant of the tabs */
|
|
29
|
+
variant?: "line" | "segmented";
|
|
30
|
+
/** Whether to align tabs to the left (only for line variant) */
|
|
29
31
|
alignLeft?: boolean;
|
|
32
|
+
/** Whether the component should stretch to fill its container */
|
|
33
|
+
stretched?: boolean;
|
|
30
34
|
/** Accessible label for the tab list */
|
|
31
35
|
"aria-label"?: string;
|
|
32
36
|
/** ID of element that labels this tab list */
|
|
@@ -47,6 +51,9 @@ interface TabsProps {
|
|
|
47
51
|
* - End: Jump to last tab
|
|
48
52
|
* - Enter/Space: Activate focused tab (when activateOnFocus is false)
|
|
49
53
|
*
|
|
54
|
+
* Variants:
|
|
55
|
+
* - "line" (default): Traditional underlined tabs
|
|
56
|
+
* - "segmented": Button-group style segmented control
|
|
50
57
|
*/
|
|
51
58
|
declare const Tabs: React.FC<TabsProps>;
|
|
52
59
|
/**
|
package/native/index.d.ts
CHANGED
|
@@ -25,8 +25,12 @@ interface TabsProps {
|
|
|
25
25
|
onChange?: (id: string) => void;
|
|
26
26
|
/** Size variant of the tabs */
|
|
27
27
|
size?: "xl" | "lg" | "md" | "sm";
|
|
28
|
-
/**
|
|
28
|
+
/** Visual variant of the tabs */
|
|
29
|
+
variant?: "line" | "segmented";
|
|
30
|
+
/** Whether to align tabs to the left (only for line variant) */
|
|
29
31
|
alignLeft?: boolean;
|
|
32
|
+
/** Whether the component should stretch to fill its container */
|
|
33
|
+
stretched?: boolean;
|
|
30
34
|
/** Accessible label for the tab list */
|
|
31
35
|
"aria-label"?: string;
|
|
32
36
|
/** ID of element that labels this tab list */
|
|
@@ -47,6 +51,9 @@ interface TabsProps {
|
|
|
47
51
|
* - End: Jump to last tab
|
|
48
52
|
* - Enter/Space: Activate focused tab (when activateOnFocus is false)
|
|
49
53
|
*
|
|
54
|
+
* Variants:
|
|
55
|
+
* - "line" (default): Traditional underlined tabs
|
|
56
|
+
* - "segmented": Button-group style segmented control
|
|
50
57
|
*/
|
|
51
58
|
declare const Tabs: React.FC<TabsProps>;
|
|
52
59
|
/**
|
package/native/index.js
CHANGED
|
@@ -521,12 +521,15 @@ TextAreaPrimitive.displayName = "TextAreaPrimitive";
|
|
|
521
521
|
var import_xui_core = require("@xsolla/xui-core");
|
|
522
522
|
var import_xui_badge = require("@xsolla/xui-badge");
|
|
523
523
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
524
|
+
var isWeb = typeof document !== "undefined";
|
|
524
525
|
var Tabs = ({
|
|
525
526
|
tabs,
|
|
526
527
|
activeTabId,
|
|
527
528
|
onChange,
|
|
528
529
|
size = "md",
|
|
530
|
+
variant = "line",
|
|
529
531
|
alignLeft = true,
|
|
532
|
+
stretched = false,
|
|
530
533
|
"aria-label": ariaLabel,
|
|
531
534
|
"aria-labelledby": ariaLabelledBy,
|
|
532
535
|
activateOnFocus = true,
|
|
@@ -534,10 +537,27 @@ var Tabs = ({
|
|
|
534
537
|
testID
|
|
535
538
|
}) => {
|
|
536
539
|
const { theme } = (0, import_xui_core.useDesignSystem)();
|
|
537
|
-
const
|
|
540
|
+
const isSegmented = variant === "segmented";
|
|
538
541
|
const tabListId = id ? `${id}-tablist` : void 0;
|
|
539
542
|
const [_focusedIndex, setFocusedIndex] = (0, import_react4.useState)(-1);
|
|
540
543
|
const tabRefs = (0, import_react4.useRef)([]);
|
|
544
|
+
const containerRef = (0, import_react4.useRef)(null);
|
|
545
|
+
const [indicatorStyle, setIndicatorStyle] = (0, import_react4.useState)({ left: 0, width: 0, initialized: false });
|
|
546
|
+
(0, import_react4.useEffect)(() => {
|
|
547
|
+
if (!isSegmented || !isWeb) return;
|
|
548
|
+
const activeIndex = tabs.findIndex((tab) => tab.id === activeTabId);
|
|
549
|
+
const activeTabEl = tabRefs.current[activeIndex];
|
|
550
|
+
const containerEl = containerRef.current;
|
|
551
|
+
if (activeTabEl && containerEl) {
|
|
552
|
+
const containerRect = containerEl.getBoundingClientRect();
|
|
553
|
+
const tabRect = activeTabEl.getBoundingClientRect();
|
|
554
|
+
setIndicatorStyle({
|
|
555
|
+
left: tabRect.left - containerRect.left,
|
|
556
|
+
width: tabRect.width,
|
|
557
|
+
initialized: true
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}, [activeTabId, tabs, isSegmented]);
|
|
541
561
|
const enabledIndices = tabs.map((tab, index) => !tab.disabled ? index : -1).filter((i) => i !== -1);
|
|
542
562
|
const focusTab = (0, import_react4.useCallback)((index) => {
|
|
543
563
|
const tabElement = tabRefs.current[index];
|
|
@@ -605,6 +625,146 @@ var Tabs = ({
|
|
|
605
625
|
},
|
|
606
626
|
[enabledIndices, focusTab, activateOnFocus, onChange, tabs]
|
|
607
627
|
);
|
|
628
|
+
if (isSegmented) {
|
|
629
|
+
const segmentedStyles = theme.sizing.tabsSegmented(size);
|
|
630
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
631
|
+
Box,
|
|
632
|
+
{
|
|
633
|
+
as: "nav",
|
|
634
|
+
role: "tablist",
|
|
635
|
+
id: tabListId,
|
|
636
|
+
"aria-label": ariaLabel,
|
|
637
|
+
"aria-labelledby": ariaLabelledBy,
|
|
638
|
+
"aria-orientation": "horizontal",
|
|
639
|
+
testID,
|
|
640
|
+
ref: (el) => {
|
|
641
|
+
containerRef.current = el;
|
|
642
|
+
},
|
|
643
|
+
flexDirection: "row",
|
|
644
|
+
alignItems: "center",
|
|
645
|
+
flexShrink: 0,
|
|
646
|
+
position: "relative",
|
|
647
|
+
width: stretched ? "100%" : "fit-content",
|
|
648
|
+
height: segmentedStyles.height,
|
|
649
|
+
backgroundColor: theme.colors.control.segmented.bg,
|
|
650
|
+
borderRadius: segmentedStyles.containerRadius,
|
|
651
|
+
padding: segmentedStyles.containerPadding,
|
|
652
|
+
overflow: "hidden",
|
|
653
|
+
children: [
|
|
654
|
+
isWeb && indicatorStyle.initialized && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
655
|
+
Box,
|
|
656
|
+
{
|
|
657
|
+
position: "absolute",
|
|
658
|
+
zIndex: 0,
|
|
659
|
+
height: `calc(100% - ${segmentedStyles.containerPadding * 2}px)`,
|
|
660
|
+
backgroundColor: theme.colors.control.segmented.bgActive,
|
|
661
|
+
borderRadius: segmentedStyles.itemRadius,
|
|
662
|
+
style: {
|
|
663
|
+
left: indicatorStyle.left,
|
|
664
|
+
width: indicatorStyle.width,
|
|
665
|
+
transition: "left 200ms ease-out, width 200ms ease-out",
|
|
666
|
+
pointerEvents: "none"
|
|
667
|
+
},
|
|
668
|
+
"aria-hidden": true
|
|
669
|
+
}
|
|
670
|
+
),
|
|
671
|
+
tabs.map((tab, index) => {
|
|
672
|
+
const isActive = tab.id === activeTabId;
|
|
673
|
+
const isDisabled = tab.disabled;
|
|
674
|
+
const tabId = id ? `${id}-tab-${tab.id}` : void 0;
|
|
675
|
+
const tabPanelId = id ? `${id}-tabpanel-${tab.id}` : void 0;
|
|
676
|
+
const handlePress = () => {
|
|
677
|
+
if (!isDisabled && onChange) {
|
|
678
|
+
onChange(tab.id);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const handleFocus = () => {
|
|
682
|
+
setFocusedIndex(index);
|
|
683
|
+
};
|
|
684
|
+
const textColor = isDisabled ? theme.colors.control.text.disable : isActive ? theme.colors.control.segmented.textActive : theme.colors.control.text.primary;
|
|
685
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
686
|
+
Box,
|
|
687
|
+
{
|
|
688
|
+
as: "button",
|
|
689
|
+
role: "tab",
|
|
690
|
+
id: tabId,
|
|
691
|
+
"aria-selected": isActive,
|
|
692
|
+
"aria-disabled": isDisabled || void 0,
|
|
693
|
+
"aria-controls": tabPanelId,
|
|
694
|
+
"aria-label": tab["aria-label"],
|
|
695
|
+
tabIndex: isActive ? 0 : -1,
|
|
696
|
+
disabled: isDisabled,
|
|
697
|
+
ref: (el) => {
|
|
698
|
+
tabRefs.current[index] = el;
|
|
699
|
+
},
|
|
700
|
+
onPress: handlePress,
|
|
701
|
+
onFocus: handleFocus,
|
|
702
|
+
onKeyDown: (e) => handleKeyDown(e, index),
|
|
703
|
+
flex: stretched ? 1 : void 0,
|
|
704
|
+
flexShrink: 0,
|
|
705
|
+
position: "relative",
|
|
706
|
+
zIndex: 1,
|
|
707
|
+
height: "100%",
|
|
708
|
+
paddingHorizontal: segmentedStyles.itemPaddingHorizontal,
|
|
709
|
+
paddingVertical: segmentedStyles.itemPaddingVertical,
|
|
710
|
+
flexDirection: "row",
|
|
711
|
+
alignItems: "center",
|
|
712
|
+
justifyContent: "center",
|
|
713
|
+
gap: segmentedStyles.gap,
|
|
714
|
+
backgroundColor: !isWeb && isActive ? theme.colors.control.segmented.bgActive : "transparent",
|
|
715
|
+
borderRadius: segmentedStyles.itemRadius,
|
|
716
|
+
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
717
|
+
hoverStyle: !isDisabled && !isActive ? {
|
|
718
|
+
backgroundColor: theme.colors.control.segmented.bgHover
|
|
719
|
+
} : void 0,
|
|
720
|
+
focusStyle: {
|
|
721
|
+
outlineColor: theme.colors.border.brand,
|
|
722
|
+
outlineWidth: 2,
|
|
723
|
+
outlineOffset: -2
|
|
724
|
+
},
|
|
725
|
+
children: [
|
|
726
|
+
tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
727
|
+
Icon,
|
|
728
|
+
{
|
|
729
|
+
size: segmentedStyles.iconSize,
|
|
730
|
+
color: textColor,
|
|
731
|
+
"aria-hidden": true,
|
|
732
|
+
children: tab.icon
|
|
733
|
+
}
|
|
734
|
+
),
|
|
735
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
736
|
+
Text,
|
|
737
|
+
{
|
|
738
|
+
color: textColor,
|
|
739
|
+
fontSize: segmentedStyles.fontSize,
|
|
740
|
+
fontWeight: "400",
|
|
741
|
+
textAlign: "center",
|
|
742
|
+
whiteSpace: "nowrap",
|
|
743
|
+
overflow: "hidden",
|
|
744
|
+
textOverflow: "ellipsis",
|
|
745
|
+
children: tab.label
|
|
746
|
+
}
|
|
747
|
+
),
|
|
748
|
+
tab.counter !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { marginLeft: 2, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
749
|
+
Text,
|
|
750
|
+
{
|
|
751
|
+
color: textColor,
|
|
752
|
+
fontSize: segmentedStyles.fontSize,
|
|
753
|
+
fontWeight: "400",
|
|
754
|
+
children: tab.counter
|
|
755
|
+
}
|
|
756
|
+
) }),
|
|
757
|
+
tab.badge && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { marginLeft: 2, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_xui_badge.Badge, { size: "sm", children: typeof tab.badge === "string" || typeof tab.badge === "number" ? tab.badge : void 0 }) })
|
|
758
|
+
]
|
|
759
|
+
},
|
|
760
|
+
tab.id
|
|
761
|
+
);
|
|
762
|
+
})
|
|
763
|
+
]
|
|
764
|
+
}
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const lineStyles = theme.sizing.tabs(size);
|
|
608
768
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
609
769
|
Box,
|
|
610
770
|
{
|
|
@@ -618,8 +778,8 @@ var Tabs = ({
|
|
|
618
778
|
flexDirection: "row",
|
|
619
779
|
alignItems: "flex-end",
|
|
620
780
|
justifyContent: alignLeft ? "flex-start" : "center",
|
|
621
|
-
width: "100%",
|
|
622
|
-
height:
|
|
781
|
+
width: stretched ? "100%" : "fit-content",
|
|
782
|
+
height: lineStyles.height,
|
|
623
783
|
borderBottomWidth: 1,
|
|
624
784
|
borderBottomColor: theme.colors.border.secondary,
|
|
625
785
|
borderStyle: "solid",
|
|
@@ -657,12 +817,12 @@ var Tabs = ({
|
|
|
657
817
|
onPress: handlePress,
|
|
658
818
|
onFocus: handleFocus,
|
|
659
819
|
onKeyDown: (e) => handleKeyDown(e, index),
|
|
660
|
-
height:
|
|
661
|
-
paddingHorizontal:
|
|
820
|
+
height: lineStyles.height,
|
|
821
|
+
paddingHorizontal: lineStyles.paddingHorizontal,
|
|
662
822
|
flexDirection: "row",
|
|
663
823
|
alignItems: "center",
|
|
664
824
|
justifyContent: "center",
|
|
665
|
-
gap:
|
|
825
|
+
gap: lineStyles.gap,
|
|
666
826
|
position: "relative",
|
|
667
827
|
borderBottomWidth,
|
|
668
828
|
borderBottomColor,
|
|
@@ -678,12 +838,12 @@ var Tabs = ({
|
|
|
678
838
|
outlineOffset: -2
|
|
679
839
|
},
|
|
680
840
|
children: [
|
|
681
|
-
tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size:
|
|
841
|
+
tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: lineStyles.iconSize, color: textColor, "aria-hidden": true, children: tab.icon }),
|
|
682
842
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
683
843
|
Text,
|
|
684
844
|
{
|
|
685
845
|
color: textColor,
|
|
686
|
-
fontSize:
|
|
846
|
+
fontSize: lineStyles.fontSize,
|
|
687
847
|
fontWeight: isActive ? "600" : "500",
|
|
688
848
|
children: tab.label
|
|
689
849
|
}
|
|
@@ -692,7 +852,7 @@ var Tabs = ({
|
|
|
692
852
|
Text,
|
|
693
853
|
{
|
|
694
854
|
color: theme.colors.content.brand.primary,
|
|
695
|
-
fontSize:
|
|
855
|
+
fontSize: lineStyles.fontSize,
|
|
696
856
|
fontWeight: "500",
|
|
697
857
|
"aria-label": `${tab.counter} items`,
|
|
698
858
|
children: tab.counter
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flowtype definitions for index
|
|
3
|
+
* Generated by Flowgen from a Typescript Definition
|
|
4
|
+
* Flowgen v1.21.0
|
|
5
|
+
* @flow
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
declare interface TabItemType {
|
|
10
|
+
/**
|
|
11
|
+
* Unique identifier for the tab
|
|
12
|
+
*/
|
|
13
|
+
id: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Display label for the tab
|
|
17
|
+
*/
|
|
18
|
+
label: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Optional icon to display before the label
|
|
22
|
+
*/
|
|
23
|
+
icon?: React.ReactNode;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional counter to display after the label
|
|
27
|
+
*/
|
|
28
|
+
counter?: string | number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optional badge to display
|
|
32
|
+
*/
|
|
33
|
+
badge?: boolean | string | number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the tab is disabled
|
|
37
|
+
*/
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Accessible label for screen readers (defaults to label)
|
|
42
|
+
*/
|
|
43
|
+
"aria-label"?: string;
|
|
44
|
+
}
|
|
45
|
+
declare interface TabsProps {
|
|
46
|
+
/**
|
|
47
|
+
* Array of tab items
|
|
48
|
+
*/
|
|
49
|
+
tabs: TabItemType[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* ID of the currently active tab
|
|
53
|
+
*/
|
|
54
|
+
activeTabId?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Callback when a tab is selected
|
|
58
|
+
*/
|
|
59
|
+
onChange?: (id: string) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Size variant of the tabs
|
|
63
|
+
*/
|
|
64
|
+
size?: "xl" | "lg" | "md" | "sm";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Visual variant of the tabs
|
|
68
|
+
*/
|
|
69
|
+
variant?: "line" | "segmented";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether to align tabs to the left (only for line variant)
|
|
73
|
+
*/
|
|
74
|
+
alignLeft?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the component should stretch to fill its container
|
|
78
|
+
*/
|
|
79
|
+
stretched?: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Accessible label for the tab list
|
|
83
|
+
*/
|
|
84
|
+
"aria-label"?: string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* ID of element that labels this tab list
|
|
88
|
+
*/
|
|
89
|
+
"aria-labelledby"?: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether keyboard navigation should automatically activate tabs
|
|
93
|
+
*/
|
|
94
|
+
activateOnFocus?: boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* HTML id attribute
|
|
98
|
+
*/
|
|
99
|
+
id?: string;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Test ID for testing frameworks
|
|
103
|
+
*/
|
|
104
|
+
testID?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Tabs - An accessible tabbed interface component
|
|
108
|
+
*
|
|
109
|
+
* Implements WAI-ARIA Tabs pattern with proper keyboard navigation:
|
|
110
|
+
* - Arrow Left/Right: Navigate between tabs
|
|
111
|
+
* - Home: Jump to first tab
|
|
112
|
+
* - End: Jump to last tab
|
|
113
|
+
* - Enter/Space: Activate focused tab (when activateOnFocus is false)
|
|
114
|
+
*
|
|
115
|
+
* Variants:
|
|
116
|
+
* - "line" (default): Traditional underlined tabs
|
|
117
|
+
* - "segmented": Button-group style segmented control
|
|
118
|
+
*/
|
|
119
|
+
declare var Tabs: React.FC<TabsProps>;
|
|
120
|
+
/**
|
|
121
|
+
* TabPanel - Container for tab content with proper accessibility attributes
|
|
122
|
+
* @example <TabPanel id="tab1" tabsId="my-tabs" hidden={activeTab !== 'tab1'}>
|
|
123
|
+
* <p>Content for tab 1</p>
|
|
124
|
+
* </TabPanel>
|
|
125
|
+
*/
|
|
126
|
+
declare interface TabPanelProps {
|
|
127
|
+
/**
|
|
128
|
+
* ID matching the tab's id
|
|
129
|
+
*/
|
|
130
|
+
id: string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* ID of the parent Tabs component
|
|
134
|
+
*/
|
|
135
|
+
tabsId: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether the panel is hidden
|
|
139
|
+
*/
|
|
140
|
+
hidden?: boolean;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Panel content
|
|
144
|
+
*/
|
|
145
|
+
children: React.ReactNode;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Accessible label for the panel
|
|
149
|
+
*/
|
|
150
|
+
"aria-label"?: string;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Test ID for testing frameworks
|
|
154
|
+
*/
|
|
155
|
+
testID?: string;
|
|
156
|
+
}
|
|
157
|
+
declare var TabPanel: React.FC<TabPanelProps>;
|
|
158
|
+
export type { TabItemType, TabPanelProps, TabsProps };
|
|
159
|
+
declare export { TabPanel, Tabs };
|