@plexui/ui 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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} +238 -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,113 @@
|
|
|
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 {
|
|
21
|
+
background: transparent;
|
|
22
|
+
}
|
|
23
|
+
|
|
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"]: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
|
+
|
|
46
|
+
.Tabs:where([data-variant="segmented"][data-block]) {
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
display: flex;
|
|
49
|
+
width: 100%;
|
|
50
|
+
white-space: normal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* =============================================
|
|
54
|
+
Variant: Underline
|
|
55
|
+
============================================= */
|
|
56
|
+
.Tabs:where([data-variant="underline"]) {
|
|
57
|
+
--segmented-control-option-radius: 0;
|
|
58
|
+
|
|
59
|
+
overflow: auto;
|
|
60
|
+
height: var(--segmented-control-size);
|
|
61
|
+
padding: 0;
|
|
62
|
+
border-radius: 0;
|
|
31
63
|
background: transparent;
|
|
64
|
+
gap: 0;
|
|
65
|
+
border-bottom: 1px solid var(--tabs-underline-border-color);
|
|
32
66
|
}
|
|
33
67
|
|
|
34
|
-
.
|
|
68
|
+
.Tabs:where([data-variant="underline"][data-block]) {
|
|
35
69
|
overflow: hidden;
|
|
36
70
|
display: flex;
|
|
37
71
|
width: 100%;
|
|
38
72
|
white-space: normal;
|
|
39
73
|
}
|
|
40
74
|
|
|
75
|
+
/* =============================================
|
|
76
|
+
Vertical orientation
|
|
77
|
+
============================================= */
|
|
78
|
+
.Tabs:where([data-orientation="vertical"]) {
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
height: auto;
|
|
81
|
+
width: -moz-fit-content;
|
|
82
|
+
width: fit-content;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.Tabs:where([data-orientation="vertical"][data-variant="segmented"]) {
|
|
87
|
+
overflow-y: auto;
|
|
88
|
+
overflow-x: hidden;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.Tabs:where([data-orientation="vertical"][data-variant="underline"]) {
|
|
92
|
+
overflow-y: auto;
|
|
93
|
+
overflow-x: hidden;
|
|
94
|
+
border-bottom: none;
|
|
95
|
+
border-left: 1px solid var(--tabs-underline-border-color);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.Tabs:where([data-orientation="vertical"][data-block]) {
|
|
99
|
+
width: 100%;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.Tabs:where([data-orientation="vertical"][data-variant="segmented"][data-pill]) {
|
|
103
|
+
border-radius: calc(var(--segmented-control-size) / 2);
|
|
104
|
+
--segmented-control-option-radius: calc(var(--segmented-control-size) / 2);
|
|
105
|
+
}
|
|
106
|
+
|
|
41
107
|
/* =============================================
|
|
42
108
|
Sizes
|
|
43
109
|
============================================= */
|
|
44
|
-
.
|
|
110
|
+
.Tabs:where([data-size="3xs"]) {
|
|
45
111
|
--segmented-control-size: var(--control-size-3xs);
|
|
46
112
|
--segmented-control-font-size: var(--control-font-size-sm);
|
|
47
113
|
--segmented-control-radius: var(--control-radius-sm);
|
|
@@ -60,7 +126,7 @@
|
|
|
60
126
|
--option-badge-radius: var(--badge-radius-2xs);
|
|
61
127
|
}
|
|
62
128
|
|
|
63
|
-
.
|
|
129
|
+
.Tabs:where([data-size="2xs"]) {
|
|
64
130
|
--segmented-control-size: var(--control-size-2xs);
|
|
65
131
|
--segmented-control-font-size: var(--control-font-size-sm);
|
|
66
132
|
--segmented-control-radius: var(--control-radius-sm);
|
|
@@ -79,7 +145,7 @@
|
|
|
79
145
|
--option-badge-radius: var(--badge-radius-2xs);
|
|
80
146
|
}
|
|
81
147
|
|
|
82
|
-
.
|
|
148
|
+
.Tabs:where([data-size="xs"]) {
|
|
83
149
|
--segmented-control-size: var(--control-size-xs);
|
|
84
150
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
85
151
|
--segmented-control-radius: var(--control-radius-sm);
|
|
@@ -98,7 +164,7 @@
|
|
|
98
164
|
--option-badge-radius: var(--badge-radius-xs);
|
|
99
165
|
}
|
|
100
166
|
|
|
101
|
-
.
|
|
167
|
+
.Tabs:where([data-size="sm"]) {
|
|
102
168
|
--segmented-control-size: var(--control-size-sm);
|
|
103
169
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
104
170
|
--segmented-control-radius: var(--control-radius-md);
|
|
@@ -117,7 +183,7 @@
|
|
|
117
183
|
--option-badge-radius: var(--badge-radius-sm);
|
|
118
184
|
}
|
|
119
185
|
|
|
120
|
-
.
|
|
186
|
+
.Tabs:where([data-size="md"]) {
|
|
121
187
|
--segmented-control-size: var(--control-size-md);
|
|
122
188
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
123
189
|
--segmented-control-radius: var(--control-radius-md);
|
|
@@ -136,7 +202,7 @@
|
|
|
136
202
|
--option-badge-radius: var(--badge-radius-md);
|
|
137
203
|
}
|
|
138
204
|
|
|
139
|
-
.
|
|
205
|
+
.Tabs:where([data-size="lg"]) {
|
|
140
206
|
--segmented-control-size: var(--control-size-lg);
|
|
141
207
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
142
208
|
--segmented-control-radius: var(--control-radius-md);
|
|
@@ -155,7 +221,7 @@
|
|
|
155
221
|
--option-badge-radius: var(--badge-radius-md);
|
|
156
222
|
}
|
|
157
223
|
|
|
158
|
-
.
|
|
224
|
+
.Tabs:where([data-size="xl"]) {
|
|
159
225
|
--segmented-control-size: var(--control-size-xl);
|
|
160
226
|
--segmented-control-font-size: var(--control-font-size-md);
|
|
161
227
|
--segmented-control-radius: var(--control-radius-lg);
|
|
@@ -174,7 +240,7 @@
|
|
|
174
240
|
--option-badge-radius: var(--badge-radius-md);
|
|
175
241
|
}
|
|
176
242
|
|
|
177
|
-
.
|
|
243
|
+
.Tabs:where([data-size="2xl"]) {
|
|
178
244
|
--segmented-control-size: var(--control-size-2xl);
|
|
179
245
|
--segmented-control-font-size: var(--control-font-size-lg);
|
|
180
246
|
--segmented-control-radius: var(--control-radius-xl);
|
|
@@ -193,7 +259,7 @@
|
|
|
193
259
|
--option-badge-radius: var(--badge-radius-lg);
|
|
194
260
|
}
|
|
195
261
|
|
|
196
|
-
.
|
|
262
|
+
.Tabs:where([data-size="3xl"]) {
|
|
197
263
|
--segmented-control-size: var(--control-size-3xl);
|
|
198
264
|
--segmented-control-font-size: var(--control-font-size-lg);
|
|
199
265
|
--segmented-control-radius: var(--control-radius-xl);
|
|
@@ -213,42 +279,35 @@
|
|
|
213
279
|
}
|
|
214
280
|
|
|
215
281
|
/* =============================================
|
|
216
|
-
Gutter sizes
|
|
282
|
+
Gutter sizes (segmented variant only)
|
|
217
283
|
============================================= */
|
|
218
|
-
.
|
|
284
|
+
.Tabs:where([data-gutter-size="2xs"]) {
|
|
219
285
|
--segmented-control-option-gutter: var(--control-gutter-2xs);
|
|
220
286
|
}
|
|
221
287
|
|
|
222
|
-
.
|
|
288
|
+
.Tabs:where([data-gutter-size="xs"]) {
|
|
223
289
|
--segmented-control-option-gutter: var(--control-gutter-xs);
|
|
224
290
|
}
|
|
225
291
|
|
|
226
|
-
.
|
|
292
|
+
.Tabs:where([data-gutter-size="sm"]) {
|
|
227
293
|
--segmented-control-option-gutter: var(--control-gutter-sm);
|
|
228
294
|
}
|
|
229
295
|
|
|
230
|
-
.
|
|
296
|
+
.Tabs:where([data-gutter-size="md"]) {
|
|
231
297
|
--segmented-control-option-gutter: var(--control-gutter-md);
|
|
232
298
|
}
|
|
233
299
|
|
|
234
|
-
.
|
|
300
|
+
.Tabs:where([data-gutter-size="lg"]) {
|
|
235
301
|
--segmented-control-option-gutter: var(--control-gutter-lg);
|
|
236
302
|
}
|
|
237
303
|
|
|
238
|
-
.
|
|
304
|
+
.Tabs:where([data-gutter-size="xl"]) {
|
|
239
305
|
--segmented-control-option-gutter: var(--control-gutter-xl);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
Pill
|
|
244
|
-
============================================= */
|
|
245
|
-
.SegmentedControl:where([data-pill]) {
|
|
246
|
-
--segmented-control-radius: var(--radius-full);
|
|
247
|
-
--segmented-control-option-radius: var(--radius-full);
|
|
248
|
-
}.SegmentedControlOption {
|
|
306
|
+
}/* =============================================
|
|
307
|
+
Tab (trigger item)
|
|
308
|
+
============================================= */.Tab {
|
|
249
309
|
position: relative;
|
|
250
310
|
padding: 0 var(--segmented-control-option-gutter);
|
|
251
|
-
border-radius: var(--segmented-control-option-radius);
|
|
252
311
|
color: var(--color-text-secondary);
|
|
253
312
|
cursor: pointer;
|
|
254
313
|
line-height: 1;
|
|
@@ -261,24 +320,28 @@
|
|
|
261
320
|
transition-timing-function: var(--transition-ease-basic);
|
|
262
321
|
}
|
|
263
322
|
|
|
264
|
-
.
|
|
323
|
+
.Tab:focus {
|
|
265
324
|
outline: 0;
|
|
266
325
|
}
|
|
267
326
|
|
|
268
|
-
|
|
327
|
+
/* =============================================
|
|
328
|
+
Segmented variant — Tab styles
|
|
329
|
+
============================================= */
|
|
330
|
+
:where(.Tabs[data-variant="segmented"]) .Tab {
|
|
331
|
+
border-radius: var(--segmented-control-option-radius);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
:where(.Tabs[data-variant="segmented"][data-block]) .Tab {
|
|
269
335
|
flex: 1;
|
|
270
336
|
}
|
|
271
337
|
|
|
272
|
-
:where(.
|
|
338
|
+
:where(.Tabs[data-variant="segmented"][data-pill]) .Tab {
|
|
273
339
|
min-width: calc(var(--segmented-control-size) - 2 * var(--segmented-control-gutter));
|
|
274
340
|
padding: 0 calc(var(--segmented-control-option-gutter) * var(--control-gutter-pill-scaling));
|
|
275
341
|
}
|
|
276
342
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.SegmentedControlOption::before {
|
|
343
|
+
/* Hover highlight pseudo-element (segmented only) */
|
|
344
|
+
:where(.Tabs[data-variant="segmented"]) .Tab::before {
|
|
282
345
|
position: absolute;
|
|
283
346
|
inset: var(--segmented-control-option-highlight-gutter);
|
|
284
347
|
border-radius: var(--segmented-control-option-radius);
|
|
@@ -293,55 +356,132 @@
|
|
|
293
356
|
will-change: transform;
|
|
294
357
|
}
|
|
295
358
|
|
|
296
|
-
.
|
|
359
|
+
:where(.Tabs[data-variant="segmented"]) .Tab:active::before {
|
|
297
360
|
transform: scale(var(--scale), 0.97);
|
|
298
361
|
}
|
|
299
|
-
|
|
300
|
-
.SegmentedControlOption svg {
|
|
301
|
-
display: block;
|
|
302
|
-
}
|
|
303
|
-
@media (hover: hover) and (pointer: fine) {.SegmentedControlOption[data-state="off"]:where(:not([disabled])):hover {
|
|
362
|
+
@media (hover: hover) and (pointer: fine) {:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):hover {
|
|
304
363
|
color: var(--color-text);
|
|
305
364
|
}
|
|
306
365
|
|
|
307
|
-
.
|
|
366
|
+
:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):hover::before {
|
|
308
367
|
opacity: 0.5;
|
|
309
368
|
}
|
|
310
369
|
}
|
|
311
370
|
|
|
312
|
-
.
|
|
371
|
+
:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):focus-visible {
|
|
313
372
|
color: var(--color-text);
|
|
314
373
|
outline: 2px solid var(--color-ring);
|
|
315
374
|
}
|
|
316
375
|
|
|
317
|
-
.
|
|
376
|
+
:where(.Tabs[data-variant="segmented"]) .Tab[data-state="off"]:where(:not([disabled])):active::before {
|
|
318
377
|
opacity: 0.75;
|
|
319
378
|
}
|
|
320
379
|
|
|
321
|
-
|
|
380
|
+
/* =============================================
|
|
381
|
+
Underline variant — Tab styles
|
|
382
|
+
============================================= */
|
|
383
|
+
:where(.Tabs[data-variant="underline"]) .Tab {
|
|
384
|
+
border-radius: 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
:where(.Tabs[data-variant="underline"][data-block]) .Tab {
|
|
388
|
+
flex: 1;
|
|
389
|
+
}
|
|
390
|
+
@media (hover: hover) and (pointer: fine) {:where(.Tabs[data-variant="underline"]) .Tab[data-state="off"]:where(:not([disabled])):hover {
|
|
391
|
+
color: var(--color-text);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
:where(.Tabs[data-variant="underline"]) .Tab[data-state="off"]:where(:not([disabled])):focus-visible {
|
|
396
|
+
color: var(--color-text);
|
|
397
|
+
outline: 2px solid var(--color-ring);
|
|
398
|
+
outline-offset: -2px;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* =============================================
|
|
402
|
+
Vertical orientation — Tab styles
|
|
403
|
+
============================================= */
|
|
404
|
+
:where(.Tabs[data-orientation="vertical"]) .Tab {
|
|
405
|
+
justify-content: flex-start;
|
|
406
|
+
width: 100%;
|
|
407
|
+
height: var(--segmented-control-size);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
:where(.Tabs[data-orientation="vertical"][data-block]) .Tab {
|
|
411
|
+
flex: none;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* =============================================
|
|
415
|
+
Shared states
|
|
416
|
+
============================================= */
|
|
417
|
+
.Tab[data-state="on"] {
|
|
322
418
|
color: var(--color-text);
|
|
323
419
|
}
|
|
324
420
|
|
|
325
|
-
.
|
|
421
|
+
.Tab[data-state="on"]:focus-visible {
|
|
422
|
+
outline: 2px solid var(--color-ring);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.Tab[data-disabled] {
|
|
326
426
|
cursor: not-allowed;
|
|
327
427
|
opacity: 0.5;
|
|
328
428
|
}
|
|
329
429
|
|
|
330
|
-
.
|
|
430
|
+
.Tab[data-disabled]::before {
|
|
331
431
|
opacity: 0 !important;
|
|
332
|
-
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.Tab svg {
|
|
435
|
+
display: block;
|
|
436
|
+
}/* =============================================
|
|
437
|
+
Thumb (animated indicator)
|
|
438
|
+
============================================= */.TabsThumb {
|
|
333
439
|
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
440
|
pointer-events: none;
|
|
341
441
|
will-change: transform;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Segmented horizontal: background highlight */
|
|
445
|
+
:where(.Tabs[data-variant="segmented"][data-orientation="horizontal"]) .TabsThumb {
|
|
446
|
+
top: var(--segmented-control-gutter);
|
|
447
|
+
bottom: var(--segmented-control-gutter);
|
|
448
|
+
left: 0;
|
|
449
|
+
border-radius: var(--segmented-control-option-radius);
|
|
450
|
+
background: var(--segmented-control-thumb-background);
|
|
451
|
+
box-shadow: var(--segmented-control-thumb-shadow);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Segmented vertical: background highlight */
|
|
455
|
+
:where(.Tabs[data-variant="segmented"][data-orientation="vertical"]) .TabsThumb {
|
|
456
|
+
left: var(--segmented-control-gutter);
|
|
457
|
+
right: var(--segmented-control-gutter);
|
|
458
|
+
top: 0;
|
|
459
|
+
border-radius: var(--segmented-control-option-radius);
|
|
460
|
+
background: var(--segmented-control-thumb-background);
|
|
461
|
+
box-shadow: var(--segmented-control-thumb-shadow);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* Underline horizontal: thin line at the bottom */
|
|
465
|
+
:where(.Tabs[data-variant="underline"][data-orientation="horizontal"]) .TabsThumb {
|
|
466
|
+
bottom: 0;
|
|
467
|
+
left: 0;
|
|
468
|
+
height: var(--tabs-underline-indicator-height);
|
|
469
|
+
border-radius: calc(var(--tabs-underline-indicator-height) / 2) calc(var(--tabs-underline-indicator-height) / 2) 0 0;
|
|
470
|
+
background: var(--tabs-underline-indicator-color);
|
|
471
|
+
box-shadow: none;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* Underline vertical: thin line on the left */
|
|
475
|
+
:where(.Tabs[data-variant="underline"][data-orientation="vertical"]) .TabsThumb {
|
|
476
|
+
left: 0;
|
|
477
|
+
top: 0;
|
|
478
|
+
width: var(--tabs-underline-indicator-height);
|
|
479
|
+
border-radius: 0 calc(var(--tabs-underline-indicator-height) / 2) calc(var(--tabs-underline-indicator-height) / 2) 0;
|
|
480
|
+
background: var(--tabs-underline-indicator-color);
|
|
481
|
+
box-shadow: none;
|
|
482
|
+
}/* =============================================
|
|
483
|
+
Tab Content (icon + text + badge layout)
|
|
484
|
+
============================================= */.TabContent {
|
|
345
485
|
position: relative;
|
|
346
486
|
display: flex;
|
|
347
487
|
align-items: center;
|
|
@@ -349,17 +489,17 @@
|
|
|
349
489
|
}
|
|
350
490
|
|
|
351
491
|
/* Icon auto-sizing (same as Button) */
|
|
352
|
-
.
|
|
492
|
+
.TabContent svg:where(:not([data-no-autosize])) {
|
|
353
493
|
width: var(--segmented-control-icon-size);
|
|
354
494
|
height: var(--segmented-control-icon-size);
|
|
355
495
|
}
|
|
356
496
|
|
|
357
497
|
/* Negative margin for icon when followed by text */
|
|
358
|
-
.
|
|
498
|
+
.TabContent svg:where(:first-child:not(:only-child)) {
|
|
359
499
|
margin-left: var(--segmented-control-icon-offset);
|
|
360
500
|
}/* =============================================
|
|
361
|
-
|
|
362
|
-
============================================= */.
|
|
501
|
+
Tab Badge (CSS-only, no Badge component)
|
|
502
|
+
============================================= */.TabBadge {
|
|
363
503
|
display: inline-flex;
|
|
364
504
|
align-items: center;
|
|
365
505
|
height: var(--option-badge-size);
|
|
@@ -377,37 +517,37 @@
|
|
|
377
517
|
/* =============================================
|
|
378
518
|
Soft variant (default)
|
|
379
519
|
============================================= */
|
|
380
|
-
.
|
|
520
|
+
.TabBadge[data-variant="soft"]:where([data-color="secondary"]) {
|
|
381
521
|
--option-badge-bg: var(--color-background-secondary-soft-alpha);
|
|
382
522
|
--option-badge-color: var(--color-text-secondary-soft);
|
|
383
523
|
}
|
|
384
524
|
|
|
385
|
-
.
|
|
525
|
+
.TabBadge[data-variant="soft"]:where([data-color="success"]) {
|
|
386
526
|
--option-badge-bg: var(--color-background-success-soft-alpha);
|
|
387
527
|
--option-badge-color: var(--color-text-success-soft);
|
|
388
528
|
}
|
|
389
529
|
|
|
390
|
-
.
|
|
530
|
+
.TabBadge[data-variant="soft"]:where([data-color="warning"]) {
|
|
391
531
|
--option-badge-bg: var(--color-background-warning-soft-alpha);
|
|
392
532
|
--option-badge-color: var(--color-text-warning-soft);
|
|
393
533
|
}
|
|
394
534
|
|
|
395
|
-
.
|
|
535
|
+
.TabBadge[data-variant="soft"]:where([data-color="danger"]) {
|
|
396
536
|
--option-badge-bg: var(--color-background-danger-soft-alpha);
|
|
397
537
|
--option-badge-color: var(--color-text-danger-soft);
|
|
398
538
|
}
|
|
399
539
|
|
|
400
|
-
.
|
|
540
|
+
.TabBadge[data-variant="soft"]:where([data-color="info"]) {
|
|
401
541
|
--option-badge-bg: var(--color-background-info-soft-alpha);
|
|
402
542
|
--option-badge-color: var(--color-text-info-soft);
|
|
403
543
|
}
|
|
404
544
|
|
|
405
|
-
.
|
|
545
|
+
.TabBadge[data-variant="soft"]:where([data-color="discovery"]) {
|
|
406
546
|
--option-badge-bg: var(--color-background-discovery-soft-alpha);
|
|
407
547
|
--option-badge-color: var(--color-text-discovery-soft);
|
|
408
548
|
}
|
|
409
549
|
|
|
410
|
-
.
|
|
550
|
+
.TabBadge[data-variant="soft"]:where([data-color="caution"]) {
|
|
411
551
|
--option-badge-bg: var(--color-background-caution-soft-alpha);
|
|
412
552
|
--option-badge-color: var(--color-text-caution-soft);
|
|
413
553
|
}
|
|
@@ -415,37 +555,37 @@
|
|
|
415
555
|
/* =============================================
|
|
416
556
|
Solid variant
|
|
417
557
|
============================================= */
|
|
418
|
-
.
|
|
558
|
+
.TabBadge[data-variant="solid"]:where([data-color="secondary"]) {
|
|
419
559
|
--option-badge-bg: var(--color-background-secondary-solid);
|
|
420
560
|
--option-badge-color: var(--color-text-secondary-solid);
|
|
421
561
|
}
|
|
422
562
|
|
|
423
|
-
.
|
|
563
|
+
.TabBadge[data-variant="solid"]:where([data-color="success"]) {
|
|
424
564
|
--option-badge-bg: var(--color-background-success-solid);
|
|
425
565
|
--option-badge-color: var(--color-text-success-solid);
|
|
426
566
|
}
|
|
427
567
|
|
|
428
|
-
.
|
|
568
|
+
.TabBadge[data-variant="solid"]:where([data-color="warning"]) {
|
|
429
569
|
--option-badge-bg: var(--color-background-warning-solid);
|
|
430
570
|
--option-badge-color: var(--color-text-warning-solid);
|
|
431
571
|
}
|
|
432
572
|
|
|
433
|
-
.
|
|
573
|
+
.TabBadge[data-variant="solid"]:where([data-color="danger"]) {
|
|
434
574
|
--option-badge-bg: var(--color-background-danger-solid);
|
|
435
575
|
--option-badge-color: var(--color-text-danger-solid);
|
|
436
576
|
}
|
|
437
577
|
|
|
438
|
-
.
|
|
578
|
+
.TabBadge[data-variant="solid"]:where([data-color="info"]) {
|
|
439
579
|
--option-badge-bg: var(--color-background-info-solid);
|
|
440
580
|
--option-badge-color: var(--color-text-info-solid);
|
|
441
581
|
}
|
|
442
582
|
|
|
443
|
-
.
|
|
583
|
+
.TabBadge[data-variant="solid"]:where([data-color="discovery"]) {
|
|
444
584
|
--option-badge-bg: var(--color-background-discovery-solid);
|
|
445
585
|
--option-badge-color: var(--color-text-discovery-solid);
|
|
446
586
|
}
|
|
447
587
|
|
|
448
|
-
.
|
|
588
|
+
.TabBadge[data-variant="solid"]:where([data-color="caution"]) {
|
|
449
589
|
--option-badge-bg: var(--color-background-caution-solid);
|
|
450
590
|
--option-badge-color: var(--color-text-caution-solid);
|
|
451
591
|
}
|
|
@@ -453,42 +593,42 @@
|
|
|
453
593
|
/* =============================================
|
|
454
594
|
Outline variant
|
|
455
595
|
============================================= */
|
|
456
|
-
.
|
|
596
|
+
.TabBadge[data-variant="outline"] {
|
|
457
597
|
background-color: transparent;
|
|
458
598
|
box-shadow: 0 0 0 1px var(--option-badge-border) inset;
|
|
459
599
|
}
|
|
460
600
|
|
|
461
|
-
.
|
|
601
|
+
.TabBadge[data-variant="outline"]:where([data-color="secondary"]) {
|
|
462
602
|
--option-badge-border: var(--color-border-secondary-outline);
|
|
463
603
|
--option-badge-color: var(--color-text-secondary-outline);
|
|
464
604
|
}
|
|
465
605
|
|
|
466
|
-
.
|
|
606
|
+
.TabBadge[data-variant="outline"]:where([data-color="success"]) {
|
|
467
607
|
--option-badge-border: var(--color-border-success-outline);
|
|
468
608
|
--option-badge-color: var(--color-text-success-outline);
|
|
469
609
|
}
|
|
470
610
|
|
|
471
|
-
.
|
|
611
|
+
.TabBadge[data-variant="outline"]:where([data-color="warning"]) {
|
|
472
612
|
--option-badge-border: var(--color-border-warning-outline);
|
|
473
613
|
--option-badge-color: var(--color-text-warning-outline);
|
|
474
614
|
}
|
|
475
615
|
|
|
476
|
-
.
|
|
616
|
+
.TabBadge[data-variant="outline"]:where([data-color="danger"]) {
|
|
477
617
|
--option-badge-border: var(--color-border-danger-outline);
|
|
478
618
|
--option-badge-color: var(--color-text-danger-outline);
|
|
479
619
|
}
|
|
480
620
|
|
|
481
|
-
.
|
|
621
|
+
.TabBadge[data-variant="outline"]:where([data-color="info"]) {
|
|
482
622
|
--option-badge-border: var(--color-border-info-outline);
|
|
483
623
|
--option-badge-color: var(--color-text-info-outline);
|
|
484
624
|
}
|
|
485
625
|
|
|
486
|
-
.
|
|
626
|
+
.TabBadge[data-variant="outline"]:where([data-color="discovery"]) {
|
|
487
627
|
--option-badge-border: var(--color-border-discovery-outline);
|
|
488
628
|
--option-badge-color: var(--color-text-discovery-outline);
|
|
489
629
|
}
|
|
490
630
|
|
|
491
|
-
.
|
|
631
|
+
.TabBadge[data-variant="outline"]:where([data-color="caution"]) {
|
|
492
632
|
--option-badge-border: var(--color-border-caution-outline);
|
|
493
633
|
--option-badge-color: var(--color-text-caution-outline);
|
|
494
634
|
}
|
|
@@ -496,7 +636,7 @@
|
|
|
496
636
|
/* =============================================
|
|
497
637
|
Pill
|
|
498
638
|
============================================= */
|
|
499
|
-
.
|
|
639
|
+
.TabBadge[data-pill] {
|
|
500
640
|
border-radius: var(--radius-full);
|
|
501
641
|
padding: 0 calc(var(--option-badge-gutter) * var(--control-gutter-pill-scaling));
|
|
502
642
|
}
|
|
@@ -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":"AAAA,YAAY,CAAA;;AAEZ,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AACpF,OAAO,EAAoE,MAAM,aAAa,CAAA;AAC9F,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,CAAC,MAAM,+BAA+B,CAAA;AAmD7C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAmB,EACjD,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,IAAI,GAAG,IAAI,EACX,IAAI,GAAG,IAAI,EACX,UAAU,EACV,SAAS,EACT,OAAO,EACP,GAAG,SAAS,EACa,EAAE,EAAE;IAC7B,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IAC5C,MAAM,QAAQ,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IAC7C,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAEhC,MAAM,gBAAgB,GAAG,WAAW,CAAC,CAAC,aAAsB,EAAE,EAAE;QAC9D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAA;QAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAA;QAE9B,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACpB,OAAM;QACR,CAAC;QAED,oBAAoB;QACpB,MAAM,UAAU,GAAG,IAAI,EAAE,aAAa,CAAiB,mBAAmB,CAAC,CAAA;QAE3E,aAAa;QACb,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAM;QACR,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAA;QAClC,IAAI,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACpD,MAAM,YAAY,GAAG,UAAU,CAAC,UAAU,CAAA;QAE1C,sEAAsE;QACtE,sFAAsF;QACtF,IAAI,SAAS,GAAG,CAAC,WAAW,GAAG,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;YACjD,WAAW,GAAG,WAAW,GAAG,CAAC,CAAA;QAC/B,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAA;QAClD,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,cAAc,YAAY,KAAK,CAAA;QAEvD,oEAAoE;QACpE,IAAI,IAAI,CAAC,WAAW,GAAG,SAAS,EAAE,CAAC;YACjC,0DAA0D;YAC1D,MAAM,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;YAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAA;YAClC,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,CAAA;YAClC,MAAM,KAAK,GAAG,IAAI,GAAG,WAAW,CAAA;YAChC,IAAI,IAAI,GAAG,UAAU,GAAG,MAAM,IAAI,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC;gBAC1E,wFAAwF;gBACxF,IAAI,aAAa,EAAE,CAAC;oBAClB,UAAU,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;gBACvF,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,iBAAiB,CAAC;QAChB,8FAA8F;QAC9F,GAAG,EAAE,OAAO;QACZ,QAAQ,EAAE,GAAG,EAAE;YACb,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAA;YAE9B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAM;YACR,CAAC;YAED,oCAAoC;YACpC,MAAM,iBAAiB,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,CAAA;YAChD,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAA;YAC3B,gBAAgB,CAAC,KAAK,CAAC,CAAA;YACvB,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,iBAAiB,CAAA;QAC5C,CAAC;KACF,CAAC,CAAA;IAEF,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAA;QAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAA;QAE9B,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACpB,OAAM;QACR,CAAC;QAED,MAAM,WAAW,GAAG,WAAW,CAAC,OAAO,KAAK,IAAI,CAAA;QAChD,WAAW,CAAC,OAAO,GAAG,IAAI,CAAA;QAE1B,IAAI,WAAW,EAAE,CAAC;YAChB,qEAAqE;YACrE,MAAM,iBAAiB,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,CAAA;YAChD,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAA;YAE3B,qBAAqB,CAAC,GAAG,EAAE;gBACzB,gBAAgB,CAAC,KAAK,CAAC,CAAA;gBACvB,qBAAqB,CAAC,GAAG,EAAE;oBACzB,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,iBAAiB,CAAA;gBAC5C,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;QACJ,CAAC;aAAM,CAAC;YACN,qCAAqC;YACrC,qBAAqB,CAAC,GAAG,EAAE;gBACzB,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;gBAE1C,oDAAoD;gBACpD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;oBAC5B,qBAAqB,CAAC,GAAG,EAAE;wBACzB,KAAK,CAAC,KAAK,CAAC,UAAU;4BACpB,oEAAoE,CAAA;oBACxE,CAAC,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,EAAE,CAAC,gBAAgB,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC,CAAA;IAErD,MAAM,iBAAiB,GAAG,CAAC,SAAY,EAAE,EAAE;QACzC,4CAA4C;QAC5C,sCAAsC;QACtC,IAAI,SAAS,IAAI,QAAQ;YAAE,QAAQ,CAAC,SAAS,CAAC,CAAA;IAChD,CAAC,CAAA;IAED,OAAO,CACL,MAAC,WAAW,CAAC,IAAI,IACf,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,gBAAgB,EAAE,SAAS,CAAC,EAC9C,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,KAAK,EACX,aAAa,EAAE,iBAAiB,EAChC,OAAO,EAAE,OAAO,gBACJ,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,eACvB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,eACrB,IAAI,sBACG,UAAU,KACxB,SAAS,aAEb,cAAK,SAAS,EAAE,CAAC,CAAC,qBAAqB,EAAE,GAAG,EAAE,QAAQ,GAAI,EACzD,QAAQ,IACQ,CACpB,CAAA;AACH,CAAC,CAAA;AA+CD,mCAAmC;AACnC,MAAM,aAAa,GAAG,CACpB,KAAgC,EAC6D,EAAE;IAC/F,OAAO,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,SAAS,IAAI,KAAK,CAAA;AACzE,CAAC,CAAA;AAED,MAAM,OAAO,GAAG,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,SAAS,EAA+B,EAAE,EAAE;IACvF,uBAAuB;IACvB,MAAM,UAAU,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAE7F,OAAO,CACL,KAAC,WAAW,CAAC,IAAI,IACf,SAAS,EAAE,CAAC,CAAC,sBAAsB,KAC/B,SAAS,EACb,cAAc,EAAE,yBAAyB,YAEzC,gBAAM,SAAS,EAAE,CAAC,CAAC,6BAA6B,aAC7C,IAAI,EACJ,QAAQ,IAAI,yBAAO,QAAQ,GAAQ,EACnC,UAAU,IAAI,CACb,eACE,SAAS,EAAE,CAAC,CAAC,WAAW,gBACZ,UAAU,CAAC,KAAK,IAAI,WAAW,kBAC7B,UAAU,CAAC,OAAO,IAAI,MAAM,eAC/B,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,YAE1C,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,KAAC,gBAAgB,KAAG,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,GAC1D,CACR,IACI,GACU,CACpB,CAAA;AACH,CAAC,CAAA;AAED,gBAAgB,CAAC,MAAM,GAAG,OAAO,CAAA","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"]}
|