@plexui/ui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/components/SegmentedControl/index.js +1 -1
- package/dist/es/components/SegmentedControl/index.js.map +1 -1
- package/dist/es/components/Tabs/Tabs.js +153 -0
- package/dist/es/components/Tabs/Tabs.js.map +1 -0
- package/dist/es/components/{SegmentedControl/SegmentedControl.module.css → Tabs/Tabs.module.css} +232 -98
- package/dist/es/components/Tabs/index.js +2 -0
- package/dist/es/components/Tabs/index.js.map +1 -0
- package/dist/es/styles/variables-components.css +7 -0
- package/dist/types/components/SegmentedControl/index.d.ts +2 -2
- package/dist/types/components/{SegmentedControl/SegmentedControl.d.ts → Tabs/Tabs.d.ts} +30 -14
- package/dist/types/components/Tabs/index.d.ts +2 -0
- package/package.json +1 -1
- package/dist/es/components/SegmentedControl/SegmentedControl.js +0 -118
- package/dist/es/components/SegmentedControl/SegmentedControl.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { SegmentedControl } from "
|
|
1
|
+
export { Tabs as SegmentedControl } from "../Tabs";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/SegmentedControl/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/SegmentedControl/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,gBAAgB,EAAE,MAAM,SAAS,CAAA","sourcesContent":["export { Tabs as SegmentedControl } from \"../Tabs\"\nexport type {\n TabsBadgeProp as SegmentedControlBadgeProp,\n TabProps as SegmentedControlOptionProps,\n TabsProps as SegmentedControlProps,\n SizeVariant,\n} from \"../Tabs\"\n"]}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { ToggleGroup } from "radix-ui";
|
|
5
|
+
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
6
|
+
import { useResizeObserver } from "usehooks-ts";
|
|
7
|
+
import { handlePressableMouseEnter, waitForAnimationFrame } from "../../lib/helpers";
|
|
8
|
+
import {} from "../../types";
|
|
9
|
+
import { LoadingIndicator } from "../Indicator";
|
|
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 }) => {
|
|
12
|
+
const rootRef = useRef(null);
|
|
13
|
+
const thumbRef = useRef(null);
|
|
14
|
+
const prevSizeRef = useRef(size);
|
|
15
|
+
const isVertical = orientation === "vertical";
|
|
16
|
+
const applyThumbSizing = useCallback((attemptScroll) => {
|
|
17
|
+
const root = rootRef.current;
|
|
18
|
+
const thumb = thumbRef.current;
|
|
19
|
+
if (!root || !thumb) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Get selected node
|
|
23
|
+
const activeNode = root?.querySelector('[data-state="on"]');
|
|
24
|
+
// Impossible
|
|
25
|
+
if (!activeNode) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (isVertical) {
|
|
29
|
+
const rootHeight = root.clientHeight;
|
|
30
|
+
let targetHeight = Math.floor(activeNode.clientHeight);
|
|
31
|
+
const targetOffset = activeNode.offsetTop;
|
|
32
|
+
// Detect subpixel edge case
|
|
33
|
+
if (rootHeight - (targetHeight + targetOffset) < 2) {
|
|
34
|
+
targetHeight = targetHeight - 1;
|
|
35
|
+
}
|
|
36
|
+
thumb.style.height = `${Math.floor(targetHeight)}px`;
|
|
37
|
+
thumb.style.width = "";
|
|
38
|
+
thumb.style.transform = `translateY(${targetOffset}px)`;
|
|
39
|
+
// Scroll into view if needed
|
|
40
|
+
if (root.scrollHeight > rootHeight) {
|
|
41
|
+
const buffer = rootHeight * 0.15;
|
|
42
|
+
const scrollTop = root.scrollTop;
|
|
43
|
+
const top = activeNode.offsetTop;
|
|
44
|
+
const bottom = top + targetHeight;
|
|
45
|
+
if (top < scrollTop + buffer || bottom > scrollTop + rootHeight - buffer) {
|
|
46
|
+
if (attemptScroll) {
|
|
47
|
+
activeNode.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const rootWidth = root.clientWidth;
|
|
54
|
+
let targetWidth = Math.floor(activeNode.clientWidth);
|
|
55
|
+
const targetOffset = activeNode.offsetLeft;
|
|
56
|
+
// Detect if the thumb is moving too far to the edge of the container.
|
|
57
|
+
// This would most commonly be due to subpixel widths adding up to excessive distance.
|
|
58
|
+
if (rootWidth - (targetWidth + targetOffset) < 2) {
|
|
59
|
+
targetWidth = targetWidth - 1;
|
|
60
|
+
}
|
|
61
|
+
thumb.style.width = `${Math.floor(targetWidth)}px`;
|
|
62
|
+
thumb.style.height = "";
|
|
63
|
+
thumb.style.transform = `translateX(${targetOffset}px)`;
|
|
64
|
+
// If the control is scrollable, ensure the active option is visible
|
|
65
|
+
if (root.scrollWidth > rootWidth) {
|
|
66
|
+
// Only scroll items near the edge, but not the inner 2/3.
|
|
67
|
+
const buffer = rootWidth * 0.15;
|
|
68
|
+
const scrollLeft = root.scrollLeft;
|
|
69
|
+
const left = activeNode.offsetLeft;
|
|
70
|
+
const right = left + targetWidth;
|
|
71
|
+
if (left < scrollLeft + buffer || right > scrollLeft + rootWidth - buffer) {
|
|
72
|
+
// Cheap trick to avoid unintentional scroll on mount - transition is set after mounting
|
|
73
|
+
if (attemptScroll) {
|
|
74
|
+
activeNode.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, [isVertical]);
|
|
80
|
+
useResizeObserver({
|
|
81
|
+
// @ts-expect-error(2322) -- bug in types: https://github.com/juliencrn/usehooks-ts/issues/663
|
|
82
|
+
ref: rootRef,
|
|
83
|
+
onResize: () => {
|
|
84
|
+
const thumb = thumbRef.current;
|
|
85
|
+
if (!thumb) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Perform the size update instantly
|
|
89
|
+
const currentTransition = thumb.style.transition;
|
|
90
|
+
thumb.style.transition = "";
|
|
91
|
+
applyThumbSizing(false);
|
|
92
|
+
thumb.style.transition = currentTransition;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const transitionProperty = isVertical
|
|
96
|
+
? "height 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)"
|
|
97
|
+
: "width 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)";
|
|
98
|
+
useLayoutEffect(() => {
|
|
99
|
+
const root = rootRef.current;
|
|
100
|
+
const thumb = thumbRef.current;
|
|
101
|
+
if (!root || !thumb) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const sizeChanged = prevSizeRef.current !== size;
|
|
105
|
+
prevSizeRef.current = size;
|
|
106
|
+
if (sizeChanged) {
|
|
107
|
+
// Size changed - disable transition, wait for CSS, then apply sizing
|
|
108
|
+
const currentTransition = thumb.style.transition;
|
|
109
|
+
thumb.style.transition = "";
|
|
110
|
+
waitForAnimationFrame(() => {
|
|
111
|
+
applyThumbSizing(false);
|
|
112
|
+
waitForAnimationFrame(() => {
|
|
113
|
+
thumb.style.transition = currentTransition;
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Normal update (value change, etc.)
|
|
119
|
+
waitForAnimationFrame(() => {
|
|
120
|
+
applyThumbSizing(!!thumb.style.transition);
|
|
121
|
+
// Apply transition after initial calculation is set
|
|
122
|
+
if (!thumb.style.transition) {
|
|
123
|
+
waitForAnimationFrame(() => {
|
|
124
|
+
thumb.style.transition = transitionProperty;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}, [applyThumbSizing, value, size, gutterSize, pill, transitionProperty]);
|
|
130
|
+
const handleValueChange = (nextValue) => {
|
|
131
|
+
// Only trigger onChange when a value exists
|
|
132
|
+
// Disallow toggling off enabled items
|
|
133
|
+
if (nextValue && onChange)
|
|
134
|
+
onChange(nextValue);
|
|
135
|
+
};
|
|
136
|
+
// Only apply pill for segmented variant
|
|
137
|
+
const isPill = variant === "segmented" && pill;
|
|
138
|
+
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-size": size, "data-gutter-size": gutterSize, ...restProps, children: [_jsx("div", { className: s.TabsThumb, ref: thumbRef }), children] }));
|
|
139
|
+
};
|
|
140
|
+
// Type guard for badge object form
|
|
141
|
+
const isBadgeObject = (badge) => {
|
|
142
|
+
return badge != null && typeof badge === "object" && "content" in badge;
|
|
143
|
+
};
|
|
144
|
+
const Tab = ({ children, icon, badge, ...restProps }) => {
|
|
145
|
+
// Normalize badge prop
|
|
146
|
+
const badgeProps = badge != null ? (isBadgeObject(badge) ? badge : { content: badge }) : null;
|
|
147
|
+
return (_jsx(ToggleGroup.Item, { className: s.Tab, ...restProps, onPointerEnter: handlePressableMouseEnter, children: _jsxs("span", { className: s.TabContent, children: [icon, children && _jsx("span", { children: children }), badgeProps && (_jsx("span", { className: s.TabBadge, "data-color": badgeProps.color ?? "secondary", "data-variant": badgeProps.variant ?? "soft", "data-pill": badgeProps.pill ? "" : undefined, children: badgeProps.loading ? _jsx(LoadingIndicator, {}) : badgeProps.content }))] }) }));
|
|
148
|
+
};
|
|
149
|
+
// Attach sub-components
|
|
150
|
+
Tabs.Tab = Tab;
|
|
151
|
+
// Backward-compat alias
|
|
152
|
+
Tabs.Option = Tab;
|
|
153
|
+
//# sourceMappingURL=Tabs.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/es/components/{SegmentedControl/SegmentedControl.module.css → Tabs/Tabs.module.css}
RENAMED
|
@@ -1,47 +1,100 @@
|
|
|
1
|
-
@layer components {.
|
|
2
|
-
--segmented-control-option-radius: calc(
|
|
3
|
-
var(--segmented-control-radius) - var(--segmented-control-gutter)
|
|
4
|
-
);
|
|
5
|
-
|
|
1
|
+
@layer components {.Tabs {
|
|
6
2
|
position: relative;
|
|
7
|
-
overflow: auto;
|
|
8
3
|
display: inline-flex;
|
|
9
4
|
flex-wrap: nowrap;
|
|
10
5
|
gap: var(--segmented-control-gap);
|
|
11
|
-
height: var(--segmented-control-size);
|
|
12
|
-
padding: var(--segmented-control-gutter);
|
|
13
|
-
border-radius: var(--segmented-control-radius);
|
|
14
|
-
background: var(--segmented-control-background);
|
|
15
6
|
font-size: var(--segmented-control-font-size);
|
|
16
7
|
font-weight: var(--segmented-control-font-weight);
|
|
17
8
|
-ms-overflow-style: none;
|
|
18
9
|
scrollbar-width: none;
|
|
19
|
-
/* Remove inline-flex baseline gap */
|
|
20
10
|
vertical-align: middle;
|
|
21
11
|
white-space: nowrap;
|
|
22
12
|
}
|
|
23
13
|
|
|
24
|
-
.
|
|
14
|
+
.Tabs::-webkit-scrollbar {
|
|
25
15
|
width: 0;
|
|
26
16
|
height: 0;
|
|
27
17
|
}
|
|
28
18
|
|
|
29
|
-
.
|
|
30
|
-
.
|
|
19
|
+
.Tabs::-webkit-scrollbar-track,
|
|
20
|
+
.Tabs::-webkit-scrollbar-thumb {
|
|
31
21
|
background: transparent;
|
|
32
22
|
}
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
/* =============================================
|
|
25
|
+
Variant: Segmented (default — existing behavior)
|
|
26
|
+
============================================= */
|
|
27
|
+
.Tabs:where([data-variant="segmented"]) {
|
|
28
|
+
overflow: auto;
|
|
29
|
+
height: var(--segmented-control-size);
|
|
30
|
+
padding: var(--segmented-control-gutter);
|
|
31
|
+
border-radius: var(--segmented-control-radius);
|
|
32
|
+
background: var(--segmented-control-background);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.Tabs:where([data-variant="segmented"][data-block]) {
|
|
35
36
|
overflow: hidden;
|
|
36
37
|
display: flex;
|
|
37
38
|
width: 100%;
|
|
38
39
|
white-space: normal;
|
|
39
40
|
}
|
|
40
41
|
|
|
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
|
+
/* =============================================
|
|
48
|
+
Variant: Underline
|
|
49
|
+
============================================= */
|
|
50
|
+
.Tabs:where([data-variant="underline"]) {
|
|
51
|
+
overflow: auto;
|
|
52
|
+
height: var(--segmented-control-size);
|
|
53
|
+
padding: 0;
|
|
54
|
+
border-radius: 0;
|
|
55
|
+
background: transparent;
|
|
56
|
+
gap: 0;
|
|
57
|
+
border-bottom: 1px solid var(--tabs-underline-border-color);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.Tabs:where([data-variant="underline"][data-block]) {
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
display: flex;
|
|
63
|
+
width: 100%;
|
|
64
|
+
white-space: normal;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* =============================================
|
|
68
|
+
Vertical orientation
|
|
69
|
+
============================================= */
|
|
70
|
+
.Tabs:where([data-orientation="vertical"]) {
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
height: auto;
|
|
73
|
+
width: -moz-fit-content;
|
|
74
|
+
width: fit-content;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.Tabs:where([data-orientation="vertical"][data-variant="segmented"]) {
|
|
79
|
+
overflow-y: auto;
|
|
80
|
+
overflow-x: hidden;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.Tabs:where([data-orientation="vertical"][data-variant="underline"]) {
|
|
84
|
+
overflow-y: auto;
|
|
85
|
+
overflow-x: hidden;
|
|
86
|
+
border-bottom: none;
|
|
87
|
+
border-left: 1px solid var(--tabs-underline-border-color);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.Tabs:where([data-orientation="vertical"][data-block]) {
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
93
|
+
|
|
41
94
|
/* =============================================
|
|
42
95
|
Sizes
|
|
43
96
|
============================================= */
|
|
44
|
-
.
|
|
97
|
+
.Tabs:where([data-size="3xs"]) {
|
|
45
98
|
--segmented-control-size: var(--control-size-3xs);
|
|
46
99
|
--segmented-control-font-size: var(--control-font-size-sm);
|
|
47
100
|
--segmented-control-radius: var(--control-radius-sm);
|
|
@@ -60,7 +113,7 @@
|
|
|
60
113
|
--option-badge-radius: var(--badge-radius-2xs);
|
|
61
114
|
}
|
|
62
115
|
|
|
63
|
-
.
|
|
116
|
+
.Tabs:where([data-size="2xs"]) {
|
|
64
117
|
--segmented-control-size: var(--control-size-2xs);
|
|
65
118
|
--segmented-control-font-size: var(--control-font-size-sm);
|
|
66
119
|
--segmented-control-radius: var(--control-radius-sm);
|
|
@@ -79,7 +132,7 @@
|
|
|
79
132
|
--option-badge-radius: var(--badge-radius-2xs);
|
|
80
133
|
}
|
|
81
134
|
|
|
82
|
-
.
|
|
135
|
+
.Tabs:where([data-size="xs"]) {
|
|
83
136
|
--segmented-control-size: var(--control-size-xs);
|
|
84
137
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
85
138
|
--segmented-control-radius: var(--control-radius-sm);
|
|
@@ -98,7 +151,7 @@
|
|
|
98
151
|
--option-badge-radius: var(--badge-radius-xs);
|
|
99
152
|
}
|
|
100
153
|
|
|
101
|
-
.
|
|
154
|
+
.Tabs:where([data-size="sm"]) {
|
|
102
155
|
--segmented-control-size: var(--control-size-sm);
|
|
103
156
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
104
157
|
--segmented-control-radius: var(--control-radius-md);
|
|
@@ -117,7 +170,7 @@
|
|
|
117
170
|
--option-badge-radius: var(--badge-radius-sm);
|
|
118
171
|
}
|
|
119
172
|
|
|
120
|
-
.
|
|
173
|
+
.Tabs:where([data-size="md"]) {
|
|
121
174
|
--segmented-control-size: var(--control-size-md);
|
|
122
175
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
123
176
|
--segmented-control-radius: var(--control-radius-md);
|
|
@@ -136,7 +189,7 @@
|
|
|
136
189
|
--option-badge-radius: var(--badge-radius-md);
|
|
137
190
|
}
|
|
138
191
|
|
|
139
|
-
.
|
|
192
|
+
.Tabs:where([data-size="lg"]) {
|
|
140
193
|
--segmented-control-size: var(--control-size-lg);
|
|
141
194
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
142
195
|
--segmented-control-radius: var(--control-radius-md);
|
|
@@ -155,7 +208,7 @@
|
|
|
155
208
|
--option-badge-radius: var(--badge-radius-md);
|
|
156
209
|
}
|
|
157
210
|
|
|
158
|
-
.
|
|
211
|
+
.Tabs:where([data-size="xl"]) {
|
|
159
212
|
--segmented-control-size: var(--control-size-xl);
|
|
160
213
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
161
214
|
--segmented-control-radius: var(--control-radius-lg);
|
|
@@ -174,7 +227,7 @@
|
|
|
174
227
|
--option-badge-radius: var(--badge-radius-md);
|
|
175
228
|
}
|
|
176
229
|
|
|
177
|
-
.
|
|
230
|
+
.Tabs:where([data-size="2xl"]) {
|
|
178
231
|
--segmented-control-size: var(--control-size-2xl);
|
|
179
232
|
--segmented-control-font-size: var(--control-font-size-lg);
|
|
180
233
|
--segmented-control-radius: var(--control-radius-xl);
|
|
@@ -193,7 +246,7 @@
|
|
|
193
246
|
--option-badge-radius: var(--badge-radius-lg);
|
|
194
247
|
}
|
|
195
248
|
|
|
196
|
-
.
|
|
249
|
+
.Tabs:where([data-size="3xl"]) {
|
|
197
250
|
--segmented-control-size: var(--control-size-3xl);
|
|
198
251
|
--segmented-control-font-size: var(--control-font-size-lg);
|
|
199
252
|
--segmented-control-radius: var(--control-radius-xl);
|
|
@@ -213,42 +266,43 @@
|
|
|
213
266
|
}
|
|
214
267
|
|
|
215
268
|
/* =============================================
|
|
216
|
-
Gutter sizes
|
|
269
|
+
Gutter sizes (segmented variant only)
|
|
217
270
|
============================================= */
|
|
218
|
-
.
|
|
271
|
+
.Tabs:where([data-gutter-size="2xs"]) {
|
|
219
272
|
--segmented-control-option-gutter: var(--control-gutter-2xs);
|
|
220
273
|
}
|
|
221
274
|
|
|
222
|
-
.
|
|
275
|
+
.Tabs:where([data-gutter-size="xs"]) {
|
|
223
276
|
--segmented-control-option-gutter: var(--control-gutter-xs);
|
|
224
277
|
}
|
|
225
278
|
|
|
226
|
-
.
|
|
279
|
+
.Tabs:where([data-gutter-size="sm"]) {
|
|
227
280
|
--segmented-control-option-gutter: var(--control-gutter-sm);
|
|
228
281
|
}
|
|
229
282
|
|
|
230
|
-
.
|
|
283
|
+
.Tabs:where([data-gutter-size="md"]) {
|
|
231
284
|
--segmented-control-option-gutter: var(--control-gutter-md);
|
|
232
285
|
}
|
|
233
286
|
|
|
234
|
-
.
|
|
287
|
+
.Tabs:where([data-gutter-size="lg"]) {
|
|
235
288
|
--segmented-control-option-gutter: var(--control-gutter-lg);
|
|
236
289
|
}
|
|
237
290
|
|
|
238
|
-
.
|
|
291
|
+
.Tabs:where([data-gutter-size="xl"]) {
|
|
239
292
|
--segmented-control-option-gutter: var(--control-gutter-xl);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
293
|
+
}/* =============================================
|
|
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
|
+
Tab (trigger item)
|
|
303
|
+
============================================= */.Tab {
|
|
249
304
|
position: relative;
|
|
250
305
|
padding: 0 var(--segmented-control-option-gutter);
|
|
251
|
-
border-radius: var(--segmented-control-option-radius);
|
|
252
306
|
color: var(--color-text-secondary);
|
|
253
307
|
cursor: pointer;
|
|
254
308
|
line-height: 1;
|
|
@@ -261,24 +315,28 @@
|
|
|
261
315
|
transition-timing-function: var(--transition-ease-basic);
|
|
262
316
|
}
|
|
263
317
|
|
|
264
|
-
.
|
|
318
|
+
.Tab:focus {
|
|
265
319
|
outline: 0;
|
|
266
320
|
}
|
|
267
321
|
|
|
268
|
-
|
|
322
|
+
/* =============================================
|
|
323
|
+
Segmented variant — Tab styles
|
|
324
|
+
============================================= */
|
|
325
|
+
:where(.Tabs[data-variant="segmented"]) .Tab {
|
|
326
|
+
border-radius: var(--segmented-control-option-radius);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
:where(.Tabs[data-variant="segmented"][data-block]) .Tab {
|
|
269
330
|
flex: 1;
|
|
270
331
|
}
|
|
271
332
|
|
|
272
|
-
:where(.
|
|
333
|
+
:where(.Tabs[data-variant="segmented"][data-pill]) .Tab {
|
|
273
334
|
min-width: calc(var(--segmented-control-size) - 2 * var(--segmented-control-gutter));
|
|
274
335
|
padding: 0 calc(var(--segmented-control-option-gutter) * var(--control-gutter-pill-scaling));
|
|
275
336
|
}
|
|
276
337
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.SegmentedControlOption::before {
|
|
338
|
+
/* Hover highlight pseudo-element (segmented only) */
|
|
339
|
+
:where(.Tabs[data-variant="segmented"]) .Tab::before {
|
|
282
340
|
position: absolute;
|
|
283
341
|
inset: var(--segmented-control-option-highlight-gutter);
|
|
284
342
|
border-radius: var(--segmented-control-option-radius);
|
|
@@ -293,55 +351,131 @@
|
|
|
293
351
|
will-change: transform;
|
|
294
352
|
}
|
|
295
353
|
|
|
296
|
-
.
|
|
354
|
+
:where(.Tabs[data-variant="segmented"]) .Tab:active::before {
|
|
297
355
|
transform: scale(var(--scale), 0.97);
|
|
298
356
|
}
|
|
299
|
-
|
|
300
|
-
.SegmentedControlOption svg {
|
|
301
|
-
display: block;
|
|
302
|
-
}
|
|
303
|
-
@media (hover: hover) and (pointer: fine) {.SegmentedControlOption[data-state="off"]:where(:not([disabled])):hover {
|
|
357
|
+
@media (hover: hover) and (pointer: fine) {:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):hover {
|
|
304
358
|
color: var(--color-text);
|
|
305
359
|
}
|
|
306
360
|
|
|
307
|
-
.
|
|
361
|
+
:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):hover::before {
|
|
308
362
|
opacity: 0.5;
|
|
309
363
|
}
|
|
310
364
|
}
|
|
311
365
|
|
|
312
|
-
.
|
|
366
|
+
:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):focus-visible {
|
|
313
367
|
color: var(--color-text);
|
|
314
368
|
outline: 2px solid var(--color-ring);
|
|
315
369
|
}
|
|
316
370
|
|
|
317
|
-
.
|
|
371
|
+
:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):active::before {
|
|
318
372
|
opacity: 0.75;
|
|
319
373
|
}
|
|
320
374
|
|
|
321
|
-
|
|
375
|
+
/* =============================================
|
|
376
|
+
Underline variant — Tab styles
|
|
377
|
+
============================================= */
|
|
378
|
+
:where(.Tabs[data-variant="underline"]) .Tab {
|
|
379
|
+
border-radius: 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
:where(.Tabs[data-variant="underline"][data-block]) .Tab {
|
|
383
|
+
flex: 1;
|
|
384
|
+
}
|
|
385
|
+
@media (hover: hover) and (pointer: fine) {:where(.Tabs[data-variant="underline"]) .Tab[data-state="off"]:where(:not([disabled])):hover {
|
|
386
|
+
color: var(--color-text);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
:where(.Tabs[data-variant="underline"]) .Tab[data-state="off"]:where(:not([disabled])):focus-visible {
|
|
391
|
+
color: var(--color-text);
|
|
392
|
+
outline: 2px solid var(--color-ring);
|
|
393
|
+
outline-offset: -2px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* =============================================
|
|
397
|
+
Vertical orientation — Tab styles
|
|
398
|
+
============================================= */
|
|
399
|
+
:where(.Tabs[data-orientation="vertical"]) .Tab {
|
|
400
|
+
justify-content: flex-start;
|
|
401
|
+
width: 100%;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
:where(.Tabs[data-orientation="vertical"][data-block]) .Tab {
|
|
405
|
+
flex: none;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* =============================================
|
|
409
|
+
Shared states
|
|
410
|
+
============================================= */
|
|
411
|
+
.Tab[data-state="on"] {
|
|
322
412
|
color: var(--color-text);
|
|
323
413
|
}
|
|
324
414
|
|
|
325
|
-
.
|
|
415
|
+
.Tab[data-state="on"]:focus-visible {
|
|
416
|
+
outline: 2px solid var(--color-ring);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.Tab[data-disabled] {
|
|
326
420
|
cursor: not-allowed;
|
|
327
421
|
opacity: 0.5;
|
|
328
422
|
}
|
|
329
423
|
|
|
330
|
-
.
|
|
424
|
+
.Tab[data-disabled]::before {
|
|
331
425
|
opacity: 0 !important;
|
|
332
|
-
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.Tab svg {
|
|
429
|
+
display: block;
|
|
430
|
+
}/* =============================================
|
|
431
|
+
Thumb (animated indicator)
|
|
432
|
+
============================================= */.TabsThumb {
|
|
333
433
|
position: absolute;
|
|
334
|
-
top: var(--segmented-control-gutter);
|
|
335
|
-
bottom: var(--segmented-control-gutter);
|
|
336
|
-
left: 0;
|
|
337
|
-
border-radius: var(--segmented-control-option-radius);
|
|
338
|
-
background: var(--segmented-control-thumb-background);
|
|
339
|
-
box-shadow: var(--segmented-control-thumb-shadow);
|
|
340
434
|
pointer-events: none;
|
|
341
435
|
will-change: transform;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* Segmented horizontal: background highlight */
|
|
439
|
+
:where(.Tabs[data-variant="segmented"][data-orientation="horizontal"]) .TabsThumb {
|
|
440
|
+
top: var(--segmented-control-gutter);
|
|
441
|
+
bottom: var(--segmented-control-gutter);
|
|
442
|
+
left: 0;
|
|
443
|
+
border-radius: var(--segmented-control-option-radius);
|
|
444
|
+
background: var(--segmented-control-thumb-background);
|
|
445
|
+
box-shadow: var(--segmented-control-thumb-shadow);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Segmented vertical: background highlight */
|
|
449
|
+
:where(.Tabs[data-variant="segmented"][data-orientation="vertical"]) .TabsThumb {
|
|
450
|
+
left: var(--segmented-control-gutter);
|
|
451
|
+
right: var(--segmented-control-gutter);
|
|
452
|
+
top: 0;
|
|
453
|
+
border-radius: var(--segmented-control-option-radius);
|
|
454
|
+
background: var(--segmented-control-thumb-background);
|
|
455
|
+
box-shadow: var(--segmented-control-thumb-shadow);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* Underline horizontal: thin line at the bottom */
|
|
459
|
+
:where(.Tabs[data-variant="underline"][data-orientation="horizontal"]) .TabsThumb {
|
|
460
|
+
bottom: 0;
|
|
461
|
+
left: 0;
|
|
462
|
+
height: var(--tabs-underline-indicator-height);
|
|
463
|
+
border-radius: calc(var(--tabs-underline-indicator-height) / 2) calc(var(--tabs-underline-indicator-height) / 2) 0 0;
|
|
464
|
+
background: var(--tabs-underline-indicator-color);
|
|
465
|
+
box-shadow: none;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* Underline vertical: thin line on the left */
|
|
469
|
+
:where(.Tabs[data-variant="underline"][data-orientation="vertical"]) .TabsThumb {
|
|
470
|
+
left: 0;
|
|
471
|
+
top: 0;
|
|
472
|
+
width: var(--tabs-underline-indicator-height);
|
|
473
|
+
border-radius: 0 calc(var(--tabs-underline-indicator-height) / 2) calc(var(--tabs-underline-indicator-height) / 2) 0;
|
|
474
|
+
background: var(--tabs-underline-indicator-color);
|
|
475
|
+
box-shadow: none;
|
|
476
|
+
}/* =============================================
|
|
477
|
+
Tab Content (icon + text + badge layout)
|
|
478
|
+
============================================= */.TabContent {
|
|
345
479
|
position: relative;
|
|
346
480
|
display: flex;
|
|
347
481
|
align-items: center;
|
|
@@ -349,17 +483,17 @@
|
|
|
349
483
|
}
|
|
350
484
|
|
|
351
485
|
/* Icon auto-sizing (same as Button) */
|
|
352
|
-
.
|
|
486
|
+
.TabContent svg:where(:not([data-no-autosize])) {
|
|
353
487
|
width: var(--segmented-control-icon-size);
|
|
354
488
|
height: var(--segmented-control-icon-size);
|
|
355
489
|
}
|
|
356
490
|
|
|
357
491
|
/* Negative margin for icon when followed by text */
|
|
358
|
-
.
|
|
492
|
+
.TabContent svg:where(:first-child:not(:only-child)) {
|
|
359
493
|
margin-left: var(--segmented-control-icon-offset);
|
|
360
494
|
}/* =============================================
|
|
361
|
-
|
|
362
|
-
============================================= */.
|
|
495
|
+
Tab Badge (CSS-only, no Badge component)
|
|
496
|
+
============================================= */.TabBadge {
|
|
363
497
|
display: inline-flex;
|
|
364
498
|
align-items: center;
|
|
365
499
|
height: var(--option-badge-size);
|
|
@@ -377,37 +511,37 @@
|
|
|
377
511
|
/* =============================================
|
|
378
512
|
Soft variant (default)
|
|
379
513
|
============================================= */
|
|
380
|
-
.
|
|
514
|
+
.TabBadge[data-variant="soft"]:where([data-color="secondary"]) {
|
|
381
515
|
--option-badge-bg: var(--color-background-secondary-soft-alpha);
|
|
382
516
|
--option-badge-color: var(--color-text-secondary-soft);
|
|
383
517
|
}
|
|
384
518
|
|
|
385
|
-
.
|
|
519
|
+
.TabBadge[data-variant="soft"]:where([data-color="success"]) {
|
|
386
520
|
--option-badge-bg: var(--color-background-success-soft-alpha);
|
|
387
521
|
--option-badge-color: var(--color-text-success-soft);
|
|
388
522
|
}
|
|
389
523
|
|
|
390
|
-
.
|
|
524
|
+
.TabBadge[data-variant="soft"]:where([data-color="warning"]) {
|
|
391
525
|
--option-badge-bg: var(--color-background-warning-soft-alpha);
|
|
392
526
|
--option-badge-color: var(--color-text-warning-soft);
|
|
393
527
|
}
|
|
394
528
|
|
|
395
|
-
.
|
|
529
|
+
.TabBadge[data-variant="soft"]:where([data-color="danger"]) {
|
|
396
530
|
--option-badge-bg: var(--color-background-danger-soft-alpha);
|
|
397
531
|
--option-badge-color: var(--color-text-danger-soft);
|
|
398
532
|
}
|
|
399
533
|
|
|
400
|
-
.
|
|
534
|
+
.TabBadge[data-variant="soft"]:where([data-color="info"]) {
|
|
401
535
|
--option-badge-bg: var(--color-background-info-soft-alpha);
|
|
402
536
|
--option-badge-color: var(--color-text-info-soft);
|
|
403
537
|
}
|
|
404
538
|
|
|
405
|
-
.
|
|
539
|
+
.TabBadge[data-variant="soft"]:where([data-color="discovery"]) {
|
|
406
540
|
--option-badge-bg: var(--color-background-discovery-soft-alpha);
|
|
407
541
|
--option-badge-color: var(--color-text-discovery-soft);
|
|
408
542
|
}
|
|
409
543
|
|
|
410
|
-
.
|
|
544
|
+
.TabBadge[data-variant="soft"]:where([data-color="caution"]) {
|
|
411
545
|
--option-badge-bg: var(--color-background-caution-soft-alpha);
|
|
412
546
|
--option-badge-color: var(--color-text-caution-soft);
|
|
413
547
|
}
|
|
@@ -415,37 +549,37 @@
|
|
|
415
549
|
/* =============================================
|
|
416
550
|
Solid variant
|
|
417
551
|
============================================= */
|
|
418
|
-
.
|
|
552
|
+
.TabBadge[data-variant="solid"]:where([data-color="secondary"]) {
|
|
419
553
|
--option-badge-bg: var(--color-background-secondary-solid);
|
|
420
554
|
--option-badge-color: var(--color-text-secondary-solid);
|
|
421
555
|
}
|
|
422
556
|
|
|
423
|
-
.
|
|
557
|
+
.TabBadge[data-variant="solid"]:where([data-color="success"]) {
|
|
424
558
|
--option-badge-bg: var(--color-background-success-solid);
|
|
425
559
|
--option-badge-color: var(--color-text-success-solid);
|
|
426
560
|
}
|
|
427
561
|
|
|
428
|
-
.
|
|
562
|
+
.TabBadge[data-variant="solid"]:where([data-color="warning"]) {
|
|
429
563
|
--option-badge-bg: var(--color-background-warning-solid);
|
|
430
564
|
--option-badge-color: var(--color-text-warning-solid);
|
|
431
565
|
}
|
|
432
566
|
|
|
433
|
-
.
|
|
567
|
+
.TabBadge[data-variant="solid"]:where([data-color="danger"]) {
|
|
434
568
|
--option-badge-bg: var(--color-background-danger-solid);
|
|
435
569
|
--option-badge-color: var(--color-text-danger-solid);
|
|
436
570
|
}
|
|
437
571
|
|
|
438
|
-
.
|
|
572
|
+
.TabBadge[data-variant="solid"]:where([data-color="info"]) {
|
|
439
573
|
--option-badge-bg: var(--color-background-info-solid);
|
|
440
574
|
--option-badge-color: var(--color-text-info-solid);
|
|
441
575
|
}
|
|
442
576
|
|
|
443
|
-
.
|
|
577
|
+
.TabBadge[data-variant="solid"]:where([data-color="discovery"]) {
|
|
444
578
|
--option-badge-bg: var(--color-background-discovery-solid);
|
|
445
579
|
--option-badge-color: var(--color-text-discovery-solid);
|
|
446
580
|
}
|
|
447
581
|
|
|
448
|
-
.
|
|
582
|
+
.TabBadge[data-variant="solid"]:where([data-color="caution"]) {
|
|
449
583
|
--option-badge-bg: var(--color-background-caution-solid);
|
|
450
584
|
--option-badge-color: var(--color-text-caution-solid);
|
|
451
585
|
}
|
|
@@ -453,42 +587,42 @@
|
|
|
453
587
|
/* =============================================
|
|
454
588
|
Outline variant
|
|
455
589
|
============================================= */
|
|
456
|
-
.
|
|
590
|
+
.TabBadge[data-variant="outline"] {
|
|
457
591
|
background-color: transparent;
|
|
458
592
|
box-shadow: 0 0 0 1px var(--option-badge-border) inset;
|
|
459
593
|
}
|
|
460
594
|
|
|
461
|
-
.
|
|
595
|
+
.TabBadge[data-variant="outline"]:where([data-color="secondary"]) {
|
|
462
596
|
--option-badge-border: var(--color-border-secondary-outline);
|
|
463
597
|
--option-badge-color: var(--color-text-secondary-outline);
|
|
464
598
|
}
|
|
465
599
|
|
|
466
|
-
.
|
|
600
|
+
.TabBadge[data-variant="outline"]:where([data-color="success"]) {
|
|
467
601
|
--option-badge-border: var(--color-border-success-outline);
|
|
468
602
|
--option-badge-color: var(--color-text-success-outline);
|
|
469
603
|
}
|
|
470
604
|
|
|
471
|
-
.
|
|
605
|
+
.TabBadge[data-variant="outline"]:where([data-color="warning"]) {
|
|
472
606
|
--option-badge-border: var(--color-border-warning-outline);
|
|
473
607
|
--option-badge-color: var(--color-text-warning-outline);
|
|
474
608
|
}
|
|
475
609
|
|
|
476
|
-
.
|
|
610
|
+
.TabBadge[data-variant="outline"]:where([data-color="danger"]) {
|
|
477
611
|
--option-badge-border: var(--color-border-danger-outline);
|
|
478
612
|
--option-badge-color: var(--color-text-danger-outline);
|
|
479
613
|
}
|
|
480
614
|
|
|
481
|
-
.
|
|
615
|
+
.TabBadge[data-variant="outline"]:where([data-color="info"]) {
|
|
482
616
|
--option-badge-border: var(--color-border-info-outline);
|
|
483
617
|
--option-badge-color: var(--color-text-info-outline);
|
|
484
618
|
}
|
|
485
619
|
|
|
486
|
-
.
|
|
620
|
+
.TabBadge[data-variant="outline"]:where([data-color="discovery"]) {
|
|
487
621
|
--option-badge-border: var(--color-border-discovery-outline);
|
|
488
622
|
--option-badge-color: var(--color-text-discovery-outline);
|
|
489
623
|
}
|
|
490
624
|
|
|
491
|
-
.
|
|
625
|
+
.TabBadge[data-variant="outline"]:where([data-color="caution"]) {
|
|
492
626
|
--option-badge-border: var(--color-border-caution-outline);
|
|
493
627
|
--option-badge-color: var(--color-text-caution-outline);
|
|
494
628
|
}
|
|
@@ -496,7 +630,7 @@
|
|
|
496
630
|
/* =============================================
|
|
497
631
|
Pill
|
|
498
632
|
============================================= */
|
|
499
|
-
.
|
|
633
|
+
.TabBadge[data-pill] {
|
|
500
634
|
border-radius: var(--radius-full);
|
|
501
635
|
padding: 0 calc(var(--option-badge-gutter) * var(--control-gutter-pill-scaling));
|
|
502
636
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/Tabs/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA","sourcesContent":["export { Tabs } from \"./Tabs\"\nexport type {\n TabsBadgeProp,\n TabsOrientation,\n TabsProps,\n TabsVariant,\n TabProps,\n SizeVariant,\n} from \"./Tabs\"\n"]}
|
|
@@ -216,6 +216,13 @@
|
|
|
216
216
|
--segmented-control-thumb-shadow: 0 1px 4px -1px rgb(0 0 0 / 20%);
|
|
217
217
|
--segmented-control-option-highlight-gutter: 1px;
|
|
218
218
|
|
|
219
|
+
/* =============================================
|
|
220
|
+
Tabs — Underline variant
|
|
221
|
+
============================================= */
|
|
222
|
+
--tabs-underline-indicator-height: 2px;
|
|
223
|
+
--tabs-underline-indicator-color: var(--color-background-primary-solid);
|
|
224
|
+
--tabs-underline-border-color: var(--color-border);
|
|
225
|
+
|
|
219
226
|
/* =============================================
|
|
220
227
|
SelectControl
|
|
221
228
|
============================================= */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { SegmentedControl } from "
|
|
2
|
-
export type { SegmentedControlBadgeProp, SegmentedControlOptionProps, SegmentedControlProps, SizeVariant, } from "
|
|
1
|
+
export { Tabs as SegmentedControl } from "../Tabs";
|
|
2
|
+
export type { TabsBadgeProp as SegmentedControlBadgeProp, TabProps as SegmentedControlOptionProps, TabsProps as SegmentedControlProps, SizeVariant, } from "../Tabs";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ControlSize, type SemanticColors, type Sizes, type Variants } from "../../types";
|
|
2
2
|
export type SizeVariant = "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
|
|
3
|
-
export type
|
|
3
|
+
export type TabsVariant = "segmented" | "underline";
|
|
4
|
+
export type TabsOrientation = "horizontal" | "vertical";
|
|
5
|
+
export type TabsProps<T extends string> = {
|
|
4
6
|
/**
|
|
5
7
|
* Controlled value for the group
|
|
6
8
|
*/
|
|
@@ -14,7 +16,19 @@ export type SegmentedControlProps<T extends string> = {
|
|
|
14
16
|
*/
|
|
15
17
|
"aria-label": string;
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
19
|
+
* Visual variant of the tab group
|
|
20
|
+
* - `"segmented"` — background container with sliding highlight (default)
|
|
21
|
+
* - `"underline"` — no background, animated line indicator under active tab
|
|
22
|
+
* @default "segmented"
|
|
23
|
+
*/
|
|
24
|
+
"variant"?: TabsVariant;
|
|
25
|
+
/**
|
|
26
|
+
* Orientation of the tab layout
|
|
27
|
+
* @default "horizontal"
|
|
28
|
+
*/
|
|
29
|
+
"orientation"?: TabsOrientation;
|
|
30
|
+
/**
|
|
31
|
+
* Controls the size of the tabs
|
|
18
32
|
*
|
|
19
33
|
* | 3xs | 2xs | xs | sm | md | lg | xl | 2xl | 3xl |
|
|
20
34
|
* | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- |
|
|
@@ -39,38 +53,40 @@ export type SegmentedControlProps<T extends string> = {
|
|
|
39
53
|
*/
|
|
40
54
|
"block"?: boolean;
|
|
41
55
|
/**
|
|
42
|
-
* Determines if the
|
|
56
|
+
* Determines if the tabs should be a fully rounded pill shape.
|
|
57
|
+
* Only applies to the `"segmented"` variant.
|
|
43
58
|
* @default true
|
|
44
59
|
*/
|
|
45
60
|
"pill"?: boolean;
|
|
46
61
|
"className"?: string;
|
|
47
62
|
"children": React.ReactNode;
|
|
48
63
|
};
|
|
49
|
-
export declare const
|
|
50
|
-
<T extends string>({ value, onChange, children, block, pill, size, gutterSize, className, onClick, ...restProps }:
|
|
51
|
-
|
|
64
|
+
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;
|
|
66
|
+
Tab: ({ children, icon, badge, ...restProps }: TabProps) => import("react/jsx-runtime").JSX.Element;
|
|
67
|
+
Option: ({ children, icon, badge, ...restProps }: TabProps) => import("react/jsx-runtime").JSX.Element;
|
|
52
68
|
};
|
|
53
69
|
/**
|
|
54
|
-
* Badge configuration for
|
|
70
|
+
* Badge configuration for Tabs.Tab
|
|
55
71
|
*/
|
|
56
|
-
export type
|
|
72
|
+
export type TabsBadgeProp = React.ReactNode | {
|
|
57
73
|
content: React.ReactNode;
|
|
58
74
|
color?: SemanticColors<"secondary" | "success" | "danger" | "warning" | "info" | "discovery" | "caution">;
|
|
59
75
|
variant?: Variants<"soft" | "solid">;
|
|
60
76
|
pill?: boolean;
|
|
61
77
|
loading?: boolean;
|
|
62
78
|
};
|
|
63
|
-
export type
|
|
79
|
+
export type TabProps = {
|
|
64
80
|
/**
|
|
65
|
-
*
|
|
81
|
+
* Tab value
|
|
66
82
|
*/
|
|
67
83
|
"value": string;
|
|
68
84
|
/**
|
|
69
|
-
* Text read aloud to screen readers when the
|
|
85
|
+
* Text read aloud to screen readers when the tab is focused
|
|
70
86
|
*/
|
|
71
87
|
"aria-label"?: string;
|
|
72
88
|
/**
|
|
73
|
-
* Text content to render in the
|
|
89
|
+
* Text content to render in the tab
|
|
74
90
|
*/
|
|
75
91
|
"children"?: React.ReactNode;
|
|
76
92
|
/**
|
|
@@ -83,9 +99,9 @@ export type SegmentedControlOptionProps = {
|
|
|
83
99
|
* @example badge={5}
|
|
84
100
|
* @example badge={{ content: 5, color: "danger" }}
|
|
85
101
|
*/
|
|
86
|
-
"badge"?:
|
|
102
|
+
"badge"?: TabsBadgeProp;
|
|
87
103
|
/**
|
|
88
|
-
* Disable the individual
|
|
104
|
+
* Disable the individual tab
|
|
89
105
|
*/
|
|
90
106
|
"disabled"?: boolean;
|
|
91
107
|
};
|
package/package.json
CHANGED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import clsx from "clsx";
|
|
4
|
-
import { ToggleGroup } from "radix-ui";
|
|
5
|
-
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
6
|
-
import { useResizeObserver } from "usehooks-ts";
|
|
7
|
-
import { handlePressableMouseEnter, waitForAnimationFrame } from "../../lib/helpers";
|
|
8
|
-
import {} from "../../types";
|
|
9
|
-
import { LoadingIndicator } from "../Indicator";
|
|
10
|
-
import s from "./SegmentedControl.module.css";
|
|
11
|
-
export const SegmentedControl = ({ value, onChange, children, block, pill = true, size = "md", gutterSize, className, onClick, ...restProps }) => {
|
|
12
|
-
const rootRef = useRef(null);
|
|
13
|
-
const thumbRef = useRef(null);
|
|
14
|
-
const prevSizeRef = useRef(size);
|
|
15
|
-
const applyThumbSizing = useCallback((attemptScroll) => {
|
|
16
|
-
const root = rootRef.current;
|
|
17
|
-
const thumb = thumbRef.current;
|
|
18
|
-
if (!root || !thumb) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
// Get selected node
|
|
22
|
-
const activeNode = root?.querySelector('[data-state="on"]');
|
|
23
|
-
// Impossible
|
|
24
|
-
if (!activeNode) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const rootWidth = root.clientWidth;
|
|
28
|
-
let targetWidth = Math.floor(activeNode.clientWidth);
|
|
29
|
-
const targetOffset = activeNode.offsetLeft;
|
|
30
|
-
// Detect if the thumb is moving too far to the edge of the container.
|
|
31
|
-
// This would most commonly be due to subpixel widths adding up to excessive distance.
|
|
32
|
-
if (rootWidth - (targetWidth + targetOffset) < 2) {
|
|
33
|
-
targetWidth = targetWidth - 1;
|
|
34
|
-
}
|
|
35
|
-
thumb.style.width = `${Math.floor(targetWidth)}px`;
|
|
36
|
-
thumb.style.transform = `translateX(${targetOffset}px)`;
|
|
37
|
-
// If the control is scrollable, ensure the active option is visible
|
|
38
|
-
if (root.scrollWidth > rootWidth) {
|
|
39
|
-
// Only scroll items near the edge, but not the inner 2/3.
|
|
40
|
-
const buffer = rootWidth * 0.15;
|
|
41
|
-
const scrollLeft = root.scrollLeft;
|
|
42
|
-
const left = activeNode.offsetLeft;
|
|
43
|
-
const right = left + targetWidth;
|
|
44
|
-
if (left < scrollLeft + buffer || right > scrollLeft + rootWidth - buffer) {
|
|
45
|
-
// Cheap trick to avoid unintentional scroll on mount - transition is set after mounting
|
|
46
|
-
if (attemptScroll) {
|
|
47
|
-
activeNode.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}, []);
|
|
52
|
-
useResizeObserver({
|
|
53
|
-
// @ts-expect-error(2322) -- bug in types: https://github.com/juliencrn/usehooks-ts/issues/663
|
|
54
|
-
ref: rootRef,
|
|
55
|
-
onResize: () => {
|
|
56
|
-
const thumb = thumbRef.current;
|
|
57
|
-
if (!thumb) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
// Perform the size update instantly
|
|
61
|
-
const currentTransition = thumb.style.transition;
|
|
62
|
-
thumb.style.transition = "";
|
|
63
|
-
applyThumbSizing(false);
|
|
64
|
-
thumb.style.transition = currentTransition;
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
useLayoutEffect(() => {
|
|
68
|
-
const root = rootRef.current;
|
|
69
|
-
const thumb = thumbRef.current;
|
|
70
|
-
if (!root || !thumb) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const sizeChanged = prevSizeRef.current !== size;
|
|
74
|
-
prevSizeRef.current = size;
|
|
75
|
-
if (sizeChanged) {
|
|
76
|
-
// Size changed - disable transition, wait for CSS, then apply sizing
|
|
77
|
-
const currentTransition = thumb.style.transition;
|
|
78
|
-
thumb.style.transition = "";
|
|
79
|
-
waitForAnimationFrame(() => {
|
|
80
|
-
applyThumbSizing(false);
|
|
81
|
-
waitForAnimationFrame(() => {
|
|
82
|
-
thumb.style.transition = currentTransition;
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
// Normal update (value change, etc.)
|
|
88
|
-
waitForAnimationFrame(() => {
|
|
89
|
-
applyThumbSizing(!!thumb.style.transition);
|
|
90
|
-
// Apply transition after initial calculation is set
|
|
91
|
-
if (!thumb.style.transition) {
|
|
92
|
-
waitForAnimationFrame(() => {
|
|
93
|
-
thumb.style.transition =
|
|
94
|
-
"width 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)";
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}, [applyThumbSizing, value, size, gutterSize, pill]);
|
|
100
|
-
const handleValueChange = (nextValue) => {
|
|
101
|
-
// Only trigger onChange when a value exists
|
|
102
|
-
// Disallow toggling off enabled items
|
|
103
|
-
if (nextValue && onChange)
|
|
104
|
-
onChange(nextValue);
|
|
105
|
-
};
|
|
106
|
-
return (_jsxs(ToggleGroup.Root, { ref: rootRef, className: clsx(s.SegmentedControl, className), type: "single", value: value, loop: false, onValueChange: handleValueChange, onClick: onClick, "data-block": block ? "" : undefined, "data-pill": pill ? "" : undefined, "data-size": size, "data-gutter-size": gutterSize, ...restProps, children: [_jsx("div", { className: s.SegmentedControlThumb, ref: thumbRef }), children] }));
|
|
107
|
-
};
|
|
108
|
-
// Type guard for badge object form
|
|
109
|
-
const isBadgeObject = (badge) => {
|
|
110
|
-
return badge != null && typeof badge === "object" && "content" in badge;
|
|
111
|
-
};
|
|
112
|
-
const Segment = ({ children, icon, badge, ...restProps }) => {
|
|
113
|
-
// Normalize badge prop
|
|
114
|
-
const badgeProps = badge != null ? (isBadgeObject(badge) ? badge : { content: badge }) : null;
|
|
115
|
-
return (_jsx(ToggleGroup.Item, { className: s.SegmentedControlOption, ...restProps, onPointerEnter: handlePressableMouseEnter, children: _jsxs("span", { className: s.SegmentedControlOptionContent, children: [icon, children && _jsx("span", { children: children }), badgeProps && (_jsx("span", { className: s.OptionBadge, "data-color": badgeProps.color ?? "secondary", "data-variant": badgeProps.variant ?? "soft", "data-pill": badgeProps.pill ? "" : undefined, children: badgeProps.loading ? _jsx(LoadingIndicator, {}) : badgeProps.content }))] }) }));
|
|
116
|
-
};
|
|
117
|
-
SegmentedControl.Option = Segment;
|
|
118
|
-
//# sourceMappingURL=SegmentedControl.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentedControl.js","sourceRoot":"","sources":["../../../../src/components/SegmentedControl/SegmentedControl.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 \"./SegmentedControl.module.css\"\n\nexport type SizeVariant = \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type SegmentedControlProps<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 * Controls the size of the segmented control\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 segment control, and its options, should be a fully rounded pill shape.\n * @default true\n */\n \"pill\"?: boolean\n \"className\"?: string\n \"children\": React.ReactNode\n}\n\nexport const SegmentedControl = <T extends string>({\n value,\n onChange,\n children,\n block,\n pill = true,\n size = \"md\",\n gutterSize,\n className,\n onClick,\n ...restProps\n}: SegmentedControlProps<T>) => {\n const rootRef = useRef<HTMLDivElement>(null)\n const thumbRef = useRef<HTMLDivElement>(null)\n const prevSizeRef = useRef(size)\n\n const applyThumbSizing = useCallback((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 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.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 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 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 =\n \"width 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)\"\n })\n }\n })\n }\n }, [applyThumbSizing, value, size, gutterSize, pill])\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 return (\n <ToggleGroup.Root\n ref={rootRef}\n className={clsx(s.SegmentedControl, className)}\n type=\"single\"\n value={value}\n loop={false}\n onValueChange={handleValueChange}\n onClick={onClick}\n data-block={block ? \"\" : undefined}\n data-pill={pill ? \"\" : undefined}\n data-size={size}\n data-gutter-size={gutterSize}\n {...restProps}\n >\n <div className={s.SegmentedControlThumb} ref={thumbRef} />\n {children}\n </ToggleGroup.Root>\n )\n}\n\n/**\n * Badge configuration for SegmentedControl.Option\n */\nexport type SegmentedControlBadgeProp =\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 SegmentedControlOptionProps = {\n /**\n * Option value\n */\n \"value\": string\n /**\n * Text read aloud to screen readers when the option is focused\n */\n \"aria-label\"?: string\n /**\n * Text content to render in the option\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\"?: SegmentedControlBadgeProp\n /**\n * Disable the individual option\n */\n \"disabled\"?: boolean\n}\n\n// Type guard for badge object form\nconst isBadgeObject = (\n badge: SegmentedControlBadgeProp,\n): badge is Exclude<SegmentedControlBadgeProp, React.ReactNode> & { content: React.ReactNode } => {\n return badge != null && typeof badge === \"object\" && \"content\" in badge\n}\n\nconst Segment = ({ children, icon, badge, ...restProps }: SegmentedControlOptionProps) => {\n // Normalize badge prop\n const badgeProps = badge != null ? (isBadgeObject(badge) ? badge : { content: badge }) : null\n\n return (\n <ToggleGroup.Item\n className={s.SegmentedControlOption}\n {...restProps}\n onPointerEnter={handlePressableMouseEnter}\n >\n <span className={s.SegmentedControlOptionContent}>\n {icon}\n {children && <span>{children}</span>}\n {badgeProps && (\n <span\n className={s.OptionBadge}\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\nSegmentedControl.Option = Segment\n"]}
|