@janbox/storefront-ui 4.0.17 → 5.0.1

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 (59) hide show
  1. package/README.md +7 -12
  2. package/dist/components/scroll-view.js +1 -1
  3. package/dist/index.js +5 -2
  4. package/dist/lib/accordion/accordion-content/accordion-content.js +1 -1
  5. package/dist/lib/button/button.js +3 -3
  6. package/dist/lib/button/helpers.js +3 -3
  7. package/dist/lib/cascader/cascader-floating-content.js +5 -5
  8. package/dist/lib/chip/chip.js +3 -3
  9. package/dist/lib/chip/helpers.d.ts +1 -1
  10. package/dist/lib/chip/helpers.js +4 -4
  11. package/dist/lib/count-up/count-up.js +1 -1
  12. package/dist/lib/date-picker/date-picker.js +3 -3
  13. package/dist/lib/drawer/drawer/drawer.js +1 -1
  14. package/dist/lib/floating/floating-content/floating-content.js +1 -1
  15. package/dist/lib/icon-button/icon-button.js +2 -2
  16. package/dist/lib/input/input.js +3 -3
  17. package/dist/lib/input-mask/input-mask.js +5 -5
  18. package/dist/lib/input-range/input-range.js +8 -8
  19. package/dist/lib/lightbox/lightbox.js +1 -1
  20. package/dist/lib/loading/image-skeleton/image-skeleton.js +1 -1
  21. package/dist/lib/notifications/notifications.js +1 -1
  22. package/dist/lib/star-rating/star-rating.js +1 -1
  23. package/dist/lib/swiper/swiper-dots-paging/swiper-dots-paging.js +1 -1
  24. package/dist/lib/tabs/README.md +92 -25
  25. package/dist/lib/tabs/tab/README.md +134 -0
  26. package/dist/lib/tabs/tab/helpers.d.ts +2 -0
  27. package/dist/lib/tabs/tab/helpers.js +17 -0
  28. package/dist/lib/tabs/tab/tab.js +51 -14
  29. package/dist/lib/tabs/tab/types.d.ts +12 -13
  30. package/dist/lib/tabs/tab-content/README.md +90 -0
  31. package/dist/lib/tabs/tab-content/tab-content.d.ts +1 -1
  32. package/dist/lib/tabs/tab-content/tab-content.js +2 -11
  33. package/dist/lib/tabs/tab-content/types.d.ts +3 -4
  34. package/dist/lib/tabs/tab-indicator/README.md +94 -0
  35. package/dist/lib/tabs/tab-indicator/index.d.ts +0 -1
  36. package/dist/lib/tabs/tab-indicator/tab-indicator.d.ts +2 -1
  37. package/dist/lib/tabs/tab-indicator/tab-indicator.js +72 -41
  38. package/dist/lib/tabs/tab-indicator/types.d.ts +5 -2
  39. package/dist/lib/tabs/tab-list/README.md +164 -0
  40. package/dist/lib/tabs/tab-list/helpers.d.ts +6 -1
  41. package/dist/lib/tabs/tab-list/helpers.js +46 -3
  42. package/dist/lib/tabs/tab-list/tab-list.js +59 -22
  43. package/dist/lib/tabs/tab-list/types.d.ts +11 -1
  44. package/dist/lib/tabs/tab-panel/README.md +86 -0
  45. package/dist/lib/tabs/tab-panel/helpers.d.ts +1 -1
  46. package/dist/lib/tabs/tab-panel/tab-panel.js +3 -5
  47. package/dist/lib/tabs/tab-panel/types.d.ts +1 -2
  48. package/dist/lib/tabs/tabs/helpers.d.ts +1 -5
  49. package/dist/lib/tabs/tabs/helpers.js +1 -20
  50. package/dist/lib/tabs/tabs/tabs.js +18 -22
  51. package/dist/lib/tabs/tabs/types.d.ts +1 -7
  52. package/dist/lib/textarea/textarea.js +3 -3
  53. package/dist/utils/common.d.ts +1 -0
  54. package/dist/utils/common.js +2 -0
  55. package/dist/utils/index.d.ts +1 -1
  56. package/dist/utils/index.js +6 -5
  57. package/dist/utils/stylesheet.d.ts +1 -1
  58. package/dist/utils/stylesheet.js +2 -2
  59. package/package.json +1 -1
