@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
package/README.md CHANGED
@@ -140,12 +140,7 @@ export const Button = ({ ref, ..._props }: ButtonProps) => {
140
140
  const { size, variant, color, children, ...rest } = getButtonProps(_props);
141
141
 
142
142
  // 2. Build CSS từ helpers
143
- const css = [
144
- getButtonCssBySize(size),
145
- getButtonCssByVariant(variant),
146
- getButtonCssByColor(color),
147
- createSxInterpolation(sx),
148
- ];
143
+ const css = [getButtonCssBySize(size), getButtonCssByVariant(variant), getButtonCssByColor(color), createSxStyle(sx)];
149
144
 
150
145
  // 3. Render với Primitive hoặc native element
151
146
  return (
@@ -287,12 +282,12 @@ Hầu hết components hỗ trợ 4 size variants:
287
282
 
288
283
  **Size mapping:**
289
284
 
290
- | size | iconSize | inputSize | inputPaddingX | textVariant |
291
- | ---- | -------- | --------- | ------------- | ----------- |
292
- | `xs` | 20px | 24px | 8px | `xs` |
293
- | `sm` | 20px | 32px | 12px | `sm` |
294
- | `md` | 24px | 40px | 12px | `sm` |
295
- | `lg` | 24px | 48px | 16px | `base` |
285
+ | size | iconSize | inputSize | inputPaddingInline | textVariant |
286
+ | ---- | -------- | --------- | ------------------ | ----------- |
287
+ | `xs` | 20px | 24px | 8px | `xs` |
288
+ | `sm` | 20px | 32px | 12px | `sm` |
289
+ | `md` | 24px | 40px | 12px | `sm` |
290
+ | `lg` | 24px | 48px | 16px | `base` |
296
291
 
297
292
  ### Color Variants
298
293
 
package/dist/index.js CHANGED
@@ -89,8 +89,9 @@ import { SwiperSlide } from "./lib/swiper/swiper-slide/swiper-slide.js";
89
89
  import { Switch } from "./lib/switch/switch.js";
90
90
  import { Tab } from "./lib/tabs/tab/tab.js";
91
91
  import { TabContent } from "./lib/tabs/tab-content/tab-content.js";
92
- import { TabIndexContext, useTabIndexContext } from "./lib/tabs/tab/helpers.js";
92
+ import { TabIndexContext, useGetTabProps, useTabIndexContext } from "./lib/tabs/tab/helpers.js";
93
93
  import { TabList } from "./lib/tabs/tab-list/tab-list.js";
94
+ import { TabListContext, getTabListProps, useTabListContext } from "./lib/tabs/tab-list/helpers.js";
94
95
  import { TabPanel } from "./lib/tabs/tab-panel/tab-panel.js";
95
96
  import { Table } from "./lib/table/table/table.js";
96
97
  import { TableBody } from "./lib/table/table-body/table-body.js";
@@ -135,7 +136,6 @@ import { getPriceLabelProps } from "./lib/price-label/helpers.js";
135
136
  import { getRadioButtonProps } from "./lib/radio-button/helpers.js";
136
137
  import { getStarRatingProps } from "./lib/star-rating/helpers.js";
137
138
  import { getSwiperProps } from "./lib/swiper/swiper/helpers.js";
138
- import { getTabListProps } from "./lib/tabs/tab-list/helpers.js";
139
139
  import { getTabPanelProps } from "./lib/tabs/tab-panel/helpers.js";
140
140
  import { getTextCssByVariant, getTextProps } from "./lib/text/helpers.js";
141
141
  import { getTextareaProps } from "./lib/textarea/helpers.js";
@@ -241,6 +241,7 @@ export {
241
241
  TabContent,
242
242
  TabIndexContext,
243
243
  TabList,
244
+ TabListContext,
244
245
  TabPanel,
245
246
  Table,
246
247
  TableBody,
@@ -308,8 +309,10 @@ export {
308
309
  useDialogContext,
309
310
  useDrawerContext,
310
311
  useFloatingContext,
312
+ useGetTabProps,
311
313
  useNotifications,
312
314
  usePopoverContext,
313
315
  useTabIndexContext,
316
+ useTabListContext,
314
317
  useTabsContext
315
318
  };
@@ -8,7 +8,7 @@ import LoadingIcon from "../../assets/svg/loading-waiting.svg.js";
8
8
  import { mediaQuery } from "../../theme/screens.js";
9
9
  import { getColorVar } from "../../theme/palette.js";
10
10
  import { getColorShadesByVariant, getSizeVariantState } from "../../utils/index.js";
11
- import { createSxInterpolation } from "../../utils/stylesheet.js";
11
+ import { createSxStyle } from "../../utils/stylesheet.js";
12
12
  import { RippleEffect } from "../ripple-effect/ripple-effect.js";
13
13
  import { Icon } from "../icon/icon.js";
14
14
  import { Primitive } from "../primitive/primitive.js";
@@ -114,7 +114,7 @@ const Button = ({ children, ref, ..._props }) => {
114
114
  [mediaQuery("md")]: createStyle(md),
115
115
  [mediaQuery("lg")]: createStyle(lg)
116
116
  },
117
- createSxInterpolation(sx)
117
+ createSxStyle(sx)
118
118
  ];
119
119
  const onClick = (e) => {
120
120
  var _a;
@@ -133,7 +133,7 @@ const Button = ({ children, ref, ..._props }) => {
133
133
  try {
134
134
  const url = new URL(href);
135
135
  window.location.href = url.toString();
136
- } catch (error) {
136
+ } catch (_error) {
137
137
  navigate(href, otherOptions);
138
138
  }
139
139
  }
@@ -16,10 +16,10 @@ const defaultProps = {
16
16
  type: "button"
17
17
  };
18
18
  const getButtonCssBySize = (size) => {
19
- const { inputSize, inputPaddingX } = getSizeVariantState(size);
19
+ const { inputSize, inputPaddingInline } = getSizeVariantState(size);
20
20
  const css = {
21
- paddingLeft: inputPaddingX,
22
- paddingRight: inputPaddingX,
21
+ paddingLeft: inputPaddingInline,
22
+ paddingRight: inputPaddingInline,
23
23
  minHeight: inputSize
24
24
  };
25
25
  switch (size) {
@@ -160,7 +160,7 @@ const CascaderFloatingContent = ({
160
160
  });
161
161
  }, [inputValue, currentOptions]);
162
162
  const responsiveSize = isNil(windowScreen.name) ? void 0 : getResponsiveProp({ sm, md, lg }, "size", windowScreen.name) ?? size;
163
- const { inputSize: rowMinHeight = 0, inputPaddingX } = getSizeVariantState(responsiveSize);
163
+ const { inputSize: rowMinHeight = 0, inputPaddingInline } = getSizeVariantState(responsiveSize);
164
164
  const onInputChange = (e) => {
165
165
  var _a;
166
166
  e.target.value = trimStart(e.target.value);
@@ -193,8 +193,8 @@ const CascaderFloatingContent = ({
193
193
  Primitive,
194
194
  {
195
195
  sx: {
196
- paddingLeft: inputPaddingX,
197
- paddingRight: inputPaddingX,
196
+ paddingLeft: inputPaddingInline,
197
+ paddingRight: inputPaddingInline,
198
198
  minHeight: rowMinHeight,
199
199
  borderTop: `1px solid ${getColorVar("neutral.200")}`,
200
200
  display: "flex",
@@ -292,8 +292,8 @@ const CascaderFloatingContent = ({
292
292
  onClick: handleSelect,
293
293
  sx: {
294
294
  minHeight: rowMinHeight,
295
- paddingLeft: inputPaddingX,
296
- paddingRight: inputPaddingX,
295
+ paddingLeft: inputPaddingInline,
296
+ paddingRight: inputPaddingInline,
297
297
  paddingBlock: 4,
298
298
  display: "flex",
299
299
  alignItems: "center",
@@ -42,10 +42,10 @@ const Chip = ({ children, ..._props }) => {
42
42
  );
43
43
  };
44
44
  const createStyle = ({ size } = {}) => {
45
- const { inputSize, inputPaddingX, textVariant, rounded } = getChipSizeState(size);
45
+ const { inputSize, paddingInline, textVariant, rounded } = getChipSizeState(size);
46
46
  return {
47
- paddingLeft: inputPaddingX,
48
- paddingRight: inputPaddingX,
47
+ paddingLeft: paddingInline,
48
+ paddingRight: paddingInline,
49
49
  minHeight: inputSize,
50
50
  fontSize: textVariant ? getTypographyVar(textVariant).fontSize : void 0,
51
51
  lineHeight: textVariant ? getTypographyVar(textVariant).lineHeight : void 0,
@@ -3,7 +3,7 @@ import { TypographySizeVariant } from '../../theme';
3
3
  import { ChipProps } from './types';
4
4
  export declare const getChipSizeState: (size?: SizeVariant) => Partial<{
5
5
  inputSize: number;
6
- inputPaddingX: number;
6
+ paddingInline: number;
7
7
  textVariant: TypographySizeVariant;
8
8
  rounded: number;
9
9
  }>;
@@ -6,13 +6,13 @@ const defaultProps = {
6
6
  const getChipSizeState = (size) => {
7
7
  switch (size) {
8
8
  case "xs":
9
- return { inputSize: 20, inputPaddingX: 6, textVariant: "xs", rounded: 4 };
9
+ return { inputSize: 20, paddingInline: 6, textVariant: "xs", rounded: 4 };
10
10
  case "sm":
11
- return { inputSize: 24, inputPaddingX: 8, textVariant: "xs", rounded: 6 };
11
+ return { inputSize: 24, paddingInline: 8, textVariant: "xs", rounded: 6 };
12
12
  case "md":
13
- return { inputSize: 28, inputPaddingX: 10, textVariant: "sm", rounded: 8 };
13
+ return { inputSize: 28, paddingInline: 10, textVariant: "sm", rounded: 8 };
14
14
  case "lg":
15
- return { inputSize: 32, inputPaddingX: 12, textVariant: "sm", rounded: 8 };
15
+ return { inputSize: 32, paddingInline: 12, textVariant: "sm", rounded: 8 };
16
16
  default:
17
17
  return {};
18
18
  }
@@ -11,7 +11,7 @@ import "lodash-es";
11
11
  import { getColorVar } from "../../theme/palette.js";
12
12
  import { getTypographyVar } from "../../theme/typography.js";
13
13
  import CloseIcon from "../../assets/svg/times-close-delete.svg.js";
14
- import { createSxInterpolation } from "../../utils/stylesheet.js";
14
+ import { createSxStyle } from "../../utils/stylesheet.js";
15
15
  import { TimePicker } from "../time-picker/time-picker.js";
16
16
  import { Floating } from "../floating/floating/floating.js";
17
17
  import { FloatingTrigger } from "../floating/floating-trigger/floating-trigger.js";
@@ -193,8 +193,8 @@ const DatePicker = (_props, ref) => {
193
193
  flexDirection: "column",
194
194
  width: "100%"
195
195
  },
196
- createSxInterpolation(sx),
197
- createSxInterpolation(containerSx)
196
+ createSxStyle(sx),
197
+ createSxStyle(containerSx)
198
198
  ],
199
199
  children: [
200
200
  /* @__PURE__ */ jsxs(
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx } from "@emotion/react/jsx-runtime";
2
2
  import { getIconButtonProps } from "./helpers.js";
3
- import { createSxInterpolation } from "../../utils/stylesheet.js";
3
+ import { createSxStyle } from "../../utils/stylesheet.js";
4
4
  import LoadingIcon from "../../assets/svg/loading-waiting.svg.js";
5
5
  import { mediaQuery } from "../../theme/screens.js";
6
6
  import { getColorVar } from "../../theme/palette.js";
@@ -92,7 +92,7 @@ const IconButton = ({ children, ref, ..._props }) => {
92
92
  [mediaQuery("md")]: createStyle(md),
93
93
  [mediaQuery("lg")]: createStyle(lg)
94
94
  },
95
- createSxInterpolation(sx)
95
+ createSxStyle(sx)
96
96
  ];
97
97
  const onClick = (e) => {
98
98
  var _a;
@@ -252,10 +252,10 @@ const Input = ({ ref, ..._props }) => {
252
252
  );
253
253
  };
254
254
  const createStyle = ({ size } = {}) => {
255
- const { inputPaddingX, inputSize, textVariant } = getSizeVariantState(size);
255
+ const { inputPaddingInline, inputSize, textVariant } = getSizeVariantState(size);
256
256
  return {
257
- paddingLeft: inputPaddingX,
258
- paddingRight: inputPaddingX,
257
+ paddingLeft: inputPaddingInline,
258
+ paddingRight: inputPaddingInline,
259
259
  height: inputSize,
260
260
  fontSize: textVariant ? getTypographyVar(textVariant).fontSize : void 0,
261
261
  lineHeight: textVariant ? getTypographyVar(textVariant).lineHeight : void 0
@@ -4,7 +4,7 @@ import { getInputMaskProps } from "./helpers.js";
4
4
  import { mediaQuery } from "../../theme/screens.js";
5
5
  import { getColorVar } from "../../theme/palette.js";
6
6
  import { getSizeVariantState } from "../../utils/index.js";
7
- import { createSxInterpolation } from "../../utils/stylesheet.js";
7
+ import { createSxStyle } from "../../utils/stylesheet.js";
8
8
  import { Primitive } from "../primitive/primitive.js";
9
9
  import { Icon } from "../icon/icon.js";
10
10
  const InputMask = (_props, ref) => {
@@ -37,7 +37,7 @@ const InputMask = (_props, ref) => {
37
37
  [mediaQuery("md")]: createStyle(md),
38
38
  [mediaQuery("lg")]: createStyle(lg)
39
39
  },
40
- createSxInterpolation(sx)
40
+ createSxStyle(sx)
41
41
  ];
42
42
  return /* @__PURE__ */ jsxs(Primitive, { css, ...props, ref, children: [
43
43
  prefix,
@@ -85,12 +85,12 @@ const InputMask = (_props, ref) => {
85
85
  };
86
86
  const ForwardRefInputMask = forwardRef(InputMask);
87
87
  const createStyle = ({ size, outline } = {}) => {
88
- const { inputSize, inputPaddingX } = getSizeVariantState(size);
88
+ const { inputSize, inputPaddingInline } = getSizeVariantState(size);
89
89
  return [
90
90
  {
91
91
  minHeight: inputSize,
92
- paddingLeft: inputPaddingX,
93
- paddingRight: inputPaddingX,
92
+ paddingLeft: inputPaddingInline,
93
+ paddingRight: inputPaddingInline,
94
94
  ":after": {
95
95
  borderTopWidth: outline == null ? void 0 : outline.top,
96
96
  borderRightWidth: outline == null ? void 0 : outline.right,
@@ -64,8 +64,8 @@ const InputRange = ({ ref: _ref, ..._props }) => {
64
64
  borderStyle: "solid",
65
65
  borderRadius: "0.5rem",
66
66
  gap: "0.5rem",
67
- paddingLeft: getSizeVariantState(size).inputPaddingX,
68
- paddingRight: getSizeVariantState(size).inputPaddingX,
67
+ paddingLeft: getSizeVariantState(size).inputPaddingInline,
68
+ paddingRight: getSizeVariantState(size).inputPaddingInline,
69
69
  "&:has(input:focus)": {
70
70
  borderColor: getColorVar("primary.600"),
71
71
  boxShadow: `0 1px 3px 0 ${getColorVar("primary.300")}, 0 1px 2px -1px ${getColorVar("primary.300")}`
@@ -74,16 +74,16 @@ const InputRange = ({ ref: _ref, ..._props }) => {
74
74
  borderColor: getColorVar("primary.300")
75
75
  },
76
76
  sm: {
77
- paddingLeft: getSizeVariantState(sm == null ? void 0 : sm.size).inputPaddingX,
78
- paddingRight: getSizeVariantState(sm == null ? void 0 : sm.size).inputPaddingX
77
+ paddingLeft: getSizeVariantState(sm == null ? void 0 : sm.size).inputPaddingInline,
78
+ paddingRight: getSizeVariantState(sm == null ? void 0 : sm.size).inputPaddingInline
79
79
  },
80
80
  md: {
81
- paddingLeft: getSizeVariantState(md == null ? void 0 : md.size).inputPaddingX,
82
- paddingRight: getSizeVariantState(md == null ? void 0 : md.size).inputPaddingX
81
+ paddingLeft: getSizeVariantState(md == null ? void 0 : md.size).inputPaddingInline,
82
+ paddingRight: getSizeVariantState(md == null ? void 0 : md.size).inputPaddingInline
83
83
  },
84
84
  lg: {
85
- paddingLeft: getSizeVariantState(lg == null ? void 0 : lg.size).inputPaddingX,
86
- paddingRight: getSizeVariantState(lg == null ? void 0 : lg.size).inputPaddingX
85
+ paddingLeft: getSizeVariantState(lg == null ? void 0 : lg.size).inputPaddingInline,
86
+ paddingRight: getSizeVariantState(lg == null ? void 0 : lg.size).inputPaddingInline
87
87
  }
88
88
  },
89
89
  children: [
@@ -1,35 +1,48 @@
1
1
  # Tabs
2
2
 
3
- A compound tabs component for switching between content panels. It manages active tab state (controlled or uncontrolled) and provides context to sub-components: TabList, Tab, TabContent, TabPanel, and TabIndicator.
3
+ Compound component để chuyển đổi giữa các panel nội dung. `Tabs` quản lý active tab index (controlled hoặc uncontrolled) và cung cấp `activeIndex` + hàm `focusTab` xuống sub-components qua context: `TabList`, `Tab`, `TabContent`, `TabPanel`, `TabIndicator`.
4
+
5
+ Việc styling các tab được cấu hình ở `TabList` (`tabConfig`, `indicator`, `align`, `overflowBehavior`), không phải ở `Tabs`.
4
6
 
5
7
  ## Import
6
8
 
7
9
  ```tsx
8
- import { Tabs } from '@janbox/storefront-ui';
10
+ import { Tabs, TabList, Tab, TabContent, TabPanel } from '@janbox/storefront-ui';
9
11
  ```
10
12
 
11
13
  ## Props
12
14
 
13
15
  | Prop | Type | Default | Description |
14
- |------|------|---------|-------------|
15
- | `activeIndex` | `number` | - | Controlled active tab index. When provided, the component delegates state management to the parent. |
16
- | `defaultActiveIndex` | `number` | `0` | Initial active tab index for uncontrolled usage. |
17
- | `onTabChange` | `(tabIndex: number) => void` | - | Callback fired when the active tab changes, receiving the new tab index. |
18
- | `tabProps` | `Partial<TabProps>` | `{ color: 'neutral', variant: 'text', states: { active: { color: 'primary', variant: 'text' } } }` | Default props applied to every Tab child. Inactive tabs default to `color='neutral'`, active tab overrides to `color='primary'`. |
19
- | `tabIndicatorProps` | `TabIndicatorProps` | `{ sx: { height: 2, backgroundColor: 'var(--color-primary-600)' } }` | Props forwarded to the TabIndicator element, cho phép custom `sx` styles trên thanh gạch chân của tab đang active. |
20
- | `tabPanelProps` | `Partial<TabPanelProps>` | - | Default props applied to every TabPanel child. |
16
+ | ------ | ------ | ------- | ----------- |
17
+ | `activeIndex` | `number` | - | Controlled active tab index. Khi được cung cấp, component chuyển sang controlled mode và state nội bộ không tự đổi. |
18
+ | `defaultActiveIndex` | `number` | `0` | Active tab index ban đầu cho uncontrolled usage. |
19
+ | `onTabChange` | `(nextIndex: number) => void` | - | Callback được gọi khi active tab thay đổi, nhận index mới. |
21
20
  | `preventActiveTabChange` | `boolean` | `true` | Khi `true`, click vào tab đang active sẽ không re-fire `onTabChange`. |
22
21
  | `sx` | `ToResponsiveProps<StyledCSS>` | - | Responsive Emotion CSS overrides cho root wrapper `div`. |
23
22
  | `children` * | `React.ReactNode` | - | Phải là composition của `TabList` (chứa các `Tab`) và `TabContent` (chứa các `TabPanel`). |
23
+ | `ref` | `React.Ref<HTMLDivElement>` | - | Forwarded ref tới root `div`. |
24
+
25
+ Kế thừa toàn bộ `JSX.IntrinsicElements['div']` attributes.
24
26
 
25
27
  *Required props
26
28
 
29
+ ## Cách hoạt động
30
+
31
+ - **Index của mỗi Tab**: `TabList` flatten children và bọc mỗi child trong `TabIndexContext.Provider` với index tương ứng. `Tab` đọc index này để so với `activeIndex` từ `TabsContext`.
32
+ - **Click handling**: mỗi `Tab` tự gắn `onClick` và gọi `focusTab(tabIndex)` lấy từ `TabsContext`. `focusTab` kiểm tra `preventActiveTabChange` rồi cập nhật state và gọi `onTabChange`. `onClick` bạn truyền vào `Tab` vẫn được gọi sau đó.
33
+ - **Styling**: `TabList` truyền `tabConfig` xuống mọi `Tab` con qua `TabListContext`. Mỗi `Tab` merge theo thứ tự `defaultProps` → `tabConfig` → props của chính nó, nên prop trực tiếp trên `Tab` luôn thắng.
34
+ - **Panel rendering**: `TabContent` chỉ render đúng một child tại `activeIndex` — các `TabPanel` khác bị unmount, state nội bộ của chúng không được giữ lại.
35
+
36
+ > ⚠️ `Tab` phải nằm trong cả `Tabs` và `TabList`. Ngoài `Tabs` thì `focusTab` là `undefined` và click sẽ throw; ngoài `TabList` thì index luôn là `-1` nên không tab nào active được.
37
+ >
38
+ > ⚠️ Ở controlled mode (`activeIndex` được truyền), state nội bộ không tự đổi — bạn phải cập nhật `activeIndex` trong `onTabChange`, nếu không tab sẽ không chuyển.
39
+
27
40
  ## Examples
28
41
 
29
42
  ### Basic uncontrolled tabs
30
43
 
31
44
  ```tsx
32
- <Tabs defaultActiveIndex={0} onTabChange={(i) => console.log(i)} style={{ width: 500 }}>
45
+ <Tabs defaultActiveIndex={0} onTabChange={(i) => console.log(i)} sx={{ width: 500 }}>
33
46
  <TabList>
34
47
  <Tab>Tab 1</Tab>
35
48
  <Tab>Tab 2</Tab>
@@ -43,12 +56,12 @@ import { Tabs } from '@janbox/storefront-ui';
43
56
  </Tabs>
44
57
  ```
45
58
 
46
- ### Controlled tabs with second tab active by default
59
+ ### Controlled tabs
47
60
 
48
61
  ```tsx
49
62
  const [active, setActive] = useState(1);
50
63
 
51
- <Tabs activeIndex={active} onTabChange={setActive} style={{ width: 500 }}>
64
+ <Tabs activeIndex={active} onTabChange={setActive} sx={{ width: 500 }}>
52
65
  <TabList>
53
66
  <Tab>Overview</Tab>
54
67
  <Tab>Details</Tab>
@@ -62,15 +75,23 @@ const [active, setActive] = useState(1);
62
75
  </Tabs>
63
76
  ```
64
77
 
65
- ### Custom tab color and variant via tabProps
78
+ ### Custom styling qua TabList
66
79
 
67
80
  ```tsx
68
- <Tabs
69
- defaultActiveIndex={0}
70
- tabProps={{ color: 'primary', variant: 'text' }}
71
- style={{ width: 500 }}
72
- >
73
- <TabList>
81
+ <Tabs defaultActiveIndex={0} sx={{ width: 500 }}>
82
+ <TabList
83
+ align="center"
84
+ tabConfig={{
85
+ sx: {
86
+ color: getColorVar('neutral.500'),
87
+ sm: { height: 48 },
88
+ },
89
+ states: {
90
+ active: { sx: { color: getColorVar('green.600') } },
91
+ },
92
+ }}
93
+ indicator={{ size: 3, sx: { backgroundColor: getColorVar('green.600') } }}
94
+ >
74
95
  <Tab>Home</Tab>
75
96
  <Tab>Profile</Tab>
76
97
  <Tab>Settings</Tab>
@@ -83,17 +104,63 @@ const [active, setActive] = useState(1);
83
104
  </Tabs>
84
105
  ```
85
106
 
107
+ ### Tab wrap thay vì scroll ngang
108
+
109
+ ```tsx
110
+ <Tabs sx={{ width: 300 }}>
111
+ <TabList overflowBehavior="wrap">
112
+ <Tab>Tab 1</Tab>
113
+ <Tab>Tab 2</Tab>
114
+ <Tab>Tab 3</Tab>
115
+ <Tab>Tab 4</Tab>
116
+ <Tab>Tab 5</Tab>
117
+ </TabList>
118
+ <TabContent>{/* … */}</TabContent>
119
+ </Tabs>
120
+ ```
121
+
122
+ ### Tab với icon
123
+
124
+ ```tsx
125
+ import HomeIcon from '@/assets/svg/home.svg?raw';
126
+
127
+ <Tabs defaultActiveIndex={0}>
128
+ <TabList>
129
+ <Tab startIcon={{ source: HomeIcon, size: 20 }}>Home</Tab>
130
+ <Tab endIcon={{ source: HomeIcon, size: 20 }}>Settings</Tab>
131
+ </TabList>
132
+ <TabContent>{/* … */}</TabContent>
133
+ </Tabs>
134
+ ```
135
+
136
+ ### Ẩn indicator
137
+
138
+ ```tsx
139
+ <Tabs defaultActiveIndex={0}>
140
+ <TabList indicator={false}>
141
+ <Tab>Tab 1</Tab>
142
+ <Tab>Tab 2</Tab>
143
+ </TabList>
144
+ <TabContent>{/* … */}</TabContent>
145
+ </Tabs>
146
+ ```
147
+
148
+ ## Sub-components
149
+
150
+ - [TabList](./tab-list/README.md)
151
+ - [Tab](./tab/README.md)
152
+ - [TabContent](./tab-content/README.md)
153
+ - [TabPanel](./tab-panel/README.md)
154
+ - [TabIndicator](./tab-indicator/README.md)
155
+
86
156
  ## Related Components
87
157
 
88
- - [TabList](../tablist/README.md)
89
- - [Tab](../tab/README.md)
90
- - [TabContent](../tabcontent/README.md)
91
- - [TabPanel](../tabpanel/README.md)
92
- - [TabIndicator](../tabindicator/README.md)
93
- - [Button](../button/README.md)
158
+ - [Icon](../icon/README.md)
159
+ - [Primitive](../primitive/README.md)
94
160
 
95
161
  ## Dependencies
96
162
 
163
+ - motion/react
97
164
  - @emotion/react
98
165
 
99
166
  ## See Also
@@ -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
  };