@janbox/storefront-ui 4.0.16 → 5.0.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 (49) hide show
  1. package/README.md +7 -12
  2. package/dist/index.js +5 -2
  3. package/dist/lib/button/button.js +3 -3
  4. package/dist/lib/button/helpers.js +3 -3
  5. package/dist/lib/cascader/cascader-floating-content.js +5 -5
  6. package/dist/lib/chip/chip.js +3 -3
  7. package/dist/lib/chip/helpers.d.ts +1 -1
  8. package/dist/lib/chip/helpers.js +4 -4
  9. package/dist/lib/date-picker/date-picker.js +3 -3
  10. package/dist/lib/icon-button/icon-button.js +2 -2
  11. package/dist/lib/input/input.js +3 -3
  12. package/dist/lib/input-mask/input-mask.js +5 -5
  13. package/dist/lib/input-range/input-range.js +8 -8
  14. package/dist/lib/tabs/README.md +92 -25
  15. package/dist/lib/tabs/tab/README.md +134 -0
  16. package/dist/lib/tabs/tab/helpers.d.ts +2 -0
  17. package/dist/lib/tabs/tab/helpers.js +17 -0
  18. package/dist/lib/tabs/tab/tab.js +51 -14
  19. package/dist/lib/tabs/tab/types.d.ts +12 -13
  20. package/dist/lib/tabs/tab-content/README.md +87 -0
  21. package/dist/lib/tabs/tab-content/tab-content.d.ts +1 -1
  22. package/dist/lib/tabs/tab-content/tab-content.js +2 -11
  23. package/dist/lib/tabs/tab-content/types.d.ts +2 -4
  24. package/dist/lib/tabs/tab-indicator/README.md +94 -0
  25. package/dist/lib/tabs/tab-indicator/index.d.ts +0 -1
  26. package/dist/lib/tabs/tab-indicator/tab-indicator.d.ts +2 -1
  27. package/dist/lib/tabs/tab-indicator/tab-indicator.js +71 -40
  28. package/dist/lib/tabs/tab-indicator/types.d.ts +5 -2
  29. package/dist/lib/tabs/tab-list/README.md +164 -0
  30. package/dist/lib/tabs/tab-list/helpers.d.ts +6 -1
  31. package/dist/lib/tabs/tab-list/helpers.js +46 -3
  32. package/dist/lib/tabs/tab-list/tab-list.js +59 -22
  33. package/dist/lib/tabs/tab-list/types.d.ts +11 -1
  34. package/dist/lib/tabs/tab-panel/README.md +86 -0
  35. package/dist/lib/tabs/tab-panel/helpers.d.ts +1 -1
  36. package/dist/lib/tabs/tab-panel/tab-panel.js +3 -5
  37. package/dist/lib/tabs/tab-panel/types.d.ts +1 -2
  38. package/dist/lib/tabs/tabs/helpers.d.ts +1 -5
  39. package/dist/lib/tabs/tabs/helpers.js +1 -20
  40. package/dist/lib/tabs/tabs/tabs.js +18 -22
  41. package/dist/lib/tabs/tabs/types.d.ts +1 -7
  42. package/dist/lib/textarea/textarea.js +3 -3
  43. package/dist/utils/common.d.ts +1 -0
  44. package/dist/utils/common.js +2 -0
  45. package/dist/utils/index.d.ts +1 -1
  46. package/dist/utils/index.js +6 -5
  47. package/dist/utils/stylesheet.d.ts +1 -1
  48. package/dist/utils/stylesheet.js +2 -2
  49. package/package.json +2 -2
@@ -1,25 +1,62 @@
1
- import { jsx } from "react/jsx-runtime";
2
- import { useTabIndexContext } from "./helpers.js";
3
- import { mergeComponentProps } from "../../../utils/index.js";
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useTabIndexContext, useGetTabProps } from "./helpers.js";
3
+ import { createSxStyle } from "../../../utils/stylesheet.js";
4
4
  import { useTabsContext } from "../tabs/helpers.js";