@@ -0,0 +1,134 @@
1
+ # Tab
2
+
3
+ Nút của một tab trong [TabList](../tab-list/README.md). Render `<button type="button">` với `aria-selected` phản ánh trạng thái active, cùng slot cho icon/nội dung phụ ở hai đầu.
4
+
5
+ `Tab` phải nằm bên trong `TabList` (để lấy index qua `TabIndexContext`) và [Tabs](../README.md) (để lấy `activeIndex` + `focusTab` qua `TabsContext`).
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import { Tab } from '@janbox/storefront-ui';
11
+ ```
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Description |
16
+ | ------ | ------ | ------- | ----------- |
17
+ | `startIcon` | `IconProps` | - | Icon render trước `children`. Tự động thêm `flexShrink: 0`. |
18
+ | `endIcon` | `IconProps` | - | Icon render sau `children`. Tự động thêm `flexShrink: 0`. |
19
+ | `prefix` | `React.ReactNode` | - | Node render ở đầu, **trước** `startIcon`. Dùng cho badge, dot, counter… |
20
+ | `suffix` | `React.ReactNode` | - | Node render ở cuối, **sau** `endIcon`. |
21
+ | `states` | `{ active?: PropsWithSx }` | Xem [TabList](../tab-list/README.md#default-tabconfig) | `states.active.sx` được apply qua selector `&[aria-selected=true]`. |
22
+ | `onClick` | `React.MouseEventHandler<HTMLButtonElement>` | - | Được gọi **sau** khi tab đã chuyển. Xem [Click handling](#click-handling). |
23
+ | `sx` | `ToResponsiveProps<StyledCSS>` | `{ flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }` | Responsive Emotion CSS overrides. |
24
+ | `children` | `React.ReactNode` | - | Nội dung label của tab. |
25
+ | `ref` | `React.Ref<HTMLButtonElement>` | - | Forwarded ref tới `<button>`. |
26
+
27
+ Kế thừa toàn bộ `JSX.IntrinsicElements['button']` attributes.
28
+
29
+ ### Click handling
30
+
31
+ `Tab` tự gắn `onClick` gọi `focusTab(tabIndex)` từ `TabsContext`, rồi mới gọi `onClick` của bạn. Nghĩa là:
32
+
33
+ - `onClick` của bạn luôn chạy, và chạy **sau** khi state đã cập nhật.
34
+ - `e.preventDefault()` hay `e.stopPropagation()` trong `onClick` **không** ngăn được tab chuyển, vì việc chuyển đã xảy ra trước đó.
35
+ - Nếu `preventActiveTabChange` (trên `Tabs`) là `true` — mặc định — click vào tab đang active không re-fire `onTabChange`, nhưng `onClick` của bạn vẫn được gọi.
36
+
37
+ ### Thứ tự merge props
38
+
39
+ `Tab` merge theo thứ tự tăng dần độ ưu tiên:
40
+
41
+ 1. `defaultProps` của `Tab` (layout flex cơ bản)
42
+ 2. `tabConfig` từ `TabList` (qua `TabListContext`)
43
+ 3. Props truyền trực tiếp vào `Tab`
44
+
45
+ Nên prop trên từng `Tab` luôn thắng `tabConfig`, và cả hai đều là deep merge — chỉ khai báo phần muốn đổi.
46
+
47
+ ### Attributes được render
48
+
49
+ | Attribute | Giá trị |
50
+ | ------ | ------ |
51
+ | `aria-selected` | `true` khi index của tab bằng `activeIndex` |
52
+ | `type` | Luôn `"button"` để không submit form khi đặt trong `<form>` |
53
+
54
+ ## Examples
55
+
56
+ ### Tab cơ bản
57
+
58
+ ```tsx
59
+ <TabList>
60
+ <Tab>Overview</Tab>
61
+ <Tab>Details</Tab>
62
+ </TabList>
63
+ ```
64
+
65
+ ### Tab với icon
66
+
67
+ ```tsx
68
+ import HomeIcon from '@/assets/svg/home.svg?raw';
69
+ import ChevronIcon from '@/assets/svg/chevron-down.svg?raw';
70
+
71
+ <TabList>
72
+ <Tab startIcon={{ source: HomeIcon, size: 20 }}>Home</Tab>
73
+ <Tab endIcon={{ source: ChevronIcon, size: 20 }}>More</Tab>
74
+ </TabList>
75
+ ```
76
+
77
+ ### Tab với counter ở suffix
78
+
79
+ ```tsx
80
+ <TabList>
81
+ <Tab suffix={<span>(3)</span>}>Notifications</Tab>
82
+ <Tab>Settings</Tab>
83
+ </TabList>
84
+ ```
85
+
86
+ ### Override style cho một Tab riêng lẻ
87
+
88
+ ```tsx
89
+ <TabList>
90
+ <Tab>Normal</Tab>
91
+ <Tab
92
+ sx={{ color: getColorVar('red.600') }}
93
+ states={{ active: { sx: { color: getColorVar('red.700') } } }}
94
+ >
95
+ Danger
96
+ </Tab>
97
+ </TabList>
98
+ ```
99
+
100
+ ### Disabled tab
101
+
102
+ ```tsx
103
+ <TabList>
104
+ <Tab>Available</Tab>
105
+ <Tab disabled sx={{ opacity: 0.5, cursor: 'not-allowed' }}>
106
+ Coming soon
107
+ </Tab>
108
+ </TabList>
109
+ ```
110
+
111
+ > ⚠️ `disabled` ngăn tab chuyển vì `<button disabled>` không phát click event — `Tab` không kiểm tra `disabled` trước khi gọi `focusTab`. Style cho trạng thái disabled phải tự khai báo, không có style mặc định.
112
+
113
+ ## Accessibility
114
+
115
+ - `aria-selected` được set tự động theo trạng thái active.
116
+ - `Tab` render `<button>` nên đã focusable và activate được bằng `Enter`/`Space`.
117
+ - `TabList` set `role="tablist"` trên wrapper, nhưng `Tab` **không** tự set `role="tab"` hay `aria-controls`, và không xử lý điều hướng bằng arrow key. Nếu cần đầy đủ ARIA tabs pattern, truyền `role="tab"` và `aria-controls` thủ công cùng `id` tương ứng trên `TabPanel`.
118
+
119
+ ## Related Components
120
+
121
+ - [Tabs](../README.md)
122
+ - [TabList](../tab-list/README.md)
123
+ - [Icon](../../icon/README.md)
124
+ - [Primitive](../../primitive/README.md)
125
+
126
+ ## Dependencies
127
+
128
+ - @emotion/react
129
+
130
+ ## See Also
131
+
132
+ - [Component Source](./tab.tsx)
133
+ - [Storybook Stories](../tabs.stories.tsx)
134
+ - [Main README](../../../README.md)
@@ -1,2 +1,4 @@
1
+ import { TabProps } from './types';
1
2
  export declare const TabIndexContext: import('react').Context<number>;
2
3
  export declare const useTabIndexContext: () => number;
4
+ export declare const useGetTabProps: () => (extended: TabProps) => TabProps;
@@ -1,9 +1,26 @@
1
+ import { mergeComponentProps } from "../../../utils/index.js";
1
2
  import { createContext, useContext } from "react";
3
+ import { useTabListContext } from "../tab-list/helpers.js";
2
4
  const TabIndexContext = createContext(-1);
3
5
  const useTabIndexContext = () => {
4
6
  return useContext(TabIndexContext);
5
7
  };
8
+ const defaultProps = {
9
+ sx: {
10
+ flexShrink: 0,
11
+ display: "flex",
12
+ alignItems: "center",
13
+ justifyContent: "center"
14
+ }
15
+ };
16
+ const useGetTabProps = () => {
17
+ const { tabConfig = {} } = useTabListContext();
18
+ return (extended) => {
19
+ return mergeComponentProps(defaultProps, tabConfig, extended);
20
+ };
21
+ };
6
22
  export {
7
23
  TabIndexContext,
24
+ useGetTabProps,
8
25
  useTabIndexContext
9
26
  };
@@ -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,90 @@
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
+ | `as` | `HTMLElementType` | `'div'` | Tag HTML của container. |
18
+ | `sx` | `ToResponsiveProps<StyledCSS>` | - | Style responsive cho container. |
19
+ | `ref` | `React.Ref<HTMLElement>` | - | Trỏ tới DOM node của container. |
20
+
21
+ Kiểu prop là `PrimitiveProps<JSX.IntrinsicElements['div'], 'div'>` — kế thừa toàn bộ div attributes cùng `as` và `sx` từ [Primitive](../../primitive/README.md).
22
+
23
+ > React 19 coi `ref` là prop thường, nên `ref` được `Primitive` spread thẳng vào DOM node của container.
24
+
25
+ *Required props
26
+
27
+ ## Cách hoạt động
28
+
29
+ - Children được `flattenChildren` trước khi chọn, nên bọc panel trong Fragment vẫn giữ index đúng.
30
+ - 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.
31
+ - Nếu `activeIndex` không khớp child nào (vd: `defaultActiveIndex` vượt số panel), `TabContent` render rỗng.
32
+
33
+ ## Examples
34
+
35
+ ### Cơ bản
36
+
37
+ ```tsx
38
+ <Tabs defaultActiveIndex={0}>
39
+ <TabList>
40
+ <Tab>Tab 1</Tab>
41
+ <Tab>Tab 2</Tab>
42
+ </TabList>
43
+ <TabContent>
44
+ <TabPanel>Content for Tab 1</TabPanel>
45
+ <TabPanel>Content for Tab 2</TabPanel>
46
+ </TabContent>
47
+ </Tabs>
48
+ ```
49
+
50
+ ### Style container
51
+
52
+ ```tsx
53
+ <TabContent style={{ paddingTop: 16, minHeight: 200 }}>
54
+ <TabPanel>Panel 1</TabPanel>
55
+ <TabPanel>Panel 2</TabPanel>
56
+ </TabContent>
57
+ ```
58
+
59
+ ### Số panel ít hơn số tab
60
+
61
+ ```tsx
62
+ {/* Tab 3 sẽ hiển thị content rỗng */}
63
+ <Tabs>
64
+ <TabList>
65
+ <Tab>Tab 1</Tab>
66
+ <Tab>Tab 2</Tab>
67
+ <Tab>Tab 3</Tab>
68
+ </TabList>
69
+ <TabContent>
70
+ <TabPanel>Panel 1</TabPanel>
71
+ <TabPanel>Panel 2</TabPanel>
72
+ </TabContent>
73
+ </Tabs>
74
+ ```
75
+
76
+ ## Related Components
77
+
78
+ - [Tabs](../README.md)
79
+ - [TabPanel](../tab-panel/README.md)
80
+ - [Primitive](../../primitive/README.md)
81
+
82
+ ## Dependencies
83
+
84
+ - @emotion/react
85
+
86
+ ## See Also
87
+
88
+ - [Component Source](./tab-content.tsx)
89
+ - [Storybook Stories](../tabs.stories.tsx)
90
+ - [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,3 @@
1
- import { PropsWithChildren } from 'react';
2
- export type TabContentProps = PropsWithChildren<{
3
- ref?: React.Ref<HTMLDivElement>;
4
- }>;
1
+ import { JSX } from 'react';
2
+ import { PrimitiveProps } from '../../primitive';
3
+ export type TabContentProps = PrimitiveProps<JSX.IntrinsicElements['div'], '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
- import { Stylesheet } from "../../stylesheet/stylesheet.js";
4
+ import "lodash-es";
5
+ import "../../../theme/palette.js";
6
+ import { isNumber } from "../../../utils/common.js";
5
7
  import { useTabsContext } from "../tabs/helpers.js";
6
- const TabIndicator = () => {
7
- const { activeIndex, tabIndicatorProps } = useTabsContext();
8
+ import { Stylesheet } from "../../stylesheet/stylesheet.js";
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>>;