@plexui/ui 0.4.0 → 0.5.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.
|
@@ -8,7 +8,7 @@ import { handlePressableMouseEnter, waitForAnimationFrame } from "../../lib/help
|
|
|
8
8
|
import {} from "../../types";
|
|
9
9
|
import { LoadingIndicator } from "../Indicator";
|
|
10
10
|
import s from "./Tabs.module.css";
|
|
11
|
-
export const Tabs = ({ value, onChange, children, variant = "segmented", orientation = "horizontal", block, pill = true, size = "md", gutterSize, className, onClick, ...restProps }) => {
|
|
11
|
+
export const Tabs = ({ value, onChange, children, variant = "segmented", orientation = "horizontal", block, pill = true, flush, size = "md", gutterSize, className, onClick, ...restProps }) => {
|
|
12
12
|
const rootRef = useRef(null);
|
|
13
13
|
const thumbRef = useRef(null);
|
|
14
14
|
const prevSizeRef = useRef(size);
|
|
@@ -126,7 +126,7 @@ export const Tabs = ({ value, onChange, children, variant = "segmented", orienta
|
|
|
126
126
|
}
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
}, [applyThumbSizing, value, size, gutterSize, pill, transitionProperty]);
|
|
129
|
+
}, [applyThumbSizing, value, size, gutterSize, pill, flush, transitionProperty]);
|
|
130
130
|
const handleValueChange = (nextValue) => {
|
|
131
131
|
// Only trigger onChange when a value exists
|
|
132
132
|
// Disallow toggling off enabled items
|
|
@@ -135,7 +135,9 @@ export const Tabs = ({ value, onChange, children, variant = "segmented", orienta
|
|
|
135
135
|
};
|
|
136
136
|
// Only apply pill for segmented variant
|
|
137
137
|
const isPill = variant === "segmented" && pill;
|
|
138
|
-
|
|
138
|
+
// Only apply flush for underline variant
|
|
139
|
+
const isFlush = variant === "underline" && flush;
|
|
140
|
+
return (_jsxs(ToggleGroup.Root, { ref: rootRef, className: clsx(s.Tabs, className), type: "single", value: value, loop: false, onValueChange: handleValueChange, onClick: onClick, "data-variant": variant, "data-orientation": orientation, "data-block": block ? "" : undefined, "data-pill": isPill ? "" : undefined, "data-flush": isFlush ? "" : undefined, "data-size": size, "data-gutter-size": gutterSize, ...restProps, children: [_jsx("div", { className: s.TabsThumb, ref: thumbRef }), children] }));
|
|
139
141
|
};
|
|
140
142
|
// Type guard for badge object form
|
|
141
143
|
const isBadgeObject = (badge) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\"\n\nimport clsx from \"clsx\"\nimport { ToggleGroup } from \"radix-ui\"\nimport { useCallback, useLayoutEffect, useRef } from \"react\"\nimport { useResizeObserver } from \"usehooks-ts\"\nimport { handlePressableMouseEnter, waitForAnimationFrame } from \"../../lib/helpers\"\nimport { type ControlSize, type SemanticColors, type Sizes, type Variants } from \"../../types\"\nimport { LoadingIndicator } from \"../Indicator\"\nimport s from \"./Tabs.module.css\"\n\nexport type SizeVariant = \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type TabsVariant = \"segmented\" | \"underline\"\nexport type TabsOrientation = \"horizontal\" | \"vertical\"\n\nexport type TabsProps<T extends string> = {\n /**\n * Controlled value for the group\n */\n \"value\": T\n /** Callback for when a new value is selected */\n \"onChange\"?: (nextValue: T) => void\n /** Callback any time the control is clicked (even if a new value was not selected) */\n \"onClick\"?: () => void\n /**\n * Text read aloud to screen readers when the control is focused\n */\n \"aria-label\": string\n /**\n * Visual variant of the tab group\n * - `\"segmented\"` — background container with sliding highlight (default)\n * - `\"underline\"` — no background, animated line indicator under active tab\n * @default \"segmented\"\n */\n \"variant\"?: TabsVariant\n /**\n * Orientation of the tab layout\n * @default \"horizontal\"\n */\n \"orientation\"?: TabsOrientation\n /**\n * Controls the size of the tabs\n *\n * | 3xs | 2xs | xs | sm | md | lg | xl | 2xl | 3xl |\n * | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- |\n * | `22px` | `24px` | `26px` | `28px` | `32px` | `36px` | `40px` | `44px` | `48px` |\n *\n * @default md\n */\n \"size\"?: ControlSize\n /**\n * Controls gutter on the edges of the button, defaults to value from `size`.\n *\n * | 2xs | xs | sm | md | lg | xl |\n * | ------ | ------ | ------ | ------ | ------ | ------ |\n * | `6px` | `8px` | `10px` | `12px` | `14px` | `16px` |\n */\n \"gutterSize\"?: Sizes<\"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\">\n /** Disable the entire group */\n \"disabled\"?: boolean\n /**\n * Display the control as a block element with equal width segments\n * @default false\n */\n \"block\"?: boolean\n /**\n * Determines if the tabs should be a fully rounded pill shape.\n * Only applies to the `\"segmented\"` variant.\n * @default true\n */\n \"pill\"?: boolean\n \"className\"?: string\n \"children\": React.ReactNode\n}\n\nexport const Tabs = <T extends string>({\n value,\n onChange,\n children,\n variant = \"segmented\",\n orientation = \"horizontal\",\n block,\n pill = true,\n size = \"md\",\n gutterSize,\n className,\n onClick,\n ...restProps\n}: TabsProps<T>) => {\n const rootRef = useRef<HTMLDivElement>(null)\n const thumbRef = useRef<HTMLDivElement>(null)\n const prevSizeRef = useRef(size)\n const isVertical = orientation === \"vertical\"\n\n const applyThumbSizing = useCallback(\n (attemptScroll: boolean) => {\n const root = rootRef.current\n const thumb = thumbRef.current\n\n if (!root || !thumb) {\n return\n }\n\n // Get selected node\n const activeNode = root?.querySelector<HTMLDivElement>('[data-state=\"on\"]')\n\n // Impossible\n if (!activeNode) {\n return\n }\n\n if (isVertical) {\n const rootHeight = root.clientHeight\n let targetHeight = Math.floor(activeNode.clientHeight)\n const targetOffset = activeNode.offsetTop\n\n // Detect subpixel edge case\n if (rootHeight - (targetHeight + targetOffset) < 2) {\n targetHeight = targetHeight - 1\n }\n\n thumb.style.height = `${Math.floor(targetHeight)}px`\n thumb.style.width = \"\"\n thumb.style.transform = `translateY(${targetOffset}px)`\n\n // Scroll into view if needed\n if (root.scrollHeight > rootHeight) {\n const buffer = rootHeight * 0.15\n const scrollTop = root.scrollTop\n const top = activeNode.offsetTop\n const bottom = top + targetHeight\n if (top < scrollTop + buffer || bottom > scrollTop + rootHeight - buffer) {\n if (attemptScroll) {\n activeNode.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: \"smooth\" })\n }\n }\n }\n } else {\n const rootWidth = root.clientWidth\n let targetWidth = Math.floor(activeNode.clientWidth)\n const targetOffset = activeNode.offsetLeft\n\n // Detect if the thumb is moving too far to the edge of the container.\n // This would most commonly be due to subpixel widths adding up to excessive distance.\n if (rootWidth - (targetWidth + targetOffset) < 2) {\n targetWidth = targetWidth - 1\n }\n\n thumb.style.width = `${Math.floor(targetWidth)}px`\n thumb.style.height = \"\"\n thumb.style.transform = `translateX(${targetOffset}px)`\n\n // If the control is scrollable, ensure the active option is visible\n if (root.scrollWidth > rootWidth) {\n // Only scroll items near the edge, but not the inner 2/3.\n const buffer = rootWidth * 0.15\n const scrollLeft = root.scrollLeft\n const left = activeNode.offsetLeft\n const right = left + targetWidth\n if (left < scrollLeft + buffer || right > scrollLeft + rootWidth - buffer) {\n // Cheap trick to avoid unintentional scroll on mount - transition is set after mounting\n if (attemptScroll) {\n activeNode.scrollIntoView({ block: \"nearest\", inline: \"center\", behavior: \"smooth\" })\n }\n }\n }\n }\n },\n [isVertical],\n )\n\n useResizeObserver({\n // @ts-expect-error(2322) -- bug in types: https://github.com/juliencrn/usehooks-ts/issues/663\n ref: rootRef,\n onResize: () => {\n const thumb = thumbRef.current\n\n if (!thumb) {\n return\n }\n\n // Perform the size update instantly\n const currentTransition = thumb.style.transition\n thumb.style.transition = \"\"\n applyThumbSizing(false)\n thumb.style.transition = currentTransition\n },\n })\n\n const transitionProperty = isVertical\n ? \"height 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)\"\n : \"width 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)\"\n\n useLayoutEffect(() => {\n const root = rootRef.current\n const thumb = thumbRef.current\n\n if (!root || !thumb) {\n return\n }\n\n const sizeChanged = prevSizeRef.current !== size\n prevSizeRef.current = size\n\n if (sizeChanged) {\n // Size changed - disable transition, wait for CSS, then apply sizing\n const currentTransition = thumb.style.transition\n thumb.style.transition = \"\"\n\n waitForAnimationFrame(() => {\n applyThumbSizing(false)\n waitForAnimationFrame(() => {\n thumb.style.transition = currentTransition\n })\n })\n } else {\n // Normal update (value change, etc.)\n waitForAnimationFrame(() => {\n applyThumbSizing(!!thumb.style.transition)\n\n // Apply transition after initial calculation is set\n if (!thumb.style.transition) {\n waitForAnimationFrame(() => {\n thumb.style.transition = transitionProperty\n })\n }\n })\n }\n }, [applyThumbSizing, value, size, gutterSize, pill, transitionProperty])\n\n const handleValueChange = (nextValue: T) => {\n // Only trigger onChange when a value exists\n // Disallow toggling off enabled items\n if (nextValue && onChange) onChange(nextValue)\n }\n\n // Only apply pill for segmented variant\n const isPill = variant === \"segmented\" && pill\n\n return (\n <ToggleGroup.Root\n ref={rootRef}\n className={clsx(s.Tabs, className)}\n type=\"single\"\n value={value}\n loop={false}\n onValueChange={handleValueChange}\n onClick={onClick}\n data-variant={variant}\n data-orientation={orientation}\n data-block={block ? \"\" : undefined}\n data-pill={isPill ? \"\" : undefined}\n data-size={size}\n data-gutter-size={gutterSize}\n {...restProps}\n >\n <div className={s.TabsThumb} ref={thumbRef} />\n {children}\n </ToggleGroup.Root>\n )\n}\n\n/**\n * Badge configuration for Tabs.Tab\n */\nexport type TabsBadgeProp =\n | React.ReactNode\n | {\n content: React.ReactNode\n color?: SemanticColors<\n \"secondary\" | \"success\" | \"danger\" | \"warning\" | \"info\" | \"discovery\" | \"caution\"\n >\n variant?: Variants<\"soft\" | \"solid\">\n pill?: boolean\n loading?: boolean\n }\n\nexport type TabProps = {\n /**\n * Tab value\n */\n \"value\": string\n /**\n * Text read aloud to screen readers when the tab is focused\n */\n \"aria-label\"?: string\n /**\n * Text content to render in the tab\n */\n \"children\"?: React.ReactNode\n /**\n * Icon to render before the text content\n */\n \"icon\"?: React.ReactNode\n /**\n * Badge to render after the text content.\n * Can be a simple value or an object with content, color, variant, and loading options.\n * @example badge={5}\n * @example badge={{ content: 5, color: \"danger\" }}\n */\n \"badge\"?: TabsBadgeProp\n /**\n * Disable the individual tab\n */\n \"disabled\"?: boolean\n}\n\n// Type guard for badge object form\nconst isBadgeObject = (\n badge: TabsBadgeProp,\n): badge is Exclude<TabsBadgeProp, React.ReactNode> & { content: React.ReactNode } => {\n return badge != null && typeof badge === \"object\" && \"content\" in badge\n}\n\nconst Tab = ({ children, icon, badge, ...restProps }: TabProps) => {\n // Normalize badge prop\n const badgeProps = badge != null ? (isBadgeObject(badge) ? badge : { content: badge }) : null\n\n return (\n <ToggleGroup.Item\n className={s.Tab}\n {...restProps}\n onPointerEnter={handlePressableMouseEnter}\n >\n <span className={s.TabContent}>\n {icon}\n {children && <span>{children}</span>}\n {badgeProps && (\n <span\n className={s.TabBadge}\n data-color={badgeProps.color ?? \"secondary\"}\n data-variant={badgeProps.variant ?? \"soft\"}\n data-pill={badgeProps.pill ? \"\" : undefined}\n >\n {badgeProps.loading ? <LoadingIndicator /> : badgeProps.content}\n </span>\n )}\n </span>\n </ToggleGroup.Item>\n )\n}\n\n// Attach sub-components\nTabs.Tab = Tab\n// Backward-compat alias\nTabs.Option = Tab\n"]}
|
|
1
|
+
{"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\"\n\nimport clsx from \"clsx\"\nimport { ToggleGroup } from \"radix-ui\"\nimport { useCallback, useLayoutEffect, useRef } from \"react\"\nimport { useResizeObserver } from \"usehooks-ts\"\nimport { handlePressableMouseEnter, waitForAnimationFrame } from \"../../lib/helpers\"\nimport { type ControlSize, type SemanticColors, type Sizes, type Variants } from \"../../types\"\nimport { LoadingIndicator } from \"../Indicator\"\nimport s from \"./Tabs.module.css\"\n\nexport type SizeVariant = \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type TabsVariant = \"segmented\" | \"underline\"\nexport type TabsOrientation = \"horizontal\" | \"vertical\"\n\nexport type TabsProps<T extends string> = {\n /**\n * Controlled value for the group\n */\n \"value\": T\n /** Callback for when a new value is selected */\n \"onChange\"?: (nextValue: T) => void\n /** Callback any time the control is clicked (even if a new value was not selected) */\n \"onClick\"?: () => void\n /**\n * Text read aloud to screen readers when the control is focused\n */\n \"aria-label\": string\n /**\n * Visual variant of the tab group\n * - `\"segmented\"` — background container with sliding highlight (default)\n * - `\"underline\"` — no background, animated line indicator under active tab\n * @default \"segmented\"\n */\n \"variant\"?: TabsVariant\n /**\n * Orientation of the tab layout\n * @default \"horizontal\"\n */\n \"orientation\"?: TabsOrientation\n /**\n * Controls the size of the tabs\n *\n * | 3xs | 2xs | xs | sm | md | lg | xl | 2xl | 3xl |\n * | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- |\n * | `22px` | `24px` | `26px` | `28px` | `32px` | `36px` | `40px` | `44px` | `48px` |\n *\n * @default md\n */\n \"size\"?: ControlSize\n /**\n * Controls gutter on the edges of the button, defaults to value from `size`.\n *\n * | 2xs | xs | sm | md | lg | xl |\n * | ------ | ------ | ------ | ------ | ------ | ------ |\n * | `6px` | `8px` | `10px` | `12px` | `14px` | `16px` |\n */\n \"gutterSize\"?: Sizes<\"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\">\n /** Disable the entire group */\n \"disabled\"?: boolean\n /**\n * Display the control as a block element with equal width segments\n * @default false\n */\n \"block\"?: boolean\n /**\n * Determines if the tabs should be a fully rounded pill shape.\n * Only applies to the `\"segmented\"` variant.\n * @default true\n */\n \"pill\"?: boolean\n /**\n * Flush underline style — removes tab padding so the indicator\n * matches the text width exactly, uses gap for spacing, and\n * removes the bottom border.\n * Only applies to the `\"underline\"` variant.\n * @default false\n */\n \"flush\"?: boolean\n \"className\"?: string\n \"children\": React.ReactNode\n}\n\nexport const Tabs = <T extends string>({\n value,\n onChange,\n children,\n variant = \"segmented\",\n orientation = \"horizontal\",\n block,\n pill = true,\n flush,\n size = \"md\",\n gutterSize,\n className,\n onClick,\n ...restProps\n}: TabsProps<T>) => {\n const rootRef = useRef<HTMLDivElement>(null)\n const thumbRef = useRef<HTMLDivElement>(null)\n const prevSizeRef = useRef(size)\n const isVertical = orientation === \"vertical\"\n\n const applyThumbSizing = useCallback(\n (attemptScroll: boolean) => {\n const root = rootRef.current\n const thumb = thumbRef.current\n\n if (!root || !thumb) {\n return\n }\n\n // Get selected node\n const activeNode = root?.querySelector<HTMLDivElement>('[data-state=\"on\"]')\n\n // Impossible\n if (!activeNode) {\n return\n }\n\n if (isVertical) {\n const rootHeight = root.clientHeight\n let targetHeight = Math.floor(activeNode.clientHeight)\n const targetOffset = activeNode.offsetTop\n\n // Detect subpixel edge case\n if (rootHeight - (targetHeight + targetOffset) < 2) {\n targetHeight = targetHeight - 1\n }\n\n thumb.style.height = `${Math.floor(targetHeight)}px`\n thumb.style.width = \"\"\n thumb.style.transform = `translateY(${targetOffset}px)`\n\n // Scroll into view if needed\n if (root.scrollHeight > rootHeight) {\n const buffer = rootHeight * 0.15\n const scrollTop = root.scrollTop\n const top = activeNode.offsetTop\n const bottom = top + targetHeight\n if (top < scrollTop + buffer || bottom > scrollTop + rootHeight - buffer) {\n if (attemptScroll) {\n activeNode.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: \"smooth\" })\n }\n }\n }\n } else {\n const rootWidth = root.clientWidth\n let targetWidth = Math.floor(activeNode.clientWidth)\n const targetOffset = activeNode.offsetLeft\n\n // Detect if the thumb is moving too far to the edge of the container.\n // This would most commonly be due to subpixel widths adding up to excessive distance.\n if (rootWidth - (targetWidth + targetOffset) < 2) {\n targetWidth = targetWidth - 1\n }\n\n thumb.style.width = `${Math.floor(targetWidth)}px`\n thumb.style.height = \"\"\n thumb.style.transform = `translateX(${targetOffset}px)`\n\n // If the control is scrollable, ensure the active option is visible\n if (root.scrollWidth > rootWidth) {\n // Only scroll items near the edge, but not the inner 2/3.\n const buffer = rootWidth * 0.15\n const scrollLeft = root.scrollLeft\n const left = activeNode.offsetLeft\n const right = left + targetWidth\n if (left < scrollLeft + buffer || right > scrollLeft + rootWidth - buffer) {\n // Cheap trick to avoid unintentional scroll on mount - transition is set after mounting\n if (attemptScroll) {\n activeNode.scrollIntoView({ block: \"nearest\", inline: \"center\", behavior: \"smooth\" })\n }\n }\n }\n }\n },\n [isVertical],\n )\n\n useResizeObserver({\n // @ts-expect-error(2322) -- bug in types: https://github.com/juliencrn/usehooks-ts/issues/663\n ref: rootRef,\n onResize: () => {\n const thumb = thumbRef.current\n\n if (!thumb) {\n return\n }\n\n // Perform the size update instantly\n const currentTransition = thumb.style.transition\n thumb.style.transition = \"\"\n applyThumbSizing(false)\n thumb.style.transition = currentTransition\n },\n })\n\n const transitionProperty = isVertical\n ? \"height 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)\"\n : \"width 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)\"\n\n useLayoutEffect(() => {\n const root = rootRef.current\n const thumb = thumbRef.current\n\n if (!root || !thumb) {\n return\n }\n\n const sizeChanged = prevSizeRef.current !== size\n prevSizeRef.current = size\n\n if (sizeChanged) {\n // Size changed - disable transition, wait for CSS, then apply sizing\n const currentTransition = thumb.style.transition\n thumb.style.transition = \"\"\n\n waitForAnimationFrame(() => {\n applyThumbSizing(false)\n waitForAnimationFrame(() => {\n thumb.style.transition = currentTransition\n })\n })\n } else {\n // Normal update (value change, etc.)\n waitForAnimationFrame(() => {\n applyThumbSizing(!!thumb.style.transition)\n\n // Apply transition after initial calculation is set\n if (!thumb.style.transition) {\n waitForAnimationFrame(() => {\n thumb.style.transition = transitionProperty\n })\n }\n })\n }\n }, [applyThumbSizing, value, size, gutterSize, pill, flush, transitionProperty])\n\n const handleValueChange = (nextValue: T) => {\n // Only trigger onChange when a value exists\n // Disallow toggling off enabled items\n if (nextValue && onChange) onChange(nextValue)\n }\n\n // Only apply pill for segmented variant\n const isPill = variant === \"segmented\" && pill\n // Only apply flush for underline variant\n const isFlush = variant === \"underline\" && flush\n\n return (\n <ToggleGroup.Root\n ref={rootRef}\n className={clsx(s.Tabs, className)}\n type=\"single\"\n value={value}\n loop={false}\n onValueChange={handleValueChange}\n onClick={onClick}\n data-variant={variant}\n data-orientation={orientation}\n data-block={block ? \"\" : undefined}\n data-pill={isPill ? \"\" : undefined}\n data-flush={isFlush ? \"\" : undefined}\n data-size={size}\n data-gutter-size={gutterSize}\n {...restProps}\n >\n <div className={s.TabsThumb} ref={thumbRef} />\n {children}\n </ToggleGroup.Root>\n )\n}\n\n/**\n * Badge configuration for Tabs.Tab\n */\nexport type TabsBadgeProp =\n | React.ReactNode\n | {\n content: React.ReactNode\n color?: SemanticColors<\n \"secondary\" | \"success\" | \"danger\" | \"warning\" | \"info\" | \"discovery\" | \"caution\"\n >\n variant?: Variants<\"soft\" | \"solid\">\n pill?: boolean\n loading?: boolean\n }\n\nexport type TabProps = {\n /**\n * Tab value\n */\n \"value\": string\n /**\n * Text read aloud to screen readers when the tab is focused\n */\n \"aria-label\"?: string\n /**\n * Text content to render in the tab\n */\n \"children\"?: React.ReactNode\n /**\n * Icon to render before the text content\n */\n \"icon\"?: React.ReactNode\n /**\n * Badge to render after the text content.\n * Can be a simple value or an object with content, color, variant, and loading options.\n * @example badge={5}\n * @example badge={{ content: 5, color: \"danger\" }}\n */\n \"badge\"?: TabsBadgeProp\n /**\n * Disable the individual tab\n */\n \"disabled\"?: boolean\n}\n\n// Type guard for badge object form\nconst isBadgeObject = (\n badge: TabsBadgeProp,\n): badge is Exclude<TabsBadgeProp, React.ReactNode> & { content: React.ReactNode } => {\n return badge != null && typeof badge === \"object\" && \"content\" in badge\n}\n\nconst Tab = ({ children, icon, badge, ...restProps }: TabProps) => {\n // Normalize badge prop\n const badgeProps = badge != null ? (isBadgeObject(badge) ? badge : { content: badge }) : null\n\n return (\n <ToggleGroup.Item\n className={s.Tab}\n {...restProps}\n onPointerEnter={handlePressableMouseEnter}\n >\n <span className={s.TabContent}>\n {icon}\n {children && <span>{children}</span>}\n {badgeProps && (\n <span\n className={s.TabBadge}\n data-color={badgeProps.color ?? \"secondary\"}\n data-variant={badgeProps.variant ?? \"soft\"}\n data-pill={badgeProps.pill ? \"\" : undefined}\n >\n {badgeProps.loading ? <LoadingIndicator /> : badgeProps.content}\n </span>\n )}\n </span>\n </ToggleGroup.Item>\n )\n}\n\n// Attach sub-components\nTabs.Tab = Tab\n// Backward-compat alias\nTabs.Option = Tab\n"]}
|
|
@@ -32,6 +32,17 @@
|
|
|
32
32
|
background: var(--segmented-control-background);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
.Tabs:where([data-variant="segmented"]:not([data-pill])) {
|
|
36
|
+
--segmented-control-option-radius: calc(
|
|
37
|
+
var(--segmented-control-radius) - var(--segmented-control-gutter)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.Tabs:where([data-variant="segmented"][data-pill]) {
|
|
42
|
+
border-radius: var(--radius-full);
|
|
43
|
+
--segmented-control-option-radius: var(--radius-full);
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
.Tabs:where([data-variant="segmented"][data-block]) {
|
|
36
47
|
overflow: hidden;
|
|
37
48
|
display: flex;
|
|
@@ -39,15 +50,12 @@
|
|
|
39
50
|
white-space: normal;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
.Tabs:where([data-variant="segmented"][data-pill]) {
|
|
43
|
-
--segmented-control-radius: var(--radius-full);
|
|
44
|
-
--segmented-control-option-radius: var(--radius-full);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
53
|
/* =============================================
|
|
48
54
|
Variant: Underline
|
|
49
55
|
============================================= */
|
|
50
56
|
.Tabs:where([data-variant="underline"]) {
|
|
57
|
+
--segmented-control-option-radius: 0;
|
|
58
|
+
|
|
51
59
|
overflow: auto;
|
|
52
60
|
height: var(--segmented-control-size);
|
|
53
61
|
padding: 0;
|
|
@@ -64,6 +72,18 @@
|
|
|
64
72
|
white-space: normal;
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
/* =============================================
|
|
76
|
+
Variant: Underline flush
|
|
77
|
+
============================================= */
|
|
78
|
+
.Tabs:where([data-variant="underline"][data-flush]) {
|
|
79
|
+
gap: var(--segmented-control-option-gutter);
|
|
80
|
+
border-bottom: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.Tabs:where([data-variant="underline"][data-flush][data-block]) {
|
|
84
|
+
gap: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
/* =============================================
|
|
68
88
|
Vertical orientation
|
|
69
89
|
============================================= */
|
|
@@ -91,6 +111,11 @@
|
|
|
91
111
|
width: 100%;
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
.Tabs:where([data-orientation="vertical"][data-variant="segmented"][data-pill]) {
|
|
115
|
+
border-radius: calc(var(--segmented-control-size) / 2);
|
|
116
|
+
--segmented-control-option-radius: calc(var(--segmented-control-size) / 2);
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
/* =============================================
|
|
95
120
|
Sizes
|
|
96
121
|
============================================= */
|
|
@@ -291,14 +316,6 @@
|
|
|
291
316
|
.Tabs:where([data-gutter-size="xl"]) {
|
|
292
317
|
--segmented-control-option-gutter: var(--control-gutter-xl);
|
|
293
318
|
}/* =============================================
|
|
294
|
-
Option radius (computed from parent for segmented)
|
|
295
|
-
============================================= */.Tabs:where([data-variant="segmented"]) {
|
|
296
|
-
--segmented-control-option-radius: calc(
|
|
297
|
-
var(--segmented-control-radius) - var(--segmented-control-gutter)
|
|
298
|
-
);
|
|
299
|
-
}.Tabs:where([data-variant="underline"]) {
|
|
300
|
-
--segmented-control-option-radius: 0;
|
|
301
|
-
}/* =============================================
|
|
302
319
|
Tab (trigger item)
|
|
303
320
|
============================================= */.Tab {
|
|
304
321
|
position: relative;
|
|
@@ -393,12 +410,18 @@
|
|
|
393
410
|
outline-offset: -2px;
|
|
394
411
|
}
|
|
395
412
|
|
|
413
|
+
/* Flush underline variant — Tab styles */
|
|
414
|
+
:where(.Tabs[data-variant="underline"][data-flush]) .Tab {
|
|
415
|
+
padding: 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
396
418
|
/* =============================================
|
|
397
419
|
Vertical orientation — Tab styles
|
|
398
420
|
============================================= */
|
|
399
421
|
:where(.Tabs[data-orientation="vertical"]) .Tab {
|
|
400
422
|
justify-content: flex-start;
|
|
401
423
|
width: 100%;
|
|
424
|
+
height: var(--segmented-control-size);
|
|
402
425
|
}
|
|
403
426
|
|
|
404
427
|
:where(.Tabs[data-orientation="vertical"][data-block]) .Tab {
|
|
@@ -473,6 +496,18 @@
|
|
|
473
496
|
border-radius: 0 calc(var(--tabs-underline-indicator-height) / 2) calc(var(--tabs-underline-indicator-height) / 2) 0;
|
|
474
497
|
background: var(--tabs-underline-indicator-color);
|
|
475
498
|
box-shadow: none;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Flush underline horizontal: 1px line, no radius */
|
|
502
|
+
:where(.Tabs[data-variant="underline"][data-flush][data-orientation="horizontal"]) .TabsThumb {
|
|
503
|
+
height: 1px;
|
|
504
|
+
border-radius: 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* Flush underline vertical: 1px line, no radius */
|
|
508
|
+
:where(.Tabs[data-variant="underline"][data-flush][data-orientation="vertical"]) .TabsThumb {
|
|
509
|
+
width: 1px;
|
|
510
|
+
border-radius: 0;
|
|
476
511
|
}/* =============================================
|
|
477
512
|
Tab Content (icon + text + badge layout)
|
|
478
513
|
============================================= */.TabContent {
|
|
@@ -58,11 +58,19 @@ export type TabsProps<T extends string> = {
|
|
|
58
58
|
* @default true
|
|
59
59
|
*/
|
|
60
60
|
"pill"?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Flush underline style — removes tab padding so the indicator
|
|
63
|
+
* matches the text width exactly, uses gap for spacing, and
|
|
64
|
+
* removes the bottom border.
|
|
65
|
+
* Only applies to the `"underline"` variant.
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
"flush"?: boolean;
|
|
61
69
|
"className"?: string;
|
|
62
70
|
"children": React.ReactNode;
|
|
63
71
|
};
|
|
64
72
|
export declare const Tabs: {
|
|
65
|
-
<T extends string>({ value, onChange, children, variant, orientation, block, pill, size, gutterSize, className, onClick, ...restProps }: TabsProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
73
|
+
<T extends string>({ value, onChange, children, variant, orientation, block, pill, flush, size, gutterSize, className, onClick, ...restProps }: TabsProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
66
74
|
Tab: ({ children, icon, badge, ...restProps }: TabProps) => import("react/jsx-runtime").JSX.Element;
|
|
67
75
|
Option: ({ children, icon, badge, ...restProps }: TabProps) => import("react/jsx-runtime").JSX.Element;
|
|
68
76
|
};
|