5
- import { Button } from "../../button/button.js";
5
+ import { Primitive } from "../../primitive/primitive.js";
6
+ import { Icon } from "../../icon/icon.js";
6
7
  const Tab = ({ children, ref, ..._props }) => {
7
8
  var _a;
8
- const { focusTab, activeIndex, tabProps: defaultTabProps = {}, preventActiveTabChange } = useTabsContext();
9
- const { color, states, ...rest } = mergeComponentProps(defaultTabProps, _props);
9
+ const { activeIndex, focusTab } = useTabsContext();
10
10
  const tabIndex = useTabIndexContext();
11
+ const getTabProps = useGetTabProps();
12
+ const { startIcon, endIcon, prefix, suffix, states, ...props } = getTabProps(_props);
11
13
  const isTabActive = tabIndex === activeIndex;
12
14
  const onClick = (e) => {
13
15
  var _a2;
14
- e.preventDefault();
15
- if (preventActiveTabChange) {
16
- !isTabActive && focusTab(tabIndex);
17
- } else {
18
- focusTab(tabIndex);
19
- }
20
- (_a2 = rest.onClick) == null ? void 0 : _a2.call(rest, e);
16
+ focusTab(tabIndex);
17
+ (_a2 = props.onClick) == null ? void 0 : _a2.call(props, e);
21
18
  };
22
- return /* @__PURE__ */ jsx(Button, { ...rest, onClick, color: isTabActive ? (_a = states == null ? void 0 : states.active) == null ? void 0 : _a.color : color, ref, children });
19
+ return /* @__PURE__ */ jsxs(
20
+ Primitive,
21
+ {
22
+ ...props,
23
+ type: "button",
24
+ "aria-selected": isTabActive,
25
+ as: "button",
26
+ role: "tab",
27
+ ref,
28
+ sx: {
29
+ ...props.sx,
30
+ "&[aria-selected=true]": createSxStyle((_a = states == null ? void 0 : states.active) == null ? void 0 : _a.sx)
31
+ },
32
+ onClick,
33
+ children: [
34
+ prefix,
35
+ !!startIcon && /* @__PURE__ */ jsx(
36
+ Icon,
37
+ {
38
+ ...startIcon,
39
+ sx: {
40
+ flexShrink: 0,
41
+ ...startIcon.sx
42
+ }
43
+ }
44
+ ),
45
+ children,
46
+ !!endIcon && /* @__PURE__ */ jsx(
47
+ Icon,
48
+ {
49
+ ...endIcon,
50
+ sx: {
51
+ flexShrink: 0,
52
+ ...endIcon.sx
53
+ }
54
+ }
55
+ ),
56
+ suffix
57
+ ]
58
+ }
59
+ );
23
60
  };
24
61
  export {
25
62
  Tab
@@ -1,13 +1,12 @@
1
- import { ButtonSizeVariant, ButtonProps } from '../../button';
2
- import { ShallowMerge, ToResponsiveProps } from '../../../types';
3
- export type TabState = 'active';
4
- export interface TabResponsiveProps {
5
- size?: ButtonSizeVariant;
6
- }
7
- export interface TabExtendedProps extends ToResponsiveProps<TabResponsiveProps> {
8
- states?: Partial<Record<TabState, {
9
- color?: ButtonProps['color'];
10
- variant?: ButtonProps['variant'];
11
- }>>;
12
- }
13
- export type TabProps = ShallowMerge<ButtonProps, TabExtendedProps>;
1
+ import { IconProps } from '../../icon';
2
+ import { PropsWithSx, ShallowMerge } from '../../../types';
3
+ import { JSX } from 'react';
4
+ export type TabProps = PropsWithSx<ShallowMerge<JSX.IntrinsicElements['button'], {
5
+ prefix?: React.ReactNode;
6
+ suffix?: React.ReactNode;
7
+ startIcon?: IconProps;
8
+ endIcon?: IconProps;
9
+ states?: {
10
+ active?: PropsWithSx;
11
+ };
12
+ }>>;
@@ -0,0 +1,87 @@
1
+ # TabContent
2
+
3
+ Container cho các [TabPanel](../tab-panel/README.md). Đọc `activeIndex` từ context của [Tabs](../README.md) và chỉ render đúng một child tại index đó.
4
+
5
+ ## Import
6
+
7
+ ```tsx
8
+ import { TabContent } from '@janbox/storefront-ui';
9
+ ```
10
+
11
+ ## Props
12
+
13
+ | Prop | Type | Default | Description |
14
+ | ------ | ------ | ------- | ----------- |
15
+ | `children` * | `React.ReactNode` | - | Danh sách `TabPanel`, thứ tự phải khớp với thứ tự các `Tab` trong `TabList`. |
16
+ | `className` | `string` | - | Class cho container `div`. |
17
+
18
+ Kiểu prop là `JSX.IntrinsicElements['div']` — kế thừa toàn bộ div attributes. Component **không** nhận `sx`; dùng `className` hoặc `style` nếu cần style container.
19
+
20
+ > ⚠️ `TabContent` **không** forward `ref`. `ref` trong props sẽ bị truyền vào `Primitive` như prop thường và không trỏ tới DOM node.
21
+
22
+ *Required props
23
+
24
+ ## Cách hoạt động
25
+
26
+ - Children được `flattenChildren` trước khi chọn, nên bọc panel trong Fragment vẫn giữ index đúng.
27
+ - Chỉ child tại `activeIndex` được render — các panel khác **bị unmount**, state nội bộ và scroll position của chúng không được giữ lại. Nếu cần giữ state, lift state lên trên `Tabs` hoặc tự render tất cả panel và điều khiển `display` bằng CSS.
28
+ - Nếu `activeIndex` không khớp child nào (vd: `defaultActiveIndex` vượt số panel), `TabContent` render rỗng.
29
+
30
+ ## Examples
31
+
32
+ ### Cơ bản
33
+
34
+ ```tsx
35
+ <Tabs defaultActiveIndex={0}>
36
+ <TabList>
37
+ <Tab>Tab 1</Tab>
38
+ <Tab>Tab 2</Tab>
39
+ </TabList>
40
+ <TabContent>
41
+ <TabPanel>Content for Tab 1</TabPanel>
42
+ <TabPanel>Content for Tab 2</TabPanel>
43
+ </TabContent>
44
+ </Tabs>
45
+ ```
46
+
47
+ ### Style container
48
+
49
+ ```tsx
50
+ <TabContent style={{ paddingTop: 16, minHeight: 200 }}>
51
+ <TabPanel>Panel 1</TabPanel>
52
+ <TabPanel>Panel 2</TabPanel>
53
+ </TabContent>
54
+ ```
55
+
56
+ ### Số panel ít hơn số tab
57
+
58
+ ```tsx
59
+ {/* Tab 3 sẽ hiển thị content rỗng */}
60
+ <Tabs>
61
+ <TabList>
62
+ <Tab>Tab 1</Tab>
63
+ <Tab>Tab 2</Tab>
64
+ <Tab>Tab 3</Tab>
65
+ </TabList>
66
+ <TabContent>
67
+ <TabPanel>Panel 1</TabPanel>
68
+ <TabPanel>Panel 2</TabPanel>
69
+ </TabContent>
70
+ </Tabs>
71
+ ```
72
+
73
+ ## Related Components
74
+
75
+ - [Tabs](../README.md)
76
+ - [TabPanel](../tab-panel/README.md)
77
+ - [Primitive](../../primitive/README.md)
78
+
79
+ ## Dependencies
80
+
81
+ - @emotion/react
82
+
83
+ ## See Also
84
+
85
+ - [Component Source](./tab-content.tsx)
86
+ - [Storybook Stories](../tabs.stories.tsx)
87
+ - [Main README](../../../README.md)
@@ -1,2 +1,2 @@
1
1
  import { TabContentProps } from './types';
2
- export declare const TabContent: ({ children, ...rest }: TabContentProps) => import("react/jsx-runtime").JSX.Element;
2
+ export declare const TabContent: ({ children, ...props }: TabContentProps) => import("react/jsx-runtime").JSX.Element;
@@ -2,19 +2,10 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { flattenChildren } from "../../../utils/index.js";
3
3
  import { useTabsContext } from "../tabs/helpers.js";
4
4
  import { Primitive } from "../../primitive/primitive.js";
5
- const TabContent = ({ children, ...rest }) => {
5
+ const TabContent = ({ children, ...props }) => {
6
6
  const { activeIndex } = useTabsContext();
7
7
  const flattenedChildren = flattenChildren(children);
8
- return /* @__PURE__ */ jsx(
9
- Primitive,
10
- {
11
- sx: {
12
- display: "contents"
13
- },
14
- ...rest,
15
- children: flattenedChildren.find((_, index) => index === activeIndex)
16
- }
17
- );
8
+ return /* @__PURE__ */ jsx(Primitive, { ...props, children: flattenedChildren.find((_, index) => index === activeIndex) });
18
9
  };
19
10
  export {
20
11
  TabContent
@@ -1,4 +1,2 @@
1
- import { PropsWithChildren } from 'react';
2
- export type TabContentProps = PropsWithChildren<{
3
- ref?: React.Ref<HTMLDivElement>;
4
- }>;
1
+ import { JSX } from 'react';
2
+ export type TabContentProps = JSX.IntrinsicElements['div'];
@@ -0,0 +1,94 @@
1
+ # TabIndicator
2
+
3
+ Thanh gạch chân chạy theo tab đang active. Được [TabList](../tab-list/README.md) render tự động và animate bằng `motion/react` khi `activeIndex` đổi.
4
+
5
+ > ℹ️ Đây là component nội bộ — **không** được export từ `@janbox/storefront-ui`. Cấu hình nó qua prop `indicator` của `TabList`.
6
+
7
+ ## Usage
8
+
9
+ ```tsx
10
+ <TabList indicator={{ size: 3, sx: { backgroundColor: getColorVar('green.600') } }}>
11
+ <Tab>Tab 1</Tab>
12
+ <Tab>Tab 2</Tab>
13
+ </TabList>
14
+ ```
15
+
16
+ Truyền `indicator={false}` để không render indicator.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ | ------ | ------ | ------- | ----------- |
22
+ | `size` | `number` | `2` | Độ dày (px) của indicator. Được set qua CSS variable `--indicator-size` nên responsive theo breakpoint hoạt động đúng. |
23
+ | `sm` | `{ size?: number }` | - | Override `size` từ breakpoint sm (≥768px). |
24
+ | `md` | `{ size?: number }` | - | Override `size` từ breakpoint md (≥1280px). |
25
+ | `lg` | `{ size?: number }` | - | Override `size` từ breakpoint lg (≥1680px). |
26
+ | `sx` | `ToResponsiveProps<StyledCSS>` | `{ backgroundColor: getColorVar('primary.600') }` | Emotion CSS overrides. Apply sau `size` nên `height` trong `sx` sẽ thắng `size`. |
27
+
28
+ ## Cách hoạt động
29
+
30
+ - Indicator định vị `position: absolute` bên trong element chứa các `Tab` của `TabList` (element đó luôn có `position: relative`).
31
+ - Tab đang active được tìm bằng `parentElement.children.item(activeIndex)` — tức là **theo thứ tự DOM**, không qua index từ context. Vì vậy đừng chèn element không phải `Tab` vào giữa các `Tab`, nếu không indicator sẽ trỏ sai.
32
+ - Vị trí (`left`, `top`, `width`) được tính từ `getBoundingClientRect()` của tab active so với container, có bù `scrollLeft`/`scrollTop` để đúng khi tab bar scroll ngang.
33
+ - Một `MutationObserver` theo dõi `childList` của `TabList` để cập nhật khi số lượng `Tab` thay đổi (thêm/bớt tab động).
34
+ - Một `ResizeObserver` observe cả tab active **và** container: resize container không đổi kích thước từng tab nhưng có thể làm tab xuống dòng khác khi `overflowBehavior="wrap"`, nên phải theo dõi cả hai để cập nhật vị trí dòng.
35
+ - `top` được tính là mép dưới của tab trừ đi chiều cao indicator, nên indicator luôn nằm sát đáy tab active — kể cả khi tab wrap sang dòng thứ hai.
36
+
37
+ ## Examples
38
+
39
+ ### Indicator dày hơn, bo góc
40
+
41
+ ```tsx
42
+ <TabList indicator={{ size: 4, sx: { backgroundColor: getColorVar('primary.600'), borderRadius: 2 } }}>
43
+ <Tab>Tab 1</Tab>
44
+ <Tab>Tab 2</Tab>
45
+ </TabList>
46
+ ```
47
+
48
+ ### Responsive size
49
+
50
+ ```tsx
51
+ <TabList indicator={{ size: 2, md: { size: 3 } }}>
52
+ <Tab>Tab 1</Tab>
53
+ <Tab>Tab 2</Tab>
54
+ </TabList>
55
+ ```
56
+
57
+ ### Không dùng indicator (dùng pill style cho tab active)
58
+
59
+ ```tsx
60
+ <TabList
61
+ indicator={false}
62
+ tabConfig={{
63
+ sx: { borderRadius: 999 },
64
+ states: {
65
+ active: {
66
+ sx: {
67
+ backgroundColor: getColorVar('primary.600'),
68
+ color: getColorVar('primary.contrast'),
69
+ },
70
+ },
71
+ },
72
+ }}
73
+ >
74
+ <Tab>All</Tab>
75
+ <Tab>Active</Tab>
76
+ </TabList>
77
+ ```
78
+
79
+ ## Related Components
80
+
81
+ - [Tabs](../README.md)
82
+ - [TabList](../tab-list/README.md)
83
+ - [Tab](../tab/README.md)
84
+
85
+ ## Dependencies
86
+
87
+ - motion/react
88
+ - @emotion/react
89
+
90
+ ## See Also
91
+
92
+ - [Component Source](./tab-indicator.tsx)
93
+ - [Storybook Stories](../tabs.stories.tsx)
94
+ - [Main README](../../../README.md)
@@ -1,2 +1 @@
1
1
  export * from './tab-indicator';
2
- export * from './types';
@@ -1 +1,2 @@
1
- export declare const TabIndicator: () => import("react/jsx-runtime").JSX.Element;
1
+ import { TabIndicatorProps } from './types';
2
+ export declare const TabIndicator: ({ size, sm, md, lg, ...props }: TabIndicatorProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,13 +1,18 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useRef, useState, useEffect } from "react";
2
+ import { useRef, useState, useEffect, useCallback } from "react";
3
3
  import { motion } from "motion/react";
4
4
  import { Stylesheet } from "../../stylesheet/stylesheet.js";
5
+ import "lodash-es";
6
+ import "../../../theme/palette.js";
7
+ import { isNumber } from "../../../utils/common.js";
5
8
  import { useTabsContext } from "../tabs/helpers.js";
6
- const TabIndicator = () => {
7
- const { activeIndex, tabIndicatorProps } = useTabsContext();
9
+ const INDICATOR_SIZE_VAR_NAME = "--indicator-size";
10
+ const TabIndicator = ({ size, sm, md, lg, ...props }) => {
11
+ const { activeIndex } = useTabsContext();
8
12
  const indicatorEl = useRef(null);
9
13
  const [indicatorRect, setIndicatorRect] = useState({
10
14
  left: 0,
15
+ top: 0,
11
16
  width: 0
12
17
  });
13
18
  const [activeTabElement, setActiveTabElement] = useState();
@@ -31,50 +36,76 @@ const TabIndicator = () => {
31
36
  mutationObserver.disconnect();
32
37
  };
33
38
  }, [activeIndex]);
34
- useEffect(() => {
35
- const resizeObserver = new ResizeObserver((entries) => {
36
- var _a;
37
- const target = (_a = entries.at(0)) == null ? void 0 : _a.target;
38
- const parentElement = target == null ? void 0 : target.parentElement;
39
- if (!target || !parentElement) {
40
- return;
41
- }
42
- const targetRect = target.getBoundingClientRect();
43
- const parentRect = parentElement.getBoundingClientRect();
44
- setIndicatorRect({
45
- left: targetRect.left - parentRect.left + parentElement.scrollLeft,
46
- width: targetRect.width
47
- });
39
+ const syncIndicatorRect = useCallback(() => {
40
+ const indicator = indicatorEl.current;
41
+ const parentElement = indicator == null ? void 0 : indicator.parentElement;
42
+ if (!indicator || !parentElement || !activeTabElement) {
43
+ return;
44
+ }
45
+ const targetRect = activeTabElement.getBoundingClientRect();
46
+ const parentRect = parentElement.getBoundingClientRect();
47
+ setIndicatorRect({
48
+ left: targetRect.left - parentRect.left + parentElement.scrollLeft,
49
+ top: targetRect.bottom - parentRect.top + parentElement.scrollTop - indicator.offsetHeight,
50
+ width: targetRect.width
48
51
  });
52
+ }, [activeTabElement]);
53
+ useEffect(() => {
54
+ var _a;
55
+ const resizeObserver = new ResizeObserver(syncIndicatorRect);
56
+ const parentElement = (_a = indicatorEl.current) == null ? void 0 : _a.parentElement;
49
57
  if (activeTabElement) {
50
58
  resizeObserver.observe(activeTabElement);
51
59
  }
60
+ if (parentElement) {
61
+ resizeObserver.observe(parentElement);
62
+ }
52
63
  return () => {
53
64
  resizeObserver.disconnect();
54
65
  };
55
- }, [activeTabElement]);
56
- return /* @__PURE__ */ jsx(Stylesheet, { children: ({ css }) => /* @__PURE__ */ jsx(
57
- motion.div,
58
- {
59
- initial: {
60
- transform: "translateX(0)",
61
- width: 0
62
- },
63
- animate: {
64
- width: indicatorRect.width,
65
- transform: `translateX(${indicatorRect.left}px)`
66
- },
67
- ref: indicatorEl,
68
- className: css({
69
- position: "absolute",
70
- bottom: 0,
71
- left: 0,
72
- backgroundColor: "currentcolor",
73
- borderRadius: 8,
74
- ...tabIndicatorProps == null ? void 0 : tabIndicatorProps.sx
75
- })
76
- }
77
- ) });
66
+ }, [activeTabElement, syncIndicatorRect]);
67
+ return /* @__PURE__ */ jsx(Stylesheet, { children: ({ css }) => {
68
+ var _a, _b, _c;
69
+ return /* @__PURE__ */ jsx(
70
+ motion.div,
71
+ {
72
+ initial: {
73
+ transform: "translate(0, 0)",
74
+ width: 0
75
+ },
76
+ animate: {
77
+ width: indicatorRect.width,
78
+ transform: `translate(${indicatorRect.left}px, ${indicatorRect.top}px)`
79
+ },
80
+ ref: indicatorEl,
81
+ className: css({
82
+ height: `var(${INDICATOR_SIZE_VAR_NAME})`,
83
+ position: "absolute",
84
+ top: 0,
85
+ left: 0,
86
+ ...createStyle({ size }),
87
+ ...props.sx,
88
+ sm: {
89
+ ...createStyle(sm),
90
+ ...(_a = props.sx) == null ? void 0 : _a.sm
91
+ },
92
+ md: {
93
+ ...createStyle(md),
94
+ ...(_b = props.sx) == null ? void 0 : _b.md
95
+ },
96
+ lg: {
97
+ ...createStyle(lg),
98
+ ...(_c = props.sx) == null ? void 0 : _c.lg
99
+ }
100
+ })
101
+ }
102
+ );
103
+ } });
104
+ };
105
+ const createStyle = ({ size } = {}) => {
106
+ return isNumber(size) ? {
107
+ [INDICATOR_SIZE_VAR_NAME]: `${size}px`
108
+ } : {};
78
109
  };
79
110
  export {
80
111
  TabIndicator
@@ -1,2 +1,5 @@
1
- import { PropsWithSx } from '../../../types';
2
- export type TabIndicatorProps = PropsWithSx;
1
+ import { PropsWithSx, ToResponsiveProps } from '../../../types';
2
+ export type TabIndicatorResponsiveProps = {
3
+ size?: number;
4
+ };
5
+ export type TabIndicatorProps = PropsWithSx<ToResponsiveProps<TabIndicatorResponsiveProps>>;
@@ -0,0 +1,164 @@
1
+ # TabList
2
+
3
+ Container ngang chứa các `Tab`, đồng thời là nơi cấu hình style và hành vi overflow của tab bar. `TabList` gán index cho từng `Tab` con qua `TabIndexContext`, phân phối `tabConfig` xuống mọi `Tab` qua `TabListContext`, và render `TabIndicator` ở cuối.
4
+
5
+ Phải được dùng bên trong [Tabs](../README.md).
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import { TabList } from '@janbox/storefront-ui';
11
+ ```
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Description |
16
+ | ------ | ------ | ------- | ----------- |
17
+ | `overflowBehavior` | `'scroll' \| 'wrap'` | `'scroll'` | `'scroll'` cho phép scroll ngang và ẩn scrollbar; `'wrap'` cho tab xuống dòng (`flex-wrap: wrap`). Apply trên inner element. |
18
+ | `align` | `'start' \| 'center' \| 'end'` | `'start'` | Căn tab bar trong outer element qua `justify-content`. Khi tab bar hẹp hơn container, `'center'` sẽ đưa nó vào giữa. |
19
+ | `tabConfig` | `Pick<TabProps, 'sx' \| 'states'>` | Xem [Default tabConfig](#default-tabconfig) | `sx` áp dụng cho mọi `Tab` con. `states.active.sx` override cho tab đang active (match qua `[aria-selected=true]`). |
20
+ | `indicator` | `false \| TabIndicatorProps` | `{ size: 2, sx: { backgroundColor: getColorVar('primary.600') } }` | Cấu hình [TabIndicator](../tab-indicator/README.md). Truyền `false` để không render indicator. |
21
+ | `sx` | `ToResponsiveProps<StyledCSS>` | - | Responsive Emotion CSS overrides cho **inner** element. Merge sau style mặc định nên override được `display`, `width`, v.v. |
22
+ | `as` | `HTMLElementType` | `'div'` | Tag HTML của inner element. |
23
+ | `children` * | `React.ReactNode` | - | Các `Tab`. Fragment được flatten nên index vẫn đúng khi bọc trong `<>…</>`. |
24
+ | `ref` | `React.Ref<HTMLDivElement>` | - | Forwarded ref tới inner element (element chứa các `Tab`). |
25
+
26
+ Kế thừa toàn bộ `JSX.IntrinsicElements['div']` attributes — tất cả được truyền vào **inner** element, không phải outer.
27
+
28
+ *Required props
29
+
30
+ ### Cấu trúc DOM
31
+
32
+ `TabList` render hai element lồng nhau:
33
+
34
+ ```html
35
+ <!-- outer: role=tablist, nhận `align` -->
36
+ <div role="tablist" style="display: flex; justify-content: …">
37
+ <!-- inner: nhận `sx`, `ref`, `as` và mọi div attribute khác -->
38
+ <div style="display: flex; width: fit-content; max-width: 100%; position: relative">
39
+ <button aria-selected="true">…</button>
40
+ <!-- các Tab -->
41
+ <div><!-- TabIndicator --></div>
42
+ </div>
43
+ </div>
44
+ ```
45
+
46
+ Inner element có `width: fit-content` để tab bar chỉ rộng bằng tổng các tab — nhờ đó `align` trên outer mới có tác dụng. Nó cũng có `position: relative` vì `TabIndicator` định vị absolute theo element này.
47
+
48
+ ### Default tabConfig
49
+
50
+ Định nghĩa trong `tab-list/helpers.ts`, dựa trên `getSizeVariantState('md')`:
51
+
52
+ ```ts
53
+ {
54
+ sx: {
55
+ columnGap: 8,
56
+ paddingLeft: 12,
57
+ paddingRight: 12,
58
+ color: getColorVar('neutral.600'),
59
+ height: 40,
60
+ borderTopLeftRadius: 8,
61
+ borderTopRightRadius: 8,
62
+ ':hover': { backgroundColor: getColorVar('neutral.100') },
63
+ },
64
+ states: {
65
+ active: {
66
+ sx: {
67
+ color: getColorVar('primary.600'),
68
+ ':hover': { backgroundColor: getColorVar('primary.100') },
69
+ },
70
+ },
71
+ },
72
+ }
73
+ ```
74
+
75
+ `tabConfig` bạn truyền vào được **deep merge** với default trên, nên chỉ cần khai báo phần muốn đổi. Muốn xoá hoàn toàn một style mặc định, override nó bằng giá trị khác (vd: `backgroundColor: 'transparent'`) chứ không xoá key được.
76
+
77
+ ## Cách hoạt động
78
+
79
+ - Mỗi child được bọc trong `TabIndexContext.Provider value={index}` — index dựa trên thứ tự sau khi `flattenChildren`.
80
+ - `TabIndicator` được render như child **cuối cùng** của inner element. Vì `TabIndicator` tìm tab đang active bằng `parentElement.children.item(activeIndex)`, đừng chèn element không phải `Tab` vào giữa các `Tab` — index sẽ lệch.
81
+ - `role="tablist"` được set trên outer element. Nếu bạn truyền `role` qua props, nó vào inner element và **không** override được `role="tablist"` của outer.
82
+
83
+ > ⚠️ Cả hai element đang có `border` debug (`red 1px solid` ở outer, `blue 1px solid` ở inner) hardcode trong `tab-list.tsx`. Đây là style tạm khi phát triển, cần xoá trước khi release.
84
+
85
+ ## Examples
86
+
87
+ ### Căn giữa và cho tab xuống dòng
88
+
89
+ ```tsx
90
+ <Tabs sx={{ width: 300 }}>
91
+ <TabList align="center" overflowBehavior="wrap">
92
+ <Tab>Tab 1</Tab>
93
+ <Tab>Tab 2</Tab>
94
+ <Tab>Tab 3</Tab>
95
+ <Tab>Tab 4</Tab>
96
+ </TabList>
97
+ <TabContent>{/* … */}</TabContent>
98
+ </Tabs>
99
+ ```
100
+
101
+ ### Custom style cho toàn bộ Tab
102
+
103
+ ```tsx
104
+ <TabList
105
+ tabConfig={{
106
+ sx: {
107
+ height: 36,
108
+ borderRadius: 999,
109
+ ':hover': { backgroundColor: getColorVar('neutral.100') },
110
+ },
111
+ states: {
112
+ active: {
113
+ sx: {
114
+ color: getColorVar('primary.contrast'),
115
+ backgroundColor: getColorVar('primary.600'),
116
+ ':hover': { backgroundColor: getColorVar('primary.700') },
117
+ },
118
+ },
119
+ },
120
+ }}
121
+ indicator={false}
122
+ >
123
+ <Tab>All</Tab>
124
+ <Tab>Active</Tab>
125
+ <Tab>Archived</Tab>
126
+ </TabList>
127
+ ```
128
+
129
+ ### Indicator dày hơn, màu khác
130
+
131
+ ```tsx
132
+ <TabList indicator={{ size: 3, sx: { backgroundColor: getColorVar('green.600'), borderRadius: 2 } }}>
133
+ <Tab>Tab 1</Tab>
134
+ <Tab>Tab 2</Tab>
135
+ </TabList>
136
+ ```
137
+
138
+ ### Tab bar full-width, mỗi tab chia đều
139
+
140
+ `sx` apply lên inner element nên `width: '100%'` sẽ override `width: fit-content` mặc định:
141
+
142
+ ```tsx
143
+ <TabList sx={{ width: '100%' }} tabConfig={{ sx: { flex: 1, flexShrink: 1 } }}>
144
+ <Tab>Left</Tab>
145
+ <Tab>Right</Tab>
146
+ </TabList>
147
+ ```
148
+
149
+ ## Related Components
150
+
151
+ - [Tabs](../README.md)
152
+ - [Tab](../tab/README.md)
153
+ - [TabIndicator](../tab-indicator/README.md)
154
+ - [Primitive](../../primitive/README.md)
155
+
156
+ ## Dependencies
157
+
158
+ - @emotion/react
159
+
160
+ ## See Also
161
+
162
+ - [Component Source](./tab-list.tsx)
163
+ - [Storybook Stories](../tabs.stories.tsx)
164
+ - [Main README](../../../README.md)
@@ -1,2 +1,7 @@
1
- import { TabListProps } from './types';
1
+ import { TabConfig, TabListProps } from './types';
2
2
  export declare const getTabListProps: (extended: TabListProps) => TabListProps;
3
+ export type TabListContextValue = {
4
+ tabConfig?: TabConfig;
5
+ };
6
+ export declare const TabListContext: import('react').Context<TabListContextValue>;
7
+ export declare const useTabListContext: () => TabListContextValue